diff --git a/app/interface/templates/interface/project_items.html b/app/interface/templates/interface/project_items.html index 32faf4e..7b92ab8 100644 --- a/app/interface/templates/interface/project_items.html +++ b/app/interface/templates/interface/project_items.html @@ -6,27 +6,19 @@
-

{% if not ensemble %}{{ project.ensemble }}{% endif%} {{ project.name }}

+

+ {{ project.name }} +

{{ project.rough_date }}

-
- {{ project.description | markdown }} +
+ {{ project.description | markdown }} + {% if not ensemble %} +
With {{ project.ensemble }}
+ {% endif %}
-

- {% if project.deadline %}In {{ project.deadline|timeuntil }}
{% endif %} - {% if project.submissions.count %}{{ project.submissions.count }} submissions
{% endif %} -

-
-
diff --git a/app/interface/views.py b/app/interface/views.py index 1b84aa7..2a59928 100644 --- a/app/interface/views.py +++ b/app/interface/views.py @@ -5,9 +5,9 @@ from django.shortcuts import get_object_or_404, redirect, resolve_url from django.views.generic import RedirectView -from django.views.generic.detail import DetailView +from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.list import ListView -from django.views.generic.edit import CreateView, UpdateView +from django.views.generic.edit import CreateView, UpdateView, FormMixin from django.core.exceptions import SuspiciousOperation from django.http import Http404, HttpResponseRedirect from django.db.models import Q @@ -99,6 +99,8 @@ class AuthorizedResourceMixin(object): return super().dispatch(request, *args, **kwargs) +# TODO: RevokeResourceView - increment nonce + class ForgetResourceView(AuthorizedResourceMixin, RedirectView): def is_authorized(self): @@ -239,6 +241,11 @@ class EnsembleDetailView(EnsembleMixin, DetailView): data['ensemble_link'] = self.request.path + "?auth=" + self.ensemble.auth() return data +class EnsembleRevokeView(SingleObjectMixin, RedirectView): + + def get_redirect_url(self): + return + """ PROJECT VIEWS """ class ProjectListView(ProjectMixin, ListView): diff --git a/app/library/admin.py b/app/library/admin.py index 6d1f538..3fc3691 100644 --- a/app/library/admin.py +++ b/app/library/admin.py @@ -27,13 +27,14 @@ class MetaInline(admin.TabularInline): class WorkAdmin(admin.ModelAdmin): list_display = ['name', 'composer', 'edition', 'identifier', 'running_time'] list_filter = ['collection'] + search_fields = ['name', 'composer'] inlines = [MetaInline, DocInline, ItemInline] admin.site.register(models.Work, WorkAdmin) class SectionInline(admin.TabularInline): model = models.Section - fields = ['type', 'tag', 'ordinal', 'start', 'end'] + fields = ['type', 'tag', 'ordinal', 'start', 'end', 'page'] class DocumentAdmin(admin.ModelAdmin): list_display = ['work', '__str__'] @@ -52,4 +53,9 @@ 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 +admin.site.register(models.EnsembleAccess, EnsembleAccessAdmin) + +class OrchestrationAdmin(admin.ModelAdmin): + list_display = ['name', 'instruments'] + +admin.site.register(models.Orchestration, OrchestrationAdmin) \ No newline at end of file diff --git a/app/library/fixtures/default_orchestrations.json b/app/library/fixtures/default_orchestrations.json new file mode 100644 index 0000000..cd71ebb --- /dev/null +++ b/app/library/fixtures/default_orchestrations.json @@ -0,0 +1,38 @@ +[ +{ + "model": "library.orchestration", + "pk": 2, + "fields": { + "ensemble": null, + "name": "SATB", + "instruments": "score sop alt ten bass" + } +}, +{ + "model": "library.orchestration", + "pk": 3, + "fields": { + "ensemble": null, + "name": "String Quartet", + "instruments": "score vn-1 vn-2 va vc" + } +}, +{ + "model": "library.orchestration", + "pk": 4, + "fields": { + "ensemble": null, + "name": "Chamber Orchestra", + "instruments": "score fl cl ob bn vn-1 vn-2 va vc db" + } +}, +{ + "model": "library.orchestration", + "pk": 5, + "fields": { + "ensemble": null, + "name": "Full Orchestra", + "instruments": "score picc fl-1 fl-2 ob cl-1 cl-2 bcl bn tpt-1 tpt-2 tpt-3 hn-1 hn-2 hn-3 hn-4 tbn-1 tbn-2 tbn-3 tba timp perc-1 perc-2 mall pf vn-1 vn-2 va vc db" + } +} +] diff --git a/app/library/imslp.py b/app/library/imslp.py index 8799107..a5d372c 100644 --- a/app/library/imslp.py +++ b/app/library/imslp.py @@ -7,6 +7,7 @@ from collections import namedtuple ABBREVIATIONS = """ score Score cb Double bass +mall Mallet percussion acc Accordion afl Alto flute @@ -183,7 +184,11 @@ class Instrument(namedtuple('Instrument', ('name', 'variant'), defaults=[None])) if variant: return cls(name, variant) return cls(name, None) - + + @property + def tag(self): + l = self.name.lower() + return INSTRUMENT_TAGS.get(l, l) def abbreviate(self): """ diff --git a/app/library/migrations/0006_auto_20230202_0804.py b/app/library/migrations/0006_auto_20230202_0804.py new file mode 100644 index 0000000..ab476c9 --- /dev/null +++ b/app/library/migrations/0006_auto_20230202_0804.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.7 on 2023-02-01 21:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0005_auto_20230101_1547'), + ] + + operations = [ + migrations.AlterField( + model_name='section', + name='tag', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='section', + name='type', + field=models.SmallIntegerField(choices=[(1, 'Instrument'), (2, 'Movement'), (3, 'Excerpt')]), + ), + migrations.AlterField( + model_name='work', + name='licence', + field=models.PositiveSmallIntegerField(choices=[(2, 'Public Domain'), (4, 'Copyright Expired'), (5, 'Recording Licence'), (6, 'Performance Licence'), (8, 'Perusal Licence'), (10, 'Internal use only')], default=6, help_text='Copyright status'), + ), + ] diff --git a/app/library/migrations/0007_orchestration.py b/app/library/migrations/0007_orchestration.py new file mode 100644 index 0000000..d6972a0 --- /dev/null +++ b/app/library/migrations/0007_orchestration.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.7 on 2023-02-14 04:54 + +from django.db import migrations, models +import django.db.models.deletion + +def add_default_orchestration(apps, schema_editor): + m = apps.get_model('library', 'Orchestration').objects + m.create(pk=1, name="Custom", instruments="") + + +def remove_default_orchestration(apps, schema_editor): + m = apps.get_model('library', 'Orchestration').objects + m.filter(pk=1).delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('interface', '0004_auto_20230210_0938'), + ('library', '0006_auto_20230202_0804'), + ] + + operations = [ + migrations.CreateModel( + name='Orchestration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('instruments', models.TextField()), + ('ensemble', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='orchestrations', to='interface.ensemble')), + ], + ), + migrations.RunPython(add_default_orchestration, remove_default_orchestration), + ] diff --git a/app/library/migrations/0010_work_orchestration.py b/app/library/migrations/0010_work_orchestration.py new file mode 100644 index 0000000..74dc3a6 --- /dev/null +++ b/app/library/migrations/0010_work_orchestration.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.7 on 2023-02-14 05:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0007_orchestration'), + ] + + operations = [ + migrations.AddField( + model_name='work', + name='orchestration', + field=models.ForeignKey(default=1, help_text='Orchestration for the work', on_delete=django.db.models.deletion.SET_DEFAULT, to='library.orchestration'), + ), + ] diff --git a/app/library/migrations/0011_section_page.py b/app/library/migrations/0011_section_page.py new file mode 100644 index 0000000..5038b01 --- /dev/null +++ b/app/library/migrations/0011_section_page.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.7 on 2023-02-15 03:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0010_work_orchestration'), + ] + + operations = [ + migrations.AddField( + model_name='section', + name='page', + field=models.SmallIntegerField(choices=[(0, 'auto'), (1, 'left'), (2, 'right')], default=0), + ), + ] diff --git a/app/library/models.py b/app/library/models.py index 3ce9d27..56b5c2f 100644 --- a/app/library/models.py +++ b/app/library/models.py @@ -7,6 +7,8 @@ from django.utils.functional import cached_property from django.core.files.storage import get_storage_class from django.db.models import Q, Count, Min, Max +import re + from byostorage.user import BYOStorage from byostorage.cached import CachedStorage from .imslp import Instrument @@ -26,10 +28,6 @@ logger = logging.getLogger(__name__) # FIXME: move back to settings library_storage = CachedStorage(BYOStorage()) - - - -''' class Orchestration(models.Model): """ Stores a list of instrument codes as a single entry (space delimited). @@ -41,7 +39,21 @@ class Orchestration(models.Model): def as_list(self): tags = [ t.strip() for t in self.instruments.split(' ') ] - return [ (t, tag_to_instrument(t)) for t in tags if t ] + return [ (t, Instrument.from_tag(t)) for t in tags if t ] + + def tag_order(self): + tags = [ t.strip() for t in self.instruments.split(' ') if t ] + order = {'score': 0} + for i, t in enumerate(tags): + order.setdefault(t.strip('-0123456789'), i*2+1) + + return order + + def sorter(self): + tag_order = self.tag_order() + def f(x): + return (tag_order.get(x[0].strip('-0123456789'), 1000), x[0]) + return f def save(self): self.as_list() @@ -49,7 +61,6 @@ class Orchestration(models.Model): def __str__(self): return self.name -''' class ProjectItem(models.Model): """ @@ -160,7 +171,8 @@ class Work(models.Model): composer = models.CharField(max_length=255, default='Anon', help_text="Surname, Initials") - original_parts = models.JSONField(default=dict, help_text="Original printed parts (IMSLP format)") + orchestration = models.ForeignKey(Orchestration, on_delete=models.SET_DEFAULT, default=1, help_text="Orchestration for the work") + original_parts = models.JSONField(default=dict, blank=True, help_text="Original printed parts (IMSLP format)") # Collection details collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works") @@ -189,13 +201,17 @@ class Work(models.Model): @property def digital_parts(self): - return Section.objects.filter(doc__work=self.pk) + sections = [ (s.tag, s) for s in Section.objects.filter(doc__work=self.pk) ] + sections.sort(key=self.orchestration.sorter()) + return [ s[1] for s in sections ] @property def physical_parts(self): if not self.original_parts: return [] - return [ (Instrument.from_tag(k), v) for (k, v) in self.original_parts.items() ] + parts = list(self.original_parts.items()) + parts.sort(key=self.orchestration.sorter()) + return [ (Instrument.from_tag(x[0]), x[1]) for x in parts ] @property def tags(self): @@ -240,15 +256,18 @@ class Work(models.Model): return self.code; composer = self.composer or "Anon" + composer = re.sub('[^\w]', '', composer) words = self.name.split() - #if len(words) > 2: - # work = ''.join([ x[0] for x in self.name.split() ]) - #else: - # work = words[0][:3] work = words[0][:3] - return f"{composer[:4]}-{work}-{self.pk:03d}".upper() + return f"{composer[:4]}-{work}-{self.pk:05d}".upper() + def assigned_instruments(self): + return Section.objects.filter(doc__work_id=self.pk, type=Section.TYPE_INSTRUMENT).values_list('tag', flat=True) + + def unassigned_instruments(self): + assigned = set(self.assigned_instruments()) + return [ x for x in self.orchestration.as_list() if not x[0] in assigned ] def __str__(self): return f"{self.name} ({self.composer})" @@ -326,6 +345,16 @@ class Section(models.Model): TYPE_EXCERPT: 'warning', } + PAGE_AUTO = 0 + PAGE_LEFT = 1 + PAGE_RIGHT = 2 + + PAGE_PREFERENCE = ( + (PAGE_AUTO, 'auto'), + (PAGE_LEFT, 'left'), + (PAGE_RIGHT, 'right'), + ) + type = models.SmallIntegerField(choices=SECTION_TYPES) doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="sections") @@ -333,6 +362,7 @@ class Section(models.Model): ordinal = models.IntegerField(default=0) start = models.SmallIntegerField(null=True, blank=True) end = models.SmallIntegerField(null=True, blank=True) + page = models.SmallIntegerField(default=PAGE_AUTO, choices=PAGE_PREFERENCE) # NOT CURRENTLY USED class Meta: ordering = ['type', 'ordinal', 'doc', 'start', 'pk'] @@ -346,10 +376,6 @@ class Section(models.Model): return str(instr) return f"{self.ordinal} - {self.tag}" - #@property - #def instrument(self): - # return Instrument.from_tag(self.tag) - @property def bulma_class(self): return self.SECTION_CLASSES[self.type] diff --git a/app/library/templates/library/document_annotate.html b/app/library/templates/library/document_annotate.html index d36b81f..2cf3fba 100644 --- a/app/library/templates/library/document_annotate.html +++ b/app/library/templates/library/document_annotate.html @@ -5,7 +5,7 @@ Save - + Cancel {% endblock %} @@ -21,7 +21,7 @@ } .grid-page { height: 25px; - margin-bottom: 10px; + margin-bottom: 5px; border: 1px solid #999; border-radius: 5px; text-align: center; @@ -44,46 +44,45 @@ display: flex; flex-direction: row; justify-content: space-between; - align-items: center; + align-items: start; } #tag-area { min-width: 220px; } + .instrument { + display: list-item !important; + } + .instrument:hover { + background-color: var(--primary); + color: white; + } {% endblock %} {% block page %} -

{{ document.work.name }}

+

{{ document.work.name }}

+
+
+
+ +
+
+ - / - +
+
+ +
+
+ +
-
-
-
-

Page - / -

-
-
-
-
-
- - - - {% for inst in json_data.instruments.values %} - - - - - - - - -
-
-
-
@@ -97,9 +96,39 @@
+
+ + +

{{ document.upload.name }}

{% endblock %} @@ -194,10 +223,9 @@ if (pageNum <= 1) { return; } - pageNum--; - queueRenderPage(pageNum); + queueRenderPage(pageNum-1); } - //document.getElementById('prev').addEventListener('click', onPrevPage); + document.getElementById('prev').addEventListener('click', onPrevPage); /** * Displays next page. @@ -206,10 +234,9 @@ if (pageNum >= pdfDoc.numPages) { return; } - pageNum++; - queueRenderPage(pageNum); + queueRenderPage(pageNum+1); } - //document.getElementById('next').addEventListener('click', onNextPage); + document.getElementById('next').addEventListener('click', onNextPage); /** * Asynchronously downloads PDF. @@ -243,6 +270,12 @@ dirty = false; }); + function closeAddModal() { + document.getElementById('add-modal').classList.remove('is-active'); + document.getElementById('add-instrument-name').value = ""; + document.getElementById('add-instrument-variant').value = ""; + } + function addInstrument() { let name = document.getElementById('add-instrument-name'); let variant = document.getElementById('add-instrument-variant'); @@ -271,6 +304,11 @@ addTag(tag, pageNum, pageNum); } + function assignInstrument(tag, el) { + addTag(tag, pageNum, pageNum); + el.remove(); + } + function addTag(tag, start, end) { console.log("addTag", tag, start, end); const el = document.createElement('div'); @@ -295,7 +333,15 @@ let del = document.createElement('span'); del.className = "icon is-action"; del.innerHTML = ''; - del.addEventListener('click', () => {el.remove(), dirty=true}); + del.addEventListener('click', () => { + let li = document.createElement('li'); + li.classList.add("is-clickable"); + li.addEventListener('click', () => assignInstrument(tag, li)); + li.innerHTML = get_instrument(el.dataset.tag); + document.getElementById('unassigned-area').appendChild(li); + el.remove(); + dirty=true; + }); label.appendChild(del) el.appendChild(label); @@ -315,8 +361,8 @@ let start = tag.dataset.start; let end = tag.dataset.end; let span = end-start+1; - let height = span * 25 + (span-1) * 10; - let top = (start-1) * 35; + let height = span * 25 + (span-1) * 5; + let top = (start-1) * 30; tag.style.height = height + 'px'; tag.style.marginTop = top + 'px'; @@ -341,6 +387,19 @@ updateTag(el); } + function expandEntries() { + const entries = Array.from(tagArea.children); + entries.sort((a, b) => a.dataset.start-b.dataset.start); + const c = entries.length; + for (let i=0; i { if (response.ok) { - window.location = "{% url 'work_detail' document.work.pk %}" + window.location = "{% url 'work_detail' collection.pk document.work_id %}" } else { alert("Failed: " + response.statusText) } diff --git a/app/library/templates/library/document_confirm_delete.html b/app/library/templates/library/document_confirm_delete.html index 6fef7c3..bbbe5e5 100644 --- a/app/library/templates/library/document_confirm_delete.html +++ b/app/library/templates/library/document_confirm_delete.html @@ -13,7 +13,7 @@
- No + No
diff --git a/app/library/templates/library/document_entry.html b/app/library/templates/library/document_entry.html index fc72bd9..fbf6f4e 100644 --- a/app/library/templates/library/document_entry.html +++ b/app/library/templates/library/document_entry.html @@ -4,14 +4,14 @@ {{ doc.upload.name|basename }} {% for section in doc.sections.all %} - {{ section.name }} + {{ section.name }} {% endfor %} {% if request.is_admin %} - - + {% endif %} \ No newline at end of file diff --git a/app/library/templates/library/work_detail.html b/app/library/templates/library/work_detail.html index c899ded..06cadf0 100644 --- a/app/library/templates/library/work_detail.html +++ b/app/library/templates/library/work_detail.html @@ -7,11 +7,11 @@ {% endblock %} {% block admin %} - + Edit - + Add to project @@ -29,16 +29,27 @@

{{ work.notes }}

- Location: {{ work.collection }} [{{ work.identifier }}]
- Running time: {% firstof work.duration 'Unknown' %}
- Licence: {{ work.get_licence_display }}
+ + + + + + + + + + +
Location:{{ work.collection }} [{{ work.identifier }}]Orchestration:{{ work.orchestration }}
Running time:{% firstof work.duration 'Unknown' %}Licence:{{ work.get_licence_display }}
{% for meta in work.meta %} - {{ meta.get_name_display }}: {{ meta.value }}
+ + {{ meta.get_name_display }}: + {{ meta.value }} + {% endfor %} -

+
{% if work.parent %} -

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

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

{% endif %} @@ -62,7 +73,7 @@
{% for inst, c in work.physical_parts %} - {{ inst }} ({{ c }}) + {{ inst }}{% if c > 1 %} [{{ c }}]{% endif %} {% empty %}

No printed parts listed

{% endfor %} @@ -77,10 +88,10 @@
{% if work.digital_parts %} - Full Set + Full Set {% endif %} {% for section in work.digital_parts %} - {{ section.name }} {% empty %}

No digital parts available

@@ -118,7 +129,7 @@ {% if request.is_admin %}

Upload files

-
+ {% csrf_token %}
@@ -135,7 +146,7 @@ Loans - Checkout
diff --git a/app/library/urls.py b/app/library/urls.py index 2d7c6f4..9e213cc 100644 --- a/app/library/urls.py +++ b/app/library/urls.py @@ -31,10 +31,12 @@ urlpatterns = [ path('collections//docs//delete', views.DocumentDeleteView.as_view(), name="document_delete"), path('collections//docs//download', views.DocumentDownloadView.as_view(), name="document_download"), path('collections//docs//annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"), - path('collections//docs//', views.PartDownloadView.as_view(), name="part_download"), + + path('collections//download//', views.PartDownloadView.as_view(), name="part_download"), #path('api/', include(router.urls)) path('api/library/collections//export', api.CollectionExportView.as_view(), name="collection_export"), path('api/library/works//export', api.WorkExportView.as_view(), name="work_export"), path('api/library/collections//import', api.WorkImportView.as_view(), name="work_import"), + path('api/library/collections//bulk_import', api.CollectionImportView.as_view(), name="collection_import"), ] diff --git a/app/library/views/__init__.py b/app/library/views/__init__.py index 203eaf2..907358f 100644 --- a/app/library/views/__init__.py +++ b/app/library/views/__init__.py @@ -1,10 +1,11 @@ -from django.shortcuts import render, redirect, resolve_url +from django.shortcuts import get_object_or_404, redirect, resolve_url from django.views.generic.detail import DetailView, SingleObjectMixin, View from django.views.generic.list import ListView, MultipleObjectMixin from django.views.generic.edit import CreateView, FormView, UpdateView, DeleteView from django.http import FileResponse, HttpResponse, JsonResponse from django.db import IntegrityError from django.db.models import Q, Count, Sum +from django.db import transaction from django.utils.timezone import now from django.urls import reverse from django.template.loader import render_to_string @@ -119,40 +120,41 @@ class ProjectItemAddView(ProjectMixin, UpdateView): class CollectionMixin(AuthorizedResourceMixin): def is_authorized(self): - super().is_authorized() - try: - self.collection = self.get_collection() + collection_id = self.kwargs['collection'] + self.collection = get_object_or_404(models.Collection, pk=collection_id) + + if super().is_authorized(): return True - except (models.Collection.DoesNotExist, KeyError): - return False + + if self.collection.has_admininistrator(self.request.user): + self.request.is_admin = True + return True + + return False + + def get_context_data(self, **kwargs): + data = super().get_context_data(**kwargs) + if self.collection: + data['collection'] = self.collection + return data def get_queryset(self): return super().get_queryset().filter(collection=self.collection) - def get_collection(self): - collection = self.get_collection_queryset().get(pk=self.kwargs['collection']) - self.request.is_admin = collection.has_administrator(self.request.user) - return collection + +class CollectionListView(ListView): + paginate_by = 20 - def get_collection_queryset(self): + def get_queryset(self): collections = models.Collection.objects.all() if self.request.user.is_anonymous: return models.Collection.objects.none() - if self.request.is_admin: + if self.request.user.is_staff: return collections return collections.filter(Q(administrators=self.request.user) | Q(allowed_ensembles__ensemble__admins=self.request.user)) - -class CollectionListView(CollectionMixin, ListView): - paginate_by = 20 - - def is_authorized(self): - return True - - def get_queryset(self): - return self.get_collection_queryset() class WorkListView(CollectionMixin, ListView): paginate_by = 20 @@ -178,7 +180,7 @@ class WorkListView(CollectionMixin, ListView): else: works = works.filter(Q(name__contains=q) | Q(composer__contains=q) | Q(meta_info__value__contains=q)) - return works + return works.order_by('name', 'pk') class CollectionWorkListView(WorkListView): @@ -237,7 +239,7 @@ class WorkUpdateView(CollectionMixin, WorkMixin, UpdateView): -class WorkAddToProject(EnsembleMixin, FormView): +class WorkAddToProject(ProjectMixin, FormView): admin_required = True form_class = forms.ProjectSelectForm template_name = "interface/default_form.html" @@ -295,7 +297,7 @@ class WorkPartSetView(EnsembleMixin, DetailView): works = works.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id) return works -class WorkAddDocumentView(EnsembleMixin, CreateView): +class WorkAddDocumentView(CollectionMixin, CreateView): template_name = "interface/default_form.html" model = Document fields = ['upload'] @@ -333,22 +335,28 @@ class WorkAddDocumentView(EnsembleMixin, CreateView): return JsonResponse({ "message": "created", "id": doc.pk, - "entry": render_to_string('library/document_entry.html', {'doc': doc, 'request': self.request}) + "entry": render_to_string('library/document_entry.html', + {'collection': self.collection, 'doc': doc, 'request': self.request} + ) }, status=201) - return redirect('document_annotate', doc.pk) + return redirect('document_annotate', self.collection.pk, doc.pk) -class DocumentMixin(object): +class DocumentMixin(CollectionMixin): + model = models.Document def get_queryset(self): - if self.request.is_admin: - return Document.objects.select_related('work') - return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work') + return models.Document.objects.filter(work__collection=self.collection) -class DocumentDetailView(EnsembleMixin, DocumentMixin, DetailView): +# def get_queryset(self): +# if self.request.is_admin: +# return Document.objects.select_related('work') +# return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work') + +class DocumentDetailView(DocumentMixin, DetailView): pass -class DocumentDownloadView(EnsembleMixin, DocumentMixin, SingleObjectMixin, View): +class DocumentDownloadView(DocumentMixin, SingleObjectMixin, View): def get(self, request, **args): self.request = request @@ -359,7 +367,7 @@ class DocumentDownloadView(EnsembleMixin, DocumentMixin, SingleObjectMixin, View #return response return redirect(self.object.upload.url) -class DocumentAnnotateView(EnsembleMixin, DocumentMixin, DetailView): +class DocumentAnnotateView(DocumentMixin, DetailView): template_name = 'library/document_annotate.html' def post(self, request, **args): @@ -369,11 +377,12 @@ class DocumentAnnotateView(EnsembleMixin, DocumentMixin, DetailView): data = json.loads(request.body) - self.object.sections.all().delete() - for tag, start, end in data: - #pages.sort() - #end = pages[-1] if len(pages) > 1 else None - o = self.object.sections.create(tag=tag, start=start, end=end) + with transaction.atomic(): + self.object.sections.all().delete() + for tag, start, end in data: + #pages.sort() + #end = pages[-1] if len(pages) > 1 else None + o = self.object.sections.create(tag=tag, type=models.Section.TYPE_INSTRUMENT, start=start, end=end) return HttpResponse(status=204) @@ -387,15 +396,17 @@ class DocumentAnnotateView(EnsembleMixin, DocumentMixin, DetailView): data['json_data'] = {'pageTags': pages, 'instruments': dict(INSTRUMENTS)} return data -class DocumentDeleteView(EnsembleMixin, DocumentMixin, DeleteView): +class DocumentDeleteView(DocumentMixin, DeleteView): #def get_template_names(self): # return ["interface/default_form.html"] def get_success_url(self): - return resolve_url('work_detail', self.object.work.pk) + return resolve_url('work_detail', self.collection.pk, self.object.work_id) -class PartDownloadView(EnsembleMixin, SingleObjectMixin, View): +class PartDownloadView(CollectionMixin, SingleObjectMixin, View): + + pk_url_kwarg = 'section' def get(self, request, **args): self.request = request diff --git a/app/library/views/api.py b/app/library/views/api.py index b83e024..03b2e53 100644 --- a/app/library/views/api.py +++ b/app/library/views/api.py @@ -37,7 +37,7 @@ from interface.views import EnsembleMixin from rest_framework import routers, serializers, viewsets -from library.models import Collection, Work, Document, Section +from library.models import Collection, Work, Document, Section, WorkMeta import requests @@ -48,6 +48,17 @@ import shutil from django.db import transaction from django.core.files.uploadedfile import TemporaryUploadedFile +class WorkMetaSerializer(serializers.ModelSerializer): + class Meta: + model = WorkMeta + exclude = ['id', 'work'] + + def to_representation(self, instance): + return f"{instance.name}:{instance.value}" + + def to_internal_value(self, data): + name, _, value = data.partition(':') + return super().to_internal_value({'name': name, 'value': value}) class SectionSerializer(serializers.ModelSerializer): class Meta: @@ -111,6 +122,7 @@ class DocumentSerializer(serializers.ModelSerializer): class WorkSerializer(serializers.ModelSerializer): docs = DocumentSerializer(many=True) + meta_info = WorkMetaSerializer(many=True) class Meta: model = Work @@ -119,6 +131,7 @@ class WorkSerializer(serializers.ModelSerializer): def create(self, validated): with transaction.atomic(): docs = validated.pop('docs', []) + meta = validated.pop('meta_info', []) work = Work.objects.create(**validated) for d in docs: @@ -134,11 +147,25 @@ class WorkSerializer(serializers.ModelSerializer): for s in sections: Section.objects.create(doc_id=doc.pk, **s) + for m in meta: + WorkMeta.objects.create(work_id=work.pk, **m) + return work class CollectionSerializer(serializers.Serializer): works = WorkSerializer(many=True) + def create(self, validated): + s = WorkSerializer() + print(validated) + collection = validated['collection_id'] + with transaction.atomic(): + for work in validated['works']: + work['collection_id'] = collection + s.create(work) + return Collection.objects.get(pk=collection) + + from rest_framework import generics class CollectionExportView(generics.RetrieveAPIView): @@ -159,3 +186,8 @@ class WorkImportView(generics.CreateAPIView): def perform_create(self, serializer): serializer.save(collection_id=self.kwargs['pk']) +class CollectionImportView(generics.CreateAPIView): + serializer_class = CollectionSerializer + + def perform_create(self, serializer): + serializer.save(collection_id=self.kwargs['pk']) \ No newline at end of file