307 lines
10 KiB
Python
307 lines
10 KiB
Python
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
|
|
|
|
import os.path
|
|
|
|
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.CharField(max_length=100, blank=True)
|
|
#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}:{slugify(self.work.name)}>"
|
|
|
|
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.
|
|
"""
|
|
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, default='Anon',
|
|
help_text="Surname, 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")
|
|
|
|
@property
|
|
def folder(self):
|
|
return f"{slugify(self.composer)}/{slugify(self.name)}-{self.pk:04d}"
|
|
|
|
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')
|
|
|
|
@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]
|
|
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.folder}/{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 delete(self, *args, **kwargs):
|
|
self.upload.delete(save=False)
|
|
return super().delete(*args, **kwargs)
|
|
|
|
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}]' |