Got library sort of working
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
|
||||
# Model overview
|
||||
|
||||
Collections contain Works
|
||||
Only the collection's administrators can add/remove/edit items
|
||||
Collections can be enabled for Ensembles which makes everything availble in the Library
|
||||
|
||||
|
||||
## How do loans work?
|
||||
|
||||
1. Project admins need to be able to search for available music - list with access type
|
||||
2. Item can be attached to Project
|
||||
|
||||
+28
-11
@@ -2,17 +2,29 @@ from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
class OrchestrationAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'ensemble']
|
||||
list_filter = ['ensemble']
|
||||
#class OrchestrationAdmin(admin.ModelAdmin):
|
||||
# list_display = ['name', 'ensemble']
|
||||
# list_filter = ['ensemble']
|
||||
|
||||
#admin.site.register(models.Orchestration, OrchestrationAdmin)
|
||||
|
||||
class CollectionAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'location']
|
||||
|
||||
admin.site.register(models.Collection, CollectionAdmin)
|
||||
|
||||
class ItemInline(admin.TabularInline):
|
||||
model = models.Item
|
||||
|
||||
class DocInline(admin.TabularInline):
|
||||
model = models.Document
|
||||
|
||||
class WorkAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'composer', 'orchestration']
|
||||
list_filter = ['ensemble']
|
||||
inlines = [ItemInline]
|
||||
list_display = ['name', 'edition', 'composer', 'running_time']
|
||||
list_filter = ['collection']
|
||||
inlines = [DocInline, ItemInline]
|
||||
|
||||
admin.site.register(models.Work, WorkAdmin)
|
||||
|
||||
class PartInline(admin.TabularInline):
|
||||
model = models.Part
|
||||
@@ -20,14 +32,19 @@ class PartInline(admin.TabularInline):
|
||||
|
||||
class DocumentAdmin(admin.ModelAdmin):
|
||||
list_display = ['work', '__str__']
|
||||
list_filter = ['work__ensemble']
|
||||
list_filter = ['work__collection']
|
||||
inlines = [PartInline]
|
||||
|
||||
admin.site.register(models.Document, DocumentAdmin)
|
||||
|
||||
class ItemAdmin(admin.ModelAdmin):
|
||||
list_display = ['project', 'work', 'order']
|
||||
list_filter = ['project']
|
||||
|
||||
admin.site.register(models.Orchestration, OrchestrationAdmin)
|
||||
admin.site.register(models.Work, WorkAdmin)
|
||||
admin.site.register(models.Document, DocumentAdmin)
|
||||
admin.site.register(models.Item, ItemAdmin)
|
||||
admin.site.register(models.Item, ItemAdmin)
|
||||
|
||||
class EnsembleAccessAdmin(admin.ModelAdmin):
|
||||
list_display = ['ensemble', 'collection', 'access_type']
|
||||
list_filter = ['ensemble']
|
||||
|
||||
admin.site.register(models.EnsembleAccess, EnsembleAccessAdmin)
|
||||
+9
-3
@@ -1,12 +1,15 @@
|
||||
from django import forms
|
||||
from .models import Work
|
||||
from interface.models import Project
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
class WorkCreateForm(forms.ModelForm):
|
||||
uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True}))
|
||||
uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True}), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Work
|
||||
fields = ['uploads', 'name', 'orchestration', 'running_time', 'notes']
|
||||
fields = ['uploads', 'name', 'composer', 'edition', 'collection', 'code', 'running_time', 'notes']
|
||||
|
||||
class PlaylistAddForm(forms.Form):
|
||||
work = forms.ModelChoiceField(queryset=Work.objects.all())
|
||||
@@ -21,4 +24,7 @@ class PlaylistAddForm(forms.Form):
|
||||
self.instance = instance
|
||||
|
||||
def save(self):
|
||||
self.instance.works.add(self.cleaned_data['work'])
|
||||
self.instance.works.add(self.cleaned_data['work'])
|
||||
|
||||
class ProjectSelectForm(forms.Form):
|
||||
project = forms.ModelChoiceField(queryset=Project.objects.all())
|
||||
@@ -0,0 +1,20 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
import argparse
|
||||
import csv
|
||||
|
||||
from library import models
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Imports works from a csv file'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('collection', type=int, help="Collection ID")
|
||||
parser.add_argument('source', type=argparse.FileType('r'), help="Source CSV")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
collection = models.Collection.objects.get(pk=options['collection'])
|
||||
|
||||
reader = csv.DictReader(options['source'])
|
||||
for row in reader:
|
||||
collection.works.create(name=row['Piece'], composer=row['Composer'], notes=row['Notes'])
|
||||
@@ -1,9 +1,10 @@
|
||||
# Generated by Django 3.1.1 on 2021-03-11 07:07
|
||||
# Generated by Django 3.1.1 on 2021-04-28 03:53
|
||||
|
||||
import byostorage.cached
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import library.models
|
||||
import byostorage.cached
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -11,14 +12,29 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('interface', '0022_auto_20210303_2043'),
|
||||
('interface', '0027_auto_20210322_1154'),
|
||||
('byostorage', '0003_auto_20210323_1047'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Collection',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('location', models.CharField(help_text='Physical location', max_length=100)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('administrators', models.ManyToManyField(help_text='Administrators for this collection', related_name='collections', to=settings.AUTH_USER_MODEL)),
|
||||
('ensembles', models.ManyToManyField(related_name='collections', to='interface.Ensemble')),
|
||||
('storage', models.ForeignKey(blank=True, help_text='Storage for documents', null=True, on_delete=django.db.models.deletion.CASCADE, to='byostorage.userstorage')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Document',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('doctype', models.PositiveSmallIntegerField(choices=[(1, 'PDF'), (2, 'Audio'), (3, 'Video'), (4, 'Source')], default=1)),
|
||||
('upload', models.FileField(storage=byostorage.cached.CachedStorage(), upload_to=library.models.doc_upload_filename)),
|
||||
],
|
||||
),
|
||||
@@ -48,15 +64,19 @@ class Migration(migrations.Migration):
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(editable=False, max_length=100)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('running_time', models.IntegerField(blank=True, null=True)),
|
||||
('version', models.CharField(blank=True, help_text='Version or edition details', max_length=100)),
|
||||
('composer', models.CharField(blank=True, help_text='Use Composer / Arranger format', max_length=255)),
|
||||
('code', models.CharField(blank=True, help_text='Collection specific code or number', max_length=100)),
|
||||
('licence', models.PositiveSmallIntegerField(choices=[(2, 'Public Domain'), (4, 'Copyright Expired'), (6, 'Copyrighted'), (10, 'Internal use only')], default=6, help_text='Copyright status')),
|
||||
('max_loans', models.BooleanField(default=1, help_text='How many projects can this work be attached to')),
|
||||
('running_time', models.IntegerField(blank=True, help_text='Running time in seconds', null=True)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='interface.ensemble')),
|
||||
('tag_list', models.CharField(blank=True, help_text='Multiple tags for the work', max_length=255)),
|
||||
('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.collection')),
|
||||
('orchestration', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.orchestration')),
|
||||
('projects', models.ManyToManyField(related_name='works', through='library.Item', to='interface.Project')),
|
||||
('parent', models.ForeignKey(blank=True, help_text='Arrangement of another work or part of an anthology', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='related_works', to='library.work')),
|
||||
('projects', models.ManyToManyField(help_text='Current usage', related_name='works', through='library.Item', to='interface.Project')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('ensemble', 'slug')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Part',
|
||||
@@ -75,7 +95,16 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='work',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.work'),
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_items', to='library.work'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EnsembleAccess',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('access_type', models.PositiveSmallIntegerField(choices=[(1, 'Unlimited'), (2, 'Approval required')], default=2)),
|
||||
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.collection')),
|
||||
('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='interface.ensemble')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.1.1 on 2021-05-04 08:30
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('library', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='approved_by',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='auth.user'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='checkin',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='checkout',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.1 on 2021-05-04 08:45
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0002_auto_20210504_1830'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='work',
|
||||
name='max_loans',
|
||||
field=models.IntegerField(default=1, help_text='How many projects can this work be attached to'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='work',
|
||||
name='orchestration',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.orchestration'),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 3.1.1 on 2021-03-11 06:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
ORCHESTRATIONS = {
|
||||
'SATB': ('S', 'A', 'T', 'B'),
|
||||
'String Quartet': ('Vln1', 'Vln2', 'Vla', 'Vc'),
|
||||
'Chamber Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb',
|
||||
'Fl1', 'Fl2', 'Cl1', 'Cl2', 'Ob1', 'Ob2', 'Hn1', 'Hn2',
|
||||
'Tpt1', 'Tpt2', 'Tbn1', 'Tbn2', 'Tuba',
|
||||
'Timp', 'Drum', 'Perc'),
|
||||
}
|
||||
|
||||
def add_orchestrations(apps, schema_editor):
|
||||
q = apps.get_model('library', 'Orchestration').objects
|
||||
for name, instruments in ORCHESTRATIONS.items():
|
||||
q.create(name=name, instruments=", ".join(instruments))
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_orchestrations, lambda apps, schema_editor: None),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 3.1.1 on 2021-05-04 23:27
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0003_auto_20210504_1845'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='item',
|
||||
old_name='checkin',
|
||||
new_name='due',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='returned',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='collection',
|
||||
name='location',
|
||||
field=models.CharField(help_text='Physical location (institution, town...)', max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='collection',
|
||||
name='name',
|
||||
field=models.CharField(help_text='Often just the name of the owning ensemble', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='collection',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, help_text='Publicly visible notes about collection and loans policy'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='work',
|
||||
name='collection',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='library.collection'),
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 3.1.1 on 2021-03-22 23:47
|
||||
# Generated by Django 3.1.1 on 2021-05-06 02:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0003_orchestrations'),
|
||||
('library', '0004_auto_20210505_0927'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='work',
|
||||
name='composer',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
name='parts',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 3.2.7 on 2021-09-02 03:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('interface', '0028_ensemble_details'),
|
||||
('library', '0005_work_parts'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='ensembleaccess',
|
||||
options={'verbose_name_plural': 'Ensemble access'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='collection',
|
||||
name='ensembles',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='version',
|
||||
field=models.CharField(blank=True, max_length=30),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='version',
|
||||
field=models.CharField(blank=True, help_text='Limited to specific version tag', max_length=30),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='collection',
|
||||
name='name',
|
||||
field=models.CharField(help_text='Name of the collection', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ensembleaccess',
|
||||
name='collection',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allowed_ensembles', to='library.collection'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ensembleaccess',
|
||||
name='ensemble',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allowed_collections', to='interface.ensemble'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.2.7 on 2021-09-02 04:07
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0006_auto_20210902_1355'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='work',
|
||||
name='orchestration',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='work',
|
||||
name='version',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_created=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='work',
|
||||
name='edition',
|
||||
field=models.CharField(blank=True, help_text='Edition details', max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.7 on 2021-09-02 04:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0007_auto_20210902_1407'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='work',
|
||||
name='orchestration',
|
||||
field=models.CharField(blank=True, help_text='IMDB format instrumentation', max_length=255),
|
||||
),
|
||||
]
|
||||
+318
-62
@@ -1,7 +1,11 @@
|
||||
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
|
||||
|
||||
import re
|
||||
|
||||
@@ -14,42 +18,181 @@ except (ImportError, AttributeError):
|
||||
library_storage = get_storage_class()()
|
||||
logger.info("Library storage: %s", library_storage.__class__.__name__)
|
||||
|
||||
INSTRUMENTS = [
|
||||
('Score', 'Score'),
|
||||
('S', 'Soprano'),
|
||||
('A', 'Alto'),
|
||||
('T', 'Tenor'),
|
||||
('B', 'Bass'),
|
||||
('V', 'Vocals'),
|
||||
('Vln', 'Violin'),
|
||||
('Vla', 'Viola'),
|
||||
('Vc', 'Violoncello'),
|
||||
('Cb', 'Contrabass'),
|
||||
('Fl', 'Flute'),
|
||||
('Picc', 'Piccolo'),
|
||||
('Cl', 'Clarinet'),
|
||||
('Ob', 'Oboe'),
|
||||
('Hn', 'Horn'),
|
||||
('Tpt', 'Trumpet'),
|
||||
('Tbn', 'Trombone'),
|
||||
('Tuba', 'Tuba'),
|
||||
('Timp', 'Timpani'),
|
||||
('Drum', 'Drumset'),
|
||||
('Perc', 'Percussion'),
|
||||
('Pno', 'Piano'),
|
||||
('Hp', 'Harp'),
|
||||
]
|
||||
# taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_Instruments
|
||||
ABBREVIATIONS = """
|
||||
score Score
|
||||
acc Accordion
|
||||
afl Alto flute
|
||||
alt Alto (voice) (contralto)
|
||||
arp Arpeggione
|
||||
bag Bagpipe
|
||||
bar Baritone (voice)
|
||||
bass Bass (voice)
|
||||
bbar Bass baritone (voice)
|
||||
bc Continuo (Basso continuo)
|
||||
bcl Bass clarinet
|
||||
bell Bell (Chimes)
|
||||
bfl Bass flute
|
||||
bgtr Bass guitar
|
||||
bjo Banjo
|
||||
bn Bassoon
|
||||
bob Bass oboe (Baritone oboe)
|
||||
br Brass instruments
|
||||
bryt Baryton
|
||||
bstcl Basset clarinet
|
||||
bsthn Basset horn
|
||||
bug Bugle
|
||||
cbcl Contrabass clarinet
|
||||
cbn Contrabassoon
|
||||
cch Children's chorus
|
||||
cel Celesta
|
||||
ch Mixed chorus
|
||||
cimb Cimbalom
|
||||
cit Cittern
|
||||
cl Clarinet
|
||||
clvd Clavichord
|
||||
cm Chalumeau
|
||||
conc Concertina
|
||||
crh Crumhorn
|
||||
crt Cornet
|
||||
crtt Cornett (Zink)
|
||||
cv Child's voice
|
||||
db Double Bass
|
||||
dlcn Dulcian
|
||||
dom Domra
|
||||
dulc Dulcimer
|
||||
egtr Electric guitar
|
||||
eh English horn (Cor anglais)
|
||||
elec Electronic Instruments
|
||||
epf Electric piano
|
||||
eq Equal voices
|
||||
erhu Erhu
|
||||
euph Euphonium
|
||||
fch Female chorus
|
||||
fda Flute d'amore (Tenor flute)
|
||||
fgh Flugelhorn
|
||||
fife Fife
|
||||
fl Flute
|
||||
flag Flageolet
|
||||
ghca Glass harmonica (Bowl organ)
|
||||
gl Glockenspiel
|
||||
gtr Guitar
|
||||
harm Harmonium
|
||||
hca Harmonica (Mouth Organ)
|
||||
heck Heckelphone
|
||||
hn Horn
|
||||
hp Harp
|
||||
hpd Harpsichord
|
||||
kbd Keyboard instrument
|
||||
lute Lute
|
||||
lyre Lyre
|
||||
mand Mandolin
|
||||
mar Marimba
|
||||
mch Male chorus
|
||||
mez Mezzo-soprano
|
||||
mus Musette
|
||||
nar Narrator (Reciter)
|
||||
ob Oboe
|
||||
oca Ocarina
|
||||
oda Oboe d'amore
|
||||
om Ondes Martenot
|
||||
oph Ophicleide
|
||||
orch Orchestra
|
||||
org Organ
|
||||
oud Oud
|
||||
pan Pan flute (Pan-pipes)
|
||||
perc Percussion
|
||||
pf Piano
|
||||
pf3h Piano 3 hands
|
||||
pf4h Piano 4 hands
|
||||
pf5h Piano 5 hands
|
||||
pf6h Piano 6 hands
|
||||
pflh Piano left hand
|
||||
pfped Pedal piano
|
||||
pfrh Piano right hand
|
||||
picc Piccolo
|
||||
pipa Pipa
|
||||
pk Timpani
|
||||
ptpt Piccolo trumpet
|
||||
reb Rebec
|
||||
rec Recorder
|
||||
sar Sarrusophone
|
||||
sax Saxophone
|
||||
sheng Sheng
|
||||
shw Shawm
|
||||
sit Sitar
|
||||
skbt Sackbut
|
||||
sop Soprano (voice)
|
||||
srp Serpent
|
||||
stpt Slide trumpet
|
||||
str String instruments
|
||||
sxh Saxhorn
|
||||
syn Synthesizer
|
||||
tba Tuba
|
||||
tbn Trombone
|
||||
ten Tenor
|
||||
thrm Theremin
|
||||
timp Timpani
|
||||
tpt Trumpet
|
||||
uch Unison chorus
|
||||
uke Ukelele (Ukulele)
|
||||
v Voice (solo)
|
||||
va Viola
|
||||
vap Viola pomposa
|
||||
vc Cello
|
||||
vda Viola d'amore
|
||||
vib Vibraphone
|
||||
vie Vielle (Hurdy-Gurdy)
|
||||
viol Viol (Viola da gamba)
|
||||
vlne Violone
|
||||
vn Violin
|
||||
vuv Vuvuzela
|
||||
vv Voices (multiple soloists)
|
||||
wag Wagner tuba
|
||||
ww Woodwind instruments
|
||||
xiao Xiao
|
||||
xyl Xylophone
|
||||
zith Zither
|
||||
"""
|
||||
|
||||
INSTRUMENTS = []
|
||||
for line in ABBREVIATIONS.split('\n'):
|
||||
parts = line.strip().split(maxsplit=1)
|
||||
if len(parts) < 2: continue
|
||||
name, _, _ = parts[1].partition('(')
|
||||
INSTRUMENTS.append((parts[0], 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'),
|
||||
'RWE': ('Fl1', 'Fl2', 'Cl', 'Tbn', 'Vln1', 'Vln2', 'Vla', 'Vc'),
|
||||
'Custom': ()
|
||||
'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'),
|
||||
]
|
||||
|
||||
def tag_to_instrument(tag):
|
||||
m = re.match(r'([A-Za-z]+)(\d*)', tag)
|
||||
@@ -58,13 +201,18 @@ def tag_to_instrument(tag):
|
||||
l = m.groups()
|
||||
return "{0} {1}".format(dict(INSTRUMENTS).get(l[0],l[0]), l[1]).strip()
|
||||
|
||||
'''
|
||||
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(',') ]
|
||||
tags = [ t.strip() for t in self.instruments.split(' ') ]
|
||||
return [ (t, tag_to_instrument(t)) for t in tags if t ]
|
||||
|
||||
def save(self):
|
||||
@@ -73,44 +221,21 @@ class Orchestration(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Work(models.Model):
|
||||
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="works")
|
||||
slug = models.SlugField(max_length=100, editable=False)
|
||||
name = models.CharField(max_length=255)
|
||||
composer = models.CharField(max_length=255, blank=True)
|
||||
orchestration = models.ForeignKey(Orchestration, null=True, on_delete=models.SET_NULL, related_name='works')
|
||||
running_time = models.IntegerField(null=True, blank=True)
|
||||
notes = models.TextField(blank=True)
|
||||
projects = models.ManyToManyField('interface.Project', through='Item', related_name="works")
|
||||
|
||||
class Meta:
|
||||
unique_together = ['ensemble', 'slug']
|
||||
|
||||
@property
|
||||
def parts(self):
|
||||
return Part.objects.filter(doc__work=self.pk)
|
||||
|
||||
@property
|
||||
def instruments(self):
|
||||
return self.orchestration.as_list()
|
||||
|
||||
def save(self):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super(Work, self).save()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
'''
|
||||
|
||||
class Item(models.Model):
|
||||
"""
|
||||
Item represents a Work in a Project e.g. item in set list or programme
|
||||
Item represents a specic version of a Work in 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)
|
||||
work = models.ForeignKey('Work', on_delete=models.CASCADE)
|
||||
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)
|
||||
version = models.CharField(max_length=30, blank=True, help_text="Limited to specific version tag")
|
||||
|
||||
class Meta:
|
||||
ordering = ['order', 'work']
|
||||
@@ -118,20 +243,151 @@ class Item(models.Model):
|
||||
def __str__(self):
|
||||
return f"<{self.project.slug}:{self.work.slug}>"
|
||||
|
||||
class Collection(models.Model):
|
||||
"""
|
||||
Storage location for works (physical or virtual)
|
||||
"""
|
||||
name = models.CharField(max_length=255, help_text="Name of the collection")
|
||||
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...)")
|
||||
storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True, help_text="Storage for documents")
|
||||
notes = models.TextField(blank=True, help_text="Publicly visible notes about collection and loans policy")
|
||||
#ensembles = models.ManyToManyField('interface.Ensemble', related_name="collections", through='EnsembleAccess')
|
||||
|
||||
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"
|
||||
|
||||
class Work(models.Model):
|
||||
"""
|
||||
A musical work 'owned' by a collection from a licencing perspective.
|
||||
"""
|
||||
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works", help_text="Owner")
|
||||
slug = models.SlugField(max_length=100, editable=False)
|
||||
name = models.CharField(max_length=255)
|
||||
edition = models.CharField(max_length=100, blank=True, help_text="Edition details")
|
||||
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="Use Composer / Arranger format")
|
||||
#orchestration = models.ForeignKey(Orchestration, null=True, on_delete=models.SET_NULL, related_name='works', blank=True)
|
||||
orchestration = models.CharField(max_length=255, blank=True, help_text="IMDB format instrumentation")
|
||||
parts = models.JSONField(null=True, blank=True)
|
||||
|
||||
# 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_loans = models.IntegerField(default=1, help_text="How many projects can this work be attached to")
|
||||
|
||||
# Extra info
|
||||
running_time = models.IntegerField(null=True, blank=True, help_text="Running time in seconds")
|
||||
notes = models.TextField(blank=True)
|
||||
tag_list = models.CharField(max_length=255, blank=True, help_text="Multiple tags for the work")
|
||||
|
||||
projects = models.ManyToManyField('interface.Project', through='Item', related_name="works", help_text="Current usage")
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
if self.running_time is None:
|
||||
return "-:--"
|
||||
return "{0:d}:{1:02d}".format(int(self.running_time / 60), self.running_time % 60)
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return self.tag_list.split(';') if self.tag_list else []
|
||||
|
||||
@tags.setter
|
||||
def set_tags(self, tags):
|
||||
self.tag_list = ";".join(tags)
|
||||
|
||||
@property
|
||||
def digital_parts(self):
|
||||
return Part.objects.filter(doc__work=self.pk)
|
||||
|
||||
@property
|
||||
def physical_parts(self):
|
||||
if not self.parts:
|
||||
return []
|
||||
return [ (tag_to_instrument(k), v) for (k, v) in self.parts.items() ]
|
||||
|
||||
#@property
|
||||
#def instruments(self):
|
||||
# return self.orchestration.as_list()
|
||||
|
||||
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_loans < 0:
|
||||
return True
|
||||
return self.max_loans > self.loans
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
if self.max_loans < 0:
|
||||
return 'Unlimited'
|
||||
a = self.max_loans - self.loans
|
||||
return '{0} of {1}'.format(max(a, 0), self.max_loans)
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return f"{self.collection.pk:03d}-{self.pk:03d}"
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.composer})"
|
||||
|
||||
def doc_upload_filename(doc, filename):
|
||||
storage = doc.work.ensemble.storage
|
||||
storage = doc.work.collection.storage
|
||||
if not storage:
|
||||
raise RuntimeError("Storage not set")
|
||||
return f'{storage}:{doc.work.ensemble.slug}/works/{doc.work.slug}/{filename}'
|
||||
raise RuntimeError("Collection has no storage attached")
|
||||
return f'{storage}:works/{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=library_storage)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
version = models.CharField(max_length=30, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.upload.name
|
||||
|
||||
class Part(models.Model):
|
||||
"""
|
||||
Part is a tagged portion of a Document
|
||||
"""
|
||||
doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="parts")
|
||||
tag = models.SlugField(max_length=20)
|
||||
start = models.SmallIntegerField(null=True, blank=True)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
|
||||
{% block admin %}
|
||||
<a href="{% url 'item_list_manage' project.pk %}"><i class="fas fa-list"></i><span class="smhide admin">Change items</span></a>
|
||||
<a href="{% url 'item_list_manage' project.pk %}"><i class="fas fa-list"></i><span class="smhide admin"> Change items</span></a>
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
@@ -23,15 +23,22 @@
|
||||
<input id="part-preference" name="part" type="number" value="0" min="0" max="4" onchange="updateParts()" size="1"/><br/><br/>
|
||||
<button type="submit" style="height: 32px;"><i class="fas fa-copy"></i> Get My Parts!</button>
|
||||
</div>
|
||||
<table style="max-width: 600px; margin: 10pt auto;" class="">
|
||||
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th/>
|
||||
<th>Piece</th>
|
||||
<th>Part</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }})</td>
|
||||
<td>{{ forloop.counter }}.</td>
|
||||
<td>
|
||||
<a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a>
|
||||
</td>
|
||||
<td class="">
|
||||
<td class="select-cell">
|
||||
<input type="hidden" name="works" value="{{ item.work.pk }}"/>
|
||||
<select name="instruments">
|
||||
<option value='-'>None</option>
|
||||
|
||||
@@ -6,18 +6,19 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<table style="max-width: 600px; margin: 10pt auto;" class="item-table">
|
||||
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Time</th>
|
||||
<th/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="work-list">
|
||||
{% for item in object_list %}
|
||||
<tr data-pk="{{ item.pk }}" data-order="{{ forloop.counter }}">
|
||||
<td>{{ item.work.name }}</td>
|
||||
<td>{% firstof item.work.running_time '?' %}</td>
|
||||
<td>{{ item.work.duration }}</td>
|
||||
<td style="text-align: center;">
|
||||
<i class="fas fa-arrow-up clickable" title="Move up" onclick="moveItem({{ item.pk }}, -1)"></i>
|
||||
<i class="fas fa-arrow-down clickable" title="Move down" onclick="moveItem({{ item.pk }}, 1)"></i>
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
<a href="{% url 'item_list' project=project.pk %}">My Music</a>
|
||||
{% endif %}
|
||||
{% if request.is_admin %}
|
||||
<a href="{% url 'work_list' %}">Works</a>
|
||||
<a href="{% url 'work_list' %}">Library</a>
|
||||
{% endif %}
|
||||
@@ -6,23 +6,84 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<h2><a href="{% url 'work_list' %}">Works</a> / {{ work.name }}</h2>
|
||||
<h2>{{ work.name }} {% if work.running_time %}({{ work.duration }}){% endif %} <small>[{{ work.identifier }}]</small></h2>
|
||||
<h4>{{ work.composer }}{% if work.version %} - {{ work.version }}{% endif %}</h4>
|
||||
<p>{{ work.notes }}</p>
|
||||
<h3>Parts</h3>
|
||||
<ul>
|
||||
{% for part in work.parts %}
|
||||
<li><a href="{% url 'part_download' pk=part.pk filename=part.filename %}" target="part_{{ part.pk }}" rel="">{{ part.instrument }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url 'work_partset' pk=work.pk %}"><i class="fas fa-print"></i> Print part set</a>
|
||||
{% if work.collection %}
|
||||
<p>Location: {{ work.collection }} [{{ work.collection_index }}]</p>
|
||||
{% endif %}
|
||||
|
||||
<h3>Original Documents</h3>
|
||||
{% if work.parent %}
|
||||
<p>From <a href="{% url 'work_detail' work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if work.related_works.count %}
|
||||
<h3>Related</h3>
|
||||
<ul>
|
||||
{% for related in work.related_works.all %}
|
||||
<li><a href="{% url 'work_detail' related.pk %}">{{ related.name }} - {{ related.composer }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h4>Loans
|
||||
{% if request.is_admin %}
|
||||
<a href="{% url 'work_add_to_project' work.pk %}"><i class="fas fa-plus-circle"></i></a>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<table style="margin: 10px auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ensemble</th>
|
||||
<th>Project</th>
|
||||
<th>Checked Out</th>
|
||||
<th>Due Back</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in work.current_loans %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ensemble_detail' item.project.ensemble_id %}">{{ item.project.ensemble.name }}</a></td>
|
||||
<td><a href="{% url 'project_detail' item.project.pk %}">{{ item.project.name }}</a></td>
|
||||
<td>{{ item.checkout.date|date:"d/m/Y" }}</td>
|
||||
<td>{{ item.due.date|date:"d/m/Y" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td>No current loans</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Printed Parts</h4>
|
||||
<p>
|
||||
{% for inst, c in work.physical_parts %}
|
||||
<span class="badge">{{ inst }} ({{ c }})</span>
|
||||
{% empty %}
|
||||
No physical parts available
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
||||
<h4>Digital Parts
|
||||
<a href="{% url 'work_partset' pk=work.pk %}" title="Print part set"><i class="fas fa-print"></i></a>
|
||||
</h4>
|
||||
<p>
|
||||
{% for part in work.digital_parts %}
|
||||
<a class="badge" href="{% url 'part_download' pk=part.pk filename=part.filename %}"
|
||||
target="part_{{ part.pk }}" rel="">{{ part.instrument }}</a>
|
||||
{% empty %}
|
||||
No digital parts available
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
||||
<h4>Documents</h4>
|
||||
<ul>
|
||||
{% for doc in work.docs.all %}
|
||||
<li>
|
||||
<a href="{% url 'document_download' pk=doc.pk %}" target="_blank">{{ doc.upload.name|basename }}</a>
|
||||
|
||||
[{{ doc.parts.count }} parts]
|
||||
{% with parts=doc.parts.count %}
|
||||
{% if parts %}[{{ parts }} parts]{% endif %}
|
||||
{% endwith %}
|
||||
{% if request.is_admin %}
|
||||
<a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-tags"></i></a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,25 +1,61 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
{% load url_tools %}
|
||||
|
||||
{% block admin %}
|
||||
<a href="{% url 'work_add' %}"><i class="fas fa-plus-circle"></i> Add new</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<h2>Works</h2>
|
||||
<table style="max-width: 800px; margin: 10pt auto;">
|
||||
<h2>Library for {{ view.ensemble }}</h2>
|
||||
<div style="margin-bottom: 1em;">
|
||||
<form method="GET">
|
||||
<input name="filter" type="text" placeholder="Filter" value="{{ request.GET.filter }}"/>
|
||||
<a href="?">Clear</a>
|
||||
</form>
|
||||
</div>
|
||||
<table class="zebra wide">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Work</th>
|
||||
<th>Composer</th>
|
||||
<th class="smhide">Edition</th>
|
||||
<th class="smhide">Orchestration</th>
|
||||
<th class="smhide">Collection</th>
|
||||
<th>Copies</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for work in object_list %}
|
||||
{% with work.docs.count as doc_count %}
|
||||
{% with work.parts.count as part_count %}
|
||||
<tr>
|
||||
<td><a href="{% url 'work_detail' pk=work.pk %}">{{ work.name }}</a></td>
|
||||
<td>{{ doc_count }} file{{ doc_count|pluralize }} with {{ part_count }} part{{ part_count|pluralize }}</td>
|
||||
<td title="{{ work.composer }}">{{ work.composer|truncatewords:3 }}</td>
|
||||
<td class="smhide" title="{{ work.edition }}">{{ work.edition|truncatewords:2 }}</td>
|
||||
<td class="smhid" title="{{ work.orchestration }}">{{ work.orchestration|truncatewords:2}}</td>
|
||||
<td class="smhide">{{ work.collection.name }}</td>
|
||||
<td style="color: {{ work.is_available|yesno:'green,red' }};">{{ work.available }}</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<tr><td colspan="4">No works found</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination" style="text-align: right;">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="{% url_update page=1 %}" title="First">«</a>
|
||||
<a href="{% url_update page=page_obj.previous_page_number %}" title="Previous">‹</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="{% url_update page=page_obj.next_page_number %}" title="Next">›</a>
|
||||
<a href="{% url_update page=page_obj.paginator.num_pages %}" title="Last">»</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
+20
-1
@@ -1,3 +1,22 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from django.contrib.auth.models import User
|
||||
from interface.models import Ensemble, Project
|
||||
from . import models
|
||||
|
||||
class IntegrationTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.homer = User.objects.create(username='homer')
|
||||
self.ned = User.objects.create(username="ned")
|
||||
self.lisa = User.objects.create(username="lisa")
|
||||
self.dewey = User.objects.create(username="dewey")
|
||||
|
||||
self.be_sharps = self.homer.ensembles.create(name='Be Sharps', code="barbershop")
|
||||
self.sesd = self.dewey.ensembles.create(name="Springfield Elementary School Band", code="sax")
|
||||
|
||||
self.sel = self.lisa.collections.create(name="Springfield Elementary Library")
|
||||
self.flanders = self.ned.collections.create(name="Neds Shed")
|
||||
|
||||
def test_integration(self):
|
||||
pass
|
||||
|
||||
+6
-2
@@ -13,8 +13,12 @@ urlpatterns = [
|
||||
path('library/works/create', views.WorkAddView.as_view(), name="work_add"),
|
||||
path('library/works/<int:pk>', views.WorkDetailView.as_view(), name="work_detail"),
|
||||
path('library/works/<int:pk>/partset', views.WorkPartSetView.as_view(), name="work_partset"),
|
||||
path('library/works/<int:pk>/upload', views.DocumentAddView.as_view(), name="document_add"),
|
||||
path('library/works/<int:pk>/add_to_project', views.WorkAddToProject.as_view(), name="work_add_to_project"),
|
||||
path('library/documents/<int:pk>/upload', views.DocumentAddView.as_view(), name="document_add"),
|
||||
path('library/documents/<int:pk>/download', views.DocumentDownloadView.as_view(), name="document_download"),
|
||||
path('library/documents/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
|
||||
path('library/parts/<int:pk>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
|
||||
]
|
||||
]
|
||||
|
||||
from django.views.static import serve
|
||||
urlpatterns.append(path('localstorage/<path:path>', serve, {'document_root': 'local_storage'}))
|
||||
+57
-19
@@ -4,6 +4,8 @@ from django.views.generic.list import ListView, MultipleObjectMixin
|
||||
from django.views.generic.edit import CreateView, FormView, UpdateView
|
||||
from django.http import FileResponse, HttpResponse
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q, Count
|
||||
from django.utils.timezone import now
|
||||
|
||||
import json
|
||||
|
||||
@@ -98,9 +100,20 @@ class ProjectItemAddView(ProjectMixin, UpdateView):
|
||||
return self.get_project()
|
||||
|
||||
class WorkListView(EnsembleMixin, ListView):
|
||||
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
return Work.objects.filter(ensemble=self.request.ensemble_id).order_by('name')
|
||||
#works = Work.objects.filter(collection__ensembles=self.request.ensemble_id).order_by('name').select_related('collection')
|
||||
works = Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id).order_by('name').select_related('collection')
|
||||
|
||||
loan_count = Count('project_items', Q(project_items__checkout__lte=now(), project_items__returned=None))
|
||||
works = works.annotate(loan_count=loan_count)
|
||||
|
||||
q = self.request.GET.get('filter')
|
||||
if q:
|
||||
works = works.filter(Q(name__contains=q) | Q(composer__contains=q) | Q(tag_list__contains=q))
|
||||
|
||||
return works
|
||||
|
||||
class WorkAddView(EnsembleMixin, FormView):
|
||||
template_name = "interface/default_form.html"
|
||||
@@ -108,34 +121,59 @@ class WorkAddView(EnsembleMixin, FormView):
|
||||
|
||||
def get_form(self):
|
||||
f = super(WorkAddView, self).get_form()
|
||||
qs = f.fields['orchestration'].queryset
|
||||
f.fields['orchestration'].queryset = qs.filter(ensemble_id__isnull=True) | qs.filter(ensemble_id=self.request.ensemble_id)
|
||||
#qs = f.fields['orchestration'].queryset
|
||||
#f.fields['orchestration'].queryset = qs.filter(ensemble_id__isnull=True) | qs.filter(ensemble_id=self.request.ensemble_id)
|
||||
qs = f.fields['collection'].queryset
|
||||
qs = qs.filter(administrators=self.request.user)
|
||||
f.fields['collection'].queryset = qs
|
||||
return f
|
||||
|
||||
def form_valid(self, form):
|
||||
obj = form.save(commit=False)
|
||||
obj.ensemble_id = self.request.ensemble_id
|
||||
try:
|
||||
obj.save()
|
||||
except IntegrityError:
|
||||
form.add_error('name', 'Name must be unique')
|
||||
return self.form_invalid(form)
|
||||
work = form.save(commit=False)
|
||||
work.ensemble_id = self.request.ensemble_id
|
||||
work.save()
|
||||
|
||||
# handle the files
|
||||
uploads = self.request.FILES.getlist('uploads')
|
||||
docs = []
|
||||
for f in uploads:
|
||||
docs.append(obj.docs.create(upload=f).pk)
|
||||
docs.append(work.docs.create(upload=f).pk)
|
||||
|
||||
if len(docs) == 1:
|
||||
return redirect('document_annotate', docs[0])
|
||||
else:
|
||||
return redirect('work_detail', pk=obj.pk)
|
||||
return redirect('work_detail', pk=work.pk)
|
||||
|
||||
class WorkDetailView(EnsembleMixin, DetailView):
|
||||
|
||||
def get_queryset(self):
|
||||
return Work.objects.filter(ensemble=self.request.ensemble_id)
|
||||
return Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id)
|
||||
|
||||
class WorkAddToProject(EnsembleMixin, FormView):
|
||||
form_class = forms.ProjectSelectForm
|
||||
template_name = "interface/default_form.html"
|
||||
title = "Select project to add work to"
|
||||
|
||||
def get_object(self):
|
||||
return Work.objects.get(pk=self.kwargs['pk'])
|
||||
|
||||
def get_form(self):
|
||||
f = super(WorkAddToProject, self).get_form()
|
||||
qs = f.fields['project'].queryset
|
||||
|
||||
work = self.get_object()
|
||||
qs = qs.filter(ensemble_id=self.request.ensemble_id).exclude(pk__in=work.projects.all())
|
||||
|
||||
f.fields['project'].queryset = qs
|
||||
return f
|
||||
|
||||
def form_valid(self, form):
|
||||
work = self.get_object()
|
||||
project = form.cleaned_data['project']
|
||||
work.project_items.create(project=project, approved_by=self.request.user, checkout=now())
|
||||
return redirect('item_list', project=project.pk)
|
||||
|
||||
|
||||
|
||||
class WorkPartSetView(EnsembleMixin, DetailView):
|
||||
template_name = "library/work_partset.html"
|
||||
@@ -162,7 +200,7 @@ class WorkPartSetView(EnsembleMixin, DetailView):
|
||||
return response
|
||||
|
||||
def get_queryset(self):
|
||||
return Work.objects.filter(ensemble_id=self.request.ensemble_id)
|
||||
return Version.objects.filter(work__ensemble_id=self.request.ensemble_id)
|
||||
|
||||
class DocumentDetailView(EnsembleMixin, DetailView):
|
||||
|
||||
@@ -192,7 +230,7 @@ class DocumentDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
||||
return redirect(self.object.upload.url)
|
||||
|
||||
def get_queryset(self):
|
||||
return Document.objects.filter(work__ensemble=self.request.ensemble_id)
|
||||
return Document.objects.filter(work__collection__allowed_ensembles__ensemble=self.request.ensemble_id)
|
||||
|
||||
class DocumentAnnotateView(EnsembleMixin, DetailView):
|
||||
template_name = 'library/document_annotate.html'
|
||||
@@ -228,11 +266,11 @@ class DocumentAnnotateView(EnsembleMixin, DetailView):
|
||||
for i in range(part.start, (part.end or part.start)+1):
|
||||
pages[i] = part.tag
|
||||
|
||||
data['json_data'] = {'pageTags': pages, 'instruments': data['document'].work.instruments}
|
||||
data['json_data'] = {'pageTags': pages, 'instruments': data['document'].work.orchestration}
|
||||
return data
|
||||
|
||||
def get_queryset(self):
|
||||
return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work')
|
||||
return Document.objects.filter(work__collection__allowed_ensembles__ensemble=self.request.ensemble_id).select_related('work')
|
||||
|
||||
|
||||
class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
||||
@@ -251,4 +289,4 @@ class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
||||
return response
|
||||
|
||||
def get_queryset(self):
|
||||
return Part.objects.filter(doc__work__ensemble=self.request.ensemble_id).select_related('doc', 'doc__work')
|
||||
return Part.objects.filter(doc__work__collection__allowed_ensembles__ensemble=self.request.ensemble_id).select_related('doc', 'doc__work')
|
||||
Reference in New Issue
Block a user