diff --git a/interface/migrations/0028_ensemble_details.py b/interface/migrations/0028_ensemble_details.py new file mode 100644 index 0000000..0aa6ec2 --- /dev/null +++ b/interface/migrations/0028_ensemble_details.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.1 on 2021-05-05 05:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('interface', '0027_auto_20210322_1154'), + ] + + operations = [ + migrations.AddField( + model_name='ensemble', + name='details', + field=models.TextField(blank=True), + ), + ] diff --git a/interface/models.py b/interface/models.py index aded9d7..1804c87 100644 --- a/interface/models.py +++ b/interface/models.py @@ -44,6 +44,7 @@ class Ensemble(models.Model): passphrase = models.CharField(max_length=100) admins = models.ManyToManyField('auth.User', related_name='ensembles') slug = models.SlugField(max_length=100, editable=False) + details = models.TextField(blank=True) storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL) def active_projects(self): @@ -53,10 +54,10 @@ class Ensemble(models.Model): code = str(self.code) return "{}-{}-{}".format(code[:3], code[3:6], code[6:]) - def save(self): + def save(self, **kwargs): if not self.slug: self.slug = slugify(self.name) - super(Ensemble, self).save() + super(Ensemble, self).save(**kwargs) def __str__(self): return self.name @@ -86,7 +87,7 @@ class Project(models.Model): @property def has_happened(self): - return self.event_date < timezone.now() + return self.event_date < timezone.now().date() def save(self): if not self.slug: diff --git a/interface/static/interface/css/polyphonic.css b/interface/static/interface/css/polyphonic.css index 4dadfdc..ea8351e 100644 --- a/interface/static/interface/css/polyphonic.css +++ b/interface/static/interface/css/polyphonic.css @@ -3,6 +3,7 @@ --border-color: #292929; --gray-blue: #667788; --light-blue: #c5eff7; + --light-grey: #EEEEEE; } @font-face { @@ -36,7 +37,7 @@ BODY { } .main { - max-width: 1000px; + max-width: 1280px; margin: 10px auto; border: 1px solid var(--border-color); border-radius: 5px; @@ -55,16 +56,30 @@ BODY { margin: 0px auto; } +.wide { + width: 1200px; +} + .collapse { display: flex; flex-direction: row; justify-content: space-around; } +@media all and (max-width: 1200px) { + .wide { + width: 900px; + } +} + @media all and (max-width: 900px) { .mdhide { display: none; } + + .wide { + width: auto; + } } @media all and (max-width: 700px) { @@ -74,6 +89,9 @@ BODY { .collapse { flex-direction: column; } + .wide { + width: auto; + } } @@ -138,6 +156,16 @@ INPUT[type=checkbox] { margin-top: 20px; } +.badge { + display: inline-block; + border: 1px solid var(--gray-blue); + font-weight: bold; + font-size: 0.9em; + border-radius: 10px; + padding: 4px 10px 2px; + margin: 2px; +} + .btn { background-color: var(--gray-blue); display: inline-block; @@ -232,10 +260,28 @@ H1 { text-align: center; } -TD { - padding: 5px; +TABLE { + border-spacing: 0px; } +TD, TH { + padding: 3px 6px; + text-align: left; +} + +TABLE THEAD TR { + background-color: var(--gray-blue); + color: var(--light-blue); + font-weight: bolder; +} + +TABLE THEAD TH { + padding: 5px 6px; +} + +TABLE.zebra TR:nth-child(even) { + background-color: var(--light-grey); +} TABLE.horizontal TH { text-align: right; @@ -298,12 +344,12 @@ TABLE SELECT { border: 1px solid #999; } TD.select-cell { - padding: 0; + padding: 0px !important; } .select-cell SELECT { border: none; - background-color: white; + background-color: transparent; height: 30px; font-family: inherit; font-size: inherit; diff --git a/interface/templates/interface/default_form.html b/interface/templates/interface/default_form.html index 7278952..837c798 100644 --- a/interface/templates/interface/default_form.html +++ b/interface/templates/interface/default_form.html @@ -2,7 +2,7 @@ {% block page %}
-

{{ title }}

+

{% firstof title view.title %}

{% csrf_token %} {{ form }} diff --git a/interface/templates/interface/ensemble_detail.html b/interface/templates/interface/ensemble_detail.html index 326a6d7..9c14232 100644 --- a/interface/templates/interface/ensemble_detail.html +++ b/interface/templates/interface/ensemble_detail.html @@ -1,22 +1,12 @@ -{% extends "interface/project_base.html" %} +{% extends "base.html" %} -{% block page %} -
-

Projects for {{ ensemble.name }}

- -
- {{ ensemble.ensemble_code }} -
-
-{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/interface/templates/interface/ensemble_project_list.html b/interface/templates/interface/ensemble_project_list.html new file mode 100644 index 0000000..326a6d7 --- /dev/null +++ b/interface/templates/interface/ensemble_project_list.html @@ -0,0 +1,22 @@ +{% extends "interface/project_base.html" %} + +{% block page %} +
+

Projects for {{ ensemble.name }}

+ +
+ {{ ensemble.ensemble_code }} +
+
+{% endblock %} \ No newline at end of file diff --git a/interface/templates/interface/project_detail.html b/interface/templates/interface/project_detail.html index 60e1c84..a75b1e3 100644 --- a/interface/templates/interface/project_detail.html +++ b/interface/templates/interface/project_detail.html @@ -7,9 +7,9 @@ {% if project.event_date %}

{% if project.has_happened %} - {{ project.event_date|timesince }} + {{ project.event_date|timesince }} ago. {% else %} - {{ project.event_date|}} + In {{ project.event_date|timeuntil }}. {% endif %}

{% endif %} diff --git a/interface/templatetags/url_tools.py b/interface/templatetags/url_tools.py new file mode 100644 index 0000000..171e80a --- /dev/null +++ b/interface/templatetags/url_tools.py @@ -0,0 +1,10 @@ +from django import template + +register = template.Library() + +@register.simple_tag(takes_context=True) +def url_update(context, **kwargs): + params = context.request.GET.copy() + for k in kwargs: + params[k] = kwargs[k] + return "?" + params.urlencode() \ No newline at end of file diff --git a/interface/tests/test_integration.py b/interface/tests/test_integration.py new file mode 100644 index 0000000..fb41d81 --- /dev/null +++ b/interface/tests/test_integration.py @@ -0,0 +1,6 @@ +from django.test import TestCase + +class IntegrationTestCase(TestCase): + + def test_runs(self): + self.assertTrue(True) \ No newline at end of file diff --git a/interface/urls.py b/interface/urls.py index 9254873..c51916f 100644 --- a/interface/urls.py +++ b/interface/urls.py @@ -10,7 +10,9 @@ urlpatterns = [ path('register', views.register, name="register"), path('manage', views.ManageView.as_view(), name="manage"), - path('', views.EnsembleDetailView.as_view(), name='ensemble_detail'), + path('', views.EnsembleProjectListView.as_view(), name='ensemble_detail'), + path('ensembles/', views.EnsembleDetailView.as_view(), name='ensemble_detail'), + path('projects/', views.ProjectDetailView.as_view(), name="project_detail"), path('projects//submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"), diff --git a/interface/views.py b/interface/views.py index af149d8..c2f6f89 100644 --- a/interface/views.py +++ b/interface/views.py @@ -60,6 +60,10 @@ class EnsembleMixin(object): return super().dispatch(request, *args, **kwargs) + @property + def ensemble(self): + return models.Ensemble.objects.get(pk=self.request.ensemble_id) + class ProjectMixin(EnsembleMixin): def get_project(self): @@ -195,7 +199,8 @@ def logout(request): return redirect('/') -class EnsembleDetailView(EnsembleMixin, DetailView): +class EnsembleProjectListView(EnsembleMixin, DetailView): + template_name = 'interface/ensemble_project_list.html' def dispatch(self, request, *args, **kwargs): # capture provided urls @@ -207,6 +212,9 @@ class EnsembleDetailView(EnsembleMixin, DetailView): def get_object(self): return models.Ensemble.objects.get(pk=self.request.ensemble_id) +class EnsembleDetailView(DetailView): + model = models.Ensemble + class ProjectDetailView(ProjectMixin, DetailView): def get_object(self): diff --git a/library/README.md b/library/README.md new file mode 100644 index 0000000..07edda5 --- /dev/null +++ b/library/README.md @@ -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 + diff --git a/library/admin.py b/library/admin.py index cdc4494..4b6630b 100644 --- a/library/admin.py +++ b/library/admin.py @@ -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) \ No newline at end of file +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) \ No newline at end of file diff --git a/library/forms.py b/library/forms.py index 6c1c226..40d45e6 100644 --- a/library/forms.py +++ b/library/forms.py @@ -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']) \ No newline at end of file + self.instance.works.add(self.cleaned_data['work']) + +class ProjectSelectForm(forms.Form): + project = forms.ModelChoiceField(queryset=Project.objects.all()) \ No newline at end of file diff --git a/library/management/__init__.py b/library/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library/management/commands/__init__.py b/library/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library/management/commands/import_works.py b/library/management/commands/import_works.py new file mode 100644 index 0000000..2ac748c --- /dev/null +++ b/library/management/commands/import_works.py @@ -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']) \ No newline at end of file diff --git a/library/migrations/0001_initial.py b/library/migrations/0001_initial.py index 5747cd8..51340b8 100644 --- a/library/migrations/0001_initial.py +++ b/library/migrations/0001_initial.py @@ -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', diff --git a/library/migrations/0002_auto_20210504_1830.py b/library/migrations/0002_auto_20210504_1830.py new file mode 100644 index 0000000..a74b9d9 --- /dev/null +++ b/library/migrations/0002_auto_20210504_1830.py @@ -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, + ), + ] diff --git a/library/migrations/0003_auto_20210504_1845.py b/library/migrations/0003_auto_20210504_1845.py new file mode 100644 index 0000000..1e54913 --- /dev/null +++ b/library/migrations/0003_auto_20210504_1845.py @@ -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'), + ), + ] diff --git a/library/migrations/0003_orchestrations.py b/library/migrations/0003_orchestrations.py deleted file mode 100644 index 4f40154..0000000 --- a/library/migrations/0003_orchestrations.py +++ /dev/null @@ -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), - ] diff --git a/library/migrations/0004_auto_20210505_0927.py b/library/migrations/0004_auto_20210505_0927.py new file mode 100644 index 0000000..68193d5 --- /dev/null +++ b/library/migrations/0004_auto_20210505_0927.py @@ -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'), + ), + ] diff --git a/library/migrations/0004_work_composer.py b/library/migrations/0005_work_parts.py similarity index 52% rename from library/migrations/0004_work_composer.py rename to library/migrations/0005_work_parts.py index eb017b9..2a45e29 100644 --- a/library/migrations/0004_work_composer.py +++ b/library/migrations/0005_work_parts.py @@ -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), ), ] diff --git a/library/migrations/0006_auto_20210902_1355.py b/library/migrations/0006_auto_20210902_1355.py new file mode 100644 index 0000000..abf5acc --- /dev/null +++ b/library/migrations/0006_auto_20210902_1355.py @@ -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'), + ), + ] diff --git a/library/migrations/0007_auto_20210902_1407.py b/library/migrations/0007_auto_20210902_1407.py new file mode 100644 index 0000000..16dcc79 --- /dev/null +++ b/library/migrations/0007_auto_20210902_1407.py @@ -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), + ), + ] diff --git a/library/migrations/0008_work_orchestration.py b/library/migrations/0008_work_orchestration.py new file mode 100644 index 0000000..e58ed81 --- /dev/null +++ b/library/migrations/0008_work_orchestration.py @@ -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), + ), + ] diff --git a/library/models.py b/library/models.py index 029b1a3..10baf95 100644 --- a/library/models.py +++ b/library/models.py @@ -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) diff --git a/library/templates/library/item_list.html b/library/templates/library/item_list.html index da6ea16..f4e4421 100644 --- a/library/templates/library/item_list.html +++ b/library/templates/library/item_list.html @@ -1,7 +1,7 @@ {% extends "interface/project_base.html" %} {% block admin %} -Change items + Change items {% endblock %} {% block page %} @@ -23,15 +23,22 @@

- +
+ + + + + + {% for item in object_list %} - + -
+ PiecePart
{{ forloop.counter }}){{ forloop.counter }}. {{ item.work.name }} + +
+ {% for item in object_list %} - +
Item Time
{{ item.work.name }}{% firstof item.work.running_time '?' %}{{ item.work.duration }} diff --git a/library/templates/library/project_menu.html b/library/templates/library/project_menu.html index 0f3a2d4..0d2a90f 100644 --- a/library/templates/library/project_menu.html +++ b/library/templates/library/project_menu.html @@ -2,5 +2,5 @@ My Music {% endif %} {% if request.is_admin %} -Works +Library {% endif %} \ No newline at end of file diff --git a/library/templates/library/work_detail.html b/library/templates/library/work_detail.html index 01410f1..6652f84 100644 --- a/library/templates/library/work_detail.html +++ b/library/templates/library/work_detail.html @@ -6,23 +6,84 @@ {% endblock %} {% block page %} -

Works / {{ work.name }}

+

{{ work.name }} {% if work.running_time %}({{ work.duration }}){% endif %} [{{ work.identifier }}]

+

{{ work.composer }}{% if work.version %} - {{ work.version }}{% endif %}

{{ work.notes }}

-

Parts

- - Print part set +{% if work.collection %} +

Location: {{ work.collection }} [{{ work.collection_index }}]

+{% endif %} -

Original Documents

+{% if work.parent %} +

From {{ work.parent.name }} - {{ work.parent.composer }}

+{% endif %} + +{% if work.related_works.count %} +

Related

+ +{% endif %} + +

Loans +{% if request.is_admin %} + +{% endif %} +

+ + + + + + + + + + + {% for item in work.current_loans %} + + + + + + + {% empty %} + + {% endfor %} + +
EnsembleProjectChecked OutDue Back
{{ item.project.ensemble.name }}{{ item.project.name }}{{ item.checkout.date|date:"d/m/Y" }}{{ item.due.date|date:"d/m/Y" }}
No current loans
+ +

Printed Parts

+

+{% for inst, c in work.physical_parts %} + {{ inst }} ({{ c }}) +{% empty %} + No physical parts available +{% endfor %} +

+ +

Digital Parts + +

+

+{% for part in work.digital_parts %} + {{ part.instrument }} +{% empty %} + No digital parts available +{% endfor %} +

+ +

Documents

    {% for doc in work.docs.all %}
  • {{ doc.upload.name|basename }}   - [{{ doc.parts.count }} parts] + {% with parts=doc.parts.count %} + {% if parts %}[{{ parts }} parts]{% endif %} + {% endwith %} {% if request.is_admin %} {% endif %} diff --git a/library/templates/library/work_list.html b/library/templates/library/work_list.html index f7b379d..b735158 100644 --- a/library/templates/library/work_list.html +++ b/library/templates/library/work_list.html @@ -1,25 +1,61 @@ {% extends "interface/project_base.html" %} +{% load url_tools %} {% block admin %} Add new {% endblock %} {% block page %} -

    Works

    - +

    Library for {{ view.ensemble }}

    +
    + + + Clear + +
    +
    + + + + + + + + {% for work in object_list %} - {% with work.docs.count as doc_count %} - {% with work.parts.count as part_count %} - + + + + + - {% endwith %} - {% endwith %} + {% empty %} + {% endfor %}
    WorkComposerEditionOrchestrationCollectionCopies
    {{ work.name }}{{ doc_count }} file{{ doc_count|pluralize }} with {{ part_count }} part{{ part_count|pluralize }}{{ work.composer|truncatewords:3 }}{{ work.edition|truncatewords:2 }}{{ work.orchestration|truncatewords:2}}{{ work.collection.name }}{{ work.available }}
    No works found
    + + + {% endblock %} \ No newline at end of file diff --git a/library/tests.py b/library/tests.py index 7ce503c..57c2498 100644 --- a/library/tests.py +++ b/library/tests.py @@ -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 diff --git a/library/urls.py b/library/urls.py index b9789fd..625ec42 100644 --- a/library/urls.py +++ b/library/urls.py @@ -13,8 +13,12 @@ urlpatterns = [ path('library/works/create', views.WorkAddView.as_view(), name="work_add"), path('library/works/', views.WorkDetailView.as_view(), name="work_detail"), path('library/works//partset', views.WorkPartSetView.as_view(), name="work_partset"), - path('library/works//upload', views.DocumentAddView.as_view(), name="document_add"), + path('library/works//add_to_project', views.WorkAddToProject.as_view(), name="work_add_to_project"), + path('library/documents//upload', views.DocumentAddView.as_view(), name="document_add"), path('library/documents//download', views.DocumentDownloadView.as_view(), name="document_download"), path('library/documents//annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"), path('library/parts//', views.PartDownloadView.as_view(), name="part_download"), -] \ No newline at end of file +] + +from django.views.static import serve +urlpatterns.append(path('localstorage/', serve, {'document_root': 'local_storage'})) \ No newline at end of file diff --git a/library/views.py b/library/views.py index 4222b6e..a348608 100644 --- a/library/views.py +++ b/library/views.py @@ -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') \ No newline at end of file + return Part.objects.filter(doc__work__collection__allowed_ensembles__ensemble=self.request.ensemble_id).select_related('doc', 'doc__work') \ No newline at end of file diff --git a/polyphonic/settings_default.py b/polyphonic/settings_default.py index a1d8b75..7212ebd 100644 --- a/polyphonic/settings_default.py +++ b/polyphonic/settings_default.py @@ -84,6 +84,7 @@ DATABASES = { } } +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # Password validation # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators @@ -126,4 +127,4 @@ STATIC_URL = '/static/' # Need to set this AWS_BUCKET = '' -MEDIA_ROOT = 'media' \ No newline at end of file +MEDIA_ROOT = 'media' diff --git a/requirements.txt b/requirements.txt index 86c80d1..59f1452 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,14 @@ -asgiref==3.2.10 -boto3==1.15.11 -botocore==1.18.11 -Django==3.1.1 +asgiref==3.4.1 +boto3==1.18.34 +botocore==1.21.34 +Django==3.2.7 +django-byostorage @ git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@c67d636d2457faa57644cd812ca1b5a916e23766 django-markdown2==0.3.1 jmespath==0.10.0 -markdown2==2.3.9 -python-dateutil==2.8.1 -pytz==2020.1 -s3transfer==0.3.3 -six==1.15.0 -sqlparse==0.3.1 -urllib3==1.25.10 -git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@master#egg=django_byostorage +markdown2==2.4.1 +python-dateutil==2.8.2 +pytz==2021.1 +s3transfer==0.5.0 +six==1.16.0 +sqlparse==0.4.1 +urllib3==1.26.6