from os import SCHED_OTHER from django.conf import settings from django.db import models from django.utils.text import slugify from django.utils.timezone import now from django.utils.functional import cached_property from django.core.files.storage import get_storage_class from django.db.models import Q, Count, Min, Max from byostorage.user import BYOStorage from .imslp import Instrument import logging #from polyphonic.settings import LIBRARY_STORAGE logger = logging.getLogger(__name__) #try: # library_storage = get_storage_class(settings.LIBRARY_STORAGE)() #except (ImportError, AttributeError): # logger.exception("Failed to load library storage") # library_storage = get_storage_class()() #logger.info("Library storage: %s", library_storage.__class__.__name__) ''' ORCHESTRATIONS = { 'SATB': ('S', 'A', 'T', 'B'), 'String Quartet': ('Vln1', 'Vln2', 'Vla', 'Vc'), 'String Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb'), 'Chamber Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb', 'Fl1', 'Fl2', 'Cl1', 'Cl2', 'Hn1', 'Hn2', 'Tpt1', 'Tpt2', 'Tbn1', 'Tbn2', 'Tuba', 'Timp', 'Drum', 'Perc'), 'Custom': (), } ''' DOCTYPES = [ (1, 'PDF'), (2, 'Audio'), (3, 'Video'), (4, 'Source'), ] LICENCE_TYPES = [ (2, 'Public Domain'), (4, 'Copyright Expired'), (6, 'Copyrighted'), (10, 'Internal use only'), ] ACCESS_TYPES = [ (1, 'Unlimited'), (2, 'Approval required'), ] ''' class Orchestration(models.Model): """ Stores a list of instrument codes as a single entry (space delimited). Can be global or ensemble specific """ ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="orchestrations", null=True, blank=True) name = models.CharField(max_length=100) instruments = models.TextField() def as_list(self): tags = [ t.strip() for t in self.instruments.split(' ') ] return [ (t, tag_to_instrument(t)) for t in tags if t ] def save(self): self.as_list() super(Orchestration, self).save() def __str__(self): return self.name ''' class ProjectItem(models.Model): """ ProjectItem represents a Work attached to a Project e.g. item in set list or programme It also allows works to be shared from one ensemble to another on a per-project basis. """ project = models.ForeignKey('interface.Project', on_delete=models.CASCADE, related_name='items') work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name='project_items') checkout = models.DateTimeField() due = models.DateTimeField(null=True, blank=True) returned = models.DateTimeField(null=True, blank=True) approved_by = models.ForeignKey('auth.User', on_delete=models.CASCADE) order = models.SmallIntegerField(default=0) section = models.SlugField #version = models.CharField(max_length=30, blank=True, help_text="Limited to specific version tag") class Meta: ordering = ['order', 'work'] def __str__(self): return f"<{self.project_id}:{self.work.slug}>" class Collection(models.Model): """ A logical collection of works, typically owned by an organisation or person (physical or virtual) """ name = models.CharField(max_length=255, help_text="Name of the collection") prefix = models.SlugField(max_length=30, default="default", help_text="Folder to store works in") administrators = models.ManyToManyField('auth.User', related_name="collections", help_text="Administrators for this collection") location = models.CharField(max_length=100, help_text="Physical location (institution, town...)", blank=True) storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True, help_text="User storage for documents") notes = models.TextField(blank=True, help_text="Publicly visible notes about collection and loans policy (markdown format)") def __str__(self): return self.name class EnsembleAccess(models.Model): """ Can have different access levels to a collection """ ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="allowed_collections") collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="allowed_ensembles") access_type = models.PositiveSmallIntegerField(choices=ACCESS_TYPES, default=2) class Meta: verbose_name_plural = "Ensemble access" META_TAGS = ( ('tag', 'Tag'), ('arr', 'Arranger'), ('lyrics', 'Lyracist'), ('genre', 'Genre'), ('style', 'Style'), ('orchestration', 'Orchestration'), ) class Work(models.Model): """ A musical work 'owned' by a collection from a licencing perspective. """ slug = models.SlugField(max_length=100, editable=False, help_text="Used as folder name") name = models.CharField(max_length=255, help_text="Original name of the work") edition = models.CharField(max_length=255, blank=True, help_text="Edition details to distinguish multiple versions") parent = models.ForeignKey('Work', null=True, blank=True, on_delete=models.SET_NULL, related_name="related_works", help_text="Arrangement of another work or part of an anthology") composer = models.CharField(max_length=255, blank=True, help_text="Surname, First Name/Initials") original_parts = models.JSONField(default=dict, blank=True, help_text="Original printed parts (IMSLP format)") # Collection details collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works") code = models.CharField(max_length=100, blank=True, help_text="Collection specific code or number") licence = models.PositiveSmallIntegerField(choices=LICENCE_TYPES, default=6, help_text="Copyright status") max_projects = models.IntegerField(default=1, help_text="How many active projects can this work be attached to") # Extra info running_time = models.DurationField(null=True, blank=True, help_text="Running time in seconds") notes = models.TextField(blank=True) # Allocation to projects projects = models.ManyToManyField('interface.Project', through='ProjectItem', related_name="works", help_text="Current usage") def extract(self, *tags): qs = self.docs.filter(sections__tag__in=tags) qs = qs.annotate(Count('sections'), end=Min('sections__end'), start=Max('sections__start')) \ .filter(sections__count=len(tags)) return list(qs.values_list('upload', 'start', 'end')) @property def digital_parts(self): return Section.objects.filter(doc__work=self.pk) @property def physical_parts(self): if not self.original_parts: return [] return [ (Instrument.from_tag(k), v) for (k, v) in self.original_parts.items() ] @property def tags(self): return self.meta_info.filter(name='tag').values_list('value', flat=True) @property def meta(self): return self.meta_info.exclude(name='tag') def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super(Work, self).save(*args, **kwargs) @property def active_projects(self): return self.projects.filter(active=True) @property def current_loans(self): return self.project_items.filter(checkout__lte=now(), returned=None).select_related('project') @cached_property def loans(self): try: return self.loan_count except AttributeError: return self.project_items.filter(checkout__lte=now(), returned=None).count() @property def is_available(self): if self.max_projects < 0: return True return self.max_projects > self.loans @property def available(self): if self.max_projects < 0: return 'Unlimited' a = self.max_projects - self.loans return '{0} of {1}'.format(max(a, 0), self.max_projects) @property def identifier(self): if self.code: return self.code; composer = self.composer or "Anon" words = self.name.split() if len(words) > 2: work = ''.join([ x[0] for x in self.name.split() ]) else: work = words[0][:3] return f"{composer[:4]}-{work}-{self.pk:03d}".upper() def __str__(self): return f"{self.name} ({self.composer})" class WorkMeta(models.Model): work = models.ForeignKey(Work, on_delete=models.CASCADE, related_name='meta_info') name = models.SlugField(max_length=20, choices=META_TAGS) value = models.CharField(max_length=255) def doc_upload_filename(doc, filename): collection = doc.work.collection storage = collection.storage if not storage: raise RuntimeError("Collection has no storage attached") return f'{storage}:library/{collection.prefix}/{doc.work.slug}-{doc.work.pk}/{filename}' class Document(models.Model): """ Document represents a single file stored in the storage backend. """ work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs") doctype = models.PositiveSmallIntegerField(choices=DOCTYPES, default=1) upload = models.FileField(upload_to=doc_upload_filename, storage=BYOStorage()) created = models.DateTimeField(auto_now_add=True) version = models.CharField(max_length=30, blank=True) def __str__(self): return self.upload.name class Section(models.Model): """ Section is a tagged portion of a Document """ doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="sections") tag = models.CharField(max_length=50) start = models.SmallIntegerField(null=True, blank=True) end = models.SmallIntegerField(null=True, blank=True) class Meta: ordering = ['doc', 'start', 'pk'] @property def instrument(self): return Instrument.from_tag(self.tag) @property def filename(self): return slugify(f'{self.doc.work.name}_{self.instrument}') + '.pdf' @property def pagerange(self): if self.start: if self.end: return f"{self.start}-{self.end}" return str(self.start) return "all" def __str__(self): return f'{self.doc.upload} [{self.pagerange}]'