diff --git a/library/__init__.py b/library/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library/admin.py b/library/admin.py new file mode 100644 index 0000000..c3317e1 --- /dev/null +++ b/library/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin + +from . import models + +class PlaylistInline(admin.TabularInline): + model = models.Playlist + +class WorkAdmin(admin.ModelAdmin): + list_display = ['name', 'orchestration'] + inlines = [PlaylistInline] + +class PartInline(admin.TabularInline): + model = models.Part + fields = ['tag', 'start', 'end'] + +class DocumentAdmin(admin.ModelAdmin): + list_display = ['work', '__str__'] + inlines = [PartInline] + +class PlaylistAdmin(admin.ModelAdmin): + list_display = ['project', 'work', 'order'] + list_filter = ['project'] + +admin.site.register(models.Work, WorkAdmin) +admin.site.register(models.Document, DocumentAdmin) +admin.site.register(models.Playlist, PlaylistAdmin) \ No newline at end of file diff --git a/library/apps.py b/library/apps.py new file mode 100644 index 0000000..e01db0a --- /dev/null +++ b/library/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class LibraryConfig(AppConfig): + name = 'library' diff --git a/library/forms.py b/library/forms.py new file mode 100644 index 0000000..6c1c226 --- /dev/null +++ b/library/forms.py @@ -0,0 +1,24 @@ +from django import forms +from .models import Work + +class WorkCreateForm(forms.ModelForm): + uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True})) + + class Meta: + model = Work + fields = ['uploads', 'name', 'orchestration', 'running_time', 'notes'] + +class PlaylistAddForm(forms.Form): + work = forms.ModelChoiceField(queryset=Work.objects.all()) + + def __init__(self, instance, *args, **kwargs): + super(PlaylistAddForm, self).__init__(*args, **kwargs) + + existing = [ x[0] for x in instance.works.values_list('pk') ] + + qs = Work.objects.filter(ensemble_id=instance.ensemble_id).exclude(id__in=existing) + self.fields['work'].queryset = qs + self.instance = instance + + def save(self): + self.instance.works.add(self.cleaned_data['work']) \ No newline at end of file diff --git a/library/migrations/0001_initial.py b/library/migrations/0001_initial.py new file mode 100644 index 0000000..8c1d547 --- /dev/null +++ b/library/migrations/0001_initial.py @@ -0,0 +1,76 @@ +# Generated by Django 3.1.1 on 2021-03-10 22:20 + +from django.db import migrations, models +import django.db.models.deletion +import library.models +import library.storage + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('interface', '0022_auto_20210303_2043'), + ] + + operations = [ + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('upload', models.FileField(storage=library.storage.RemoteCachedStorage(), upload_to=library.models.upload_filename)), + ], + ), + migrations.CreateModel( + name='Playlist', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.SmallIntegerField(default=0)), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='interface.project')), + ], + options={ + 'ordering': ['order', 'work'], + }, + ), + migrations.CreateModel( + name='Work', + fields=[ + ('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)), + ('orchestration', models.CharField(choices=[('SATB', 'SATB'), ('String Quartet', 'String Quartet'), ('Chamber Orchestra', 'Chamber Orchestra'), ('RWE', 'RWE'), ('Custom', 'Custom')], max_length=100)), + ('running_time', models.IntegerField(blank=True, null=True)), + ('notes', models.TextField(blank=True)), + ('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='interface.ensemble')), + ('projects', models.ManyToManyField(related_name='works', through='library.Playlist', to='interface.Project')), + ], + options={ + 'unique_together': {('ensemble', 'slug')}, + }, + ), + migrations.AddField( + model_name='playlist', + name='work', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.work'), + ), + migrations.CreateModel( + name='Part', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tag', models.SlugField(max_length=20)), + ('start', models.SmallIntegerField(blank=True, null=True)), + ('end', models.SmallIntegerField(blank=True, null=True)), + ('notes', models.TextField(blank=True)), + ('doc', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parts', to='library.document')), + ], + options={ + 'ordering': ['doc', 'start', 'pk'], + }, + ), + migrations.AddField( + model_name='document', + name='work', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='docs', to='library.work'), + ), + ] diff --git a/library/migrations/__init__.py b/library/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library/models.py b/library/models.py new file mode 100644 index 0000000..3a8ae42 --- /dev/null +++ b/library/models.py @@ -0,0 +1,138 @@ +from django.conf import settings +from django.db import models +from django.utils.text import slugify +from django.core.files.storage import get_storage_class + +import re + +import logging +logger = logging.getLogger(__name__) + +try: + library_storage = get_storage_class(settings.LIBRARY_STORAGE)() +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', 'Drupset'), + ('Perc', 'Percussion'), + ('Pno', 'Piano'), + ('Hp', 'Harp'), +] + +ORCHESTRATIONS = { + 'SATB': ('S', 'A', 'T', 'B'), + 'String Quartet': ('Vln1', 'Vln2', 'Vla', 'Vc'), + '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': () +} + +def tag_to_instrument(tag): + m = re.match(r'([A-Za-z]+)(\d*)', tag) + if not m: + return tag + l = m.groups() + return "{0} {1}".format(dict(INSTRUMENTS).get(l[0],l[0]), l[1]).strip() + +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) + orchestration = models.CharField(max_length=100, choices=[ (k, k) for k, v in ORCHESTRATIONS.items() ]) + running_time = models.IntegerField(null=True, blank=True) + notes = models.TextField(blank=True) + projects = models.ManyToManyField('interface.Project', through='Playlist', 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): + tags = ORCHESTRATIONS.get(self.orchestration, 'Custom') + return [ (tag, tag_to_instrument(tag)) for tag in tags ] + + def save(self): + if not self.slug: + self.slug = slugify(self.name) + super(Work, self).save() + + def __str__(self): + return self.name + +class Playlist(models.Model): + project = models.ForeignKey('interface.Project', on_delete=models.CASCADE) + work = models.ForeignKey('Work', on_delete=models.CASCADE) + order = models.SmallIntegerField(default=0) + + class Meta: + ordering = ['order', 'work'] + + def __str__(self): + return f"<{self.project.slug}:{self.work.slug}>" + +def upload_filename(instance, filename): + return f'{instance.work.ensemble.slug}/works/{instance.work.slug}/{filename}' + +class Document(models.Model): + work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs") + upload = models.FileField(upload_to=upload_filename, storage=library_storage) + + def __str__(self): + return self.upload.name + +class Part(models.Model): + doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="parts") + tag = models.SlugField(max_length=20) + start = models.SmallIntegerField(null=True, blank=True) + end = models.SmallIntegerField(null=True, blank=True) + notes = models.TextField(blank=True) + + class Meta: + ordering = ['doc', 'start', 'pk'] + + @property + def instrument(self): + return tag_to_instrument(self.tag) + + @property + def filename(self): + return slugify(f'{self.doc.work.name}_{self.instrument}') + '.pdf' + + @property + def pagerange(self): + if self.start: + if self.end: + return f"{self.start}-{self.end}" + return str(self.start) + return "all" + + def __str__(self): + return f'{self.doc.upload} [{self.pagerange}]' \ No newline at end of file diff --git a/library/pdf_utils.py b/library/pdf_utils.py new file mode 100644 index 0000000..b7ceba4 --- /dev/null +++ b/library/pdf_utils.py @@ -0,0 +1,46 @@ +import tempfile +import subprocess +import os.path + +def extract_pages(source, bookmark, start=None, end=None): + + return extract_and_concat([(source, bookmark, start, end)]) + +def extract_and_concat(items): + + # create a temporary directory for our sections + d = tempfile.TemporaryDirectory(prefix="polyphonic_") + + sections = [] + + for i, (source, bookmark, start, end) in enumerate(items): + + if start is None: + sections.append(source) + + else: + + if end is None: + end = start + + dest = os.path.join(d.name, f'section_{i}.pdf') + + cmd = ['gs', '-sDEVICE=pdfwrite', '-q', '-dBATCH', '-dNOPAUSE', + f'-dFirstPage={start}', f'-dLastPage={end}', + f'-sOutputFile={dest}', + source] + + subprocess.run(cmd, check=True) + sections.append(dest) + + # concat the items + output = tempfile.NamedTemporaryFile(prefix="polyphonic_", suffix='.pdf') + + cmd = ['gs', '-sDEVICE=pdfwrite', '-q', '-dBATCH', '-dNOPAUSE', + '-sOutputFile=-'] + cmd.extend(sections) + + subprocess.run(cmd, stdout=output) + + output.seek(0) + return output diff --git a/library/storage.py b/library/storage.py new file mode 100644 index 0000000..822e511 --- /dev/null +++ b/library/storage.py @@ -0,0 +1,96 @@ +from django.core.files.storage import Storage, get_storage_class +from django.utils.deconstruct import deconstructible +from django.conf import settings +from hashlib import sha1 +import os.path +import shutil +import tempfile +import time + +import logging +logger = logging.getLogger(__name__) + +@deconstructible +class RemoteCachedStorage(Storage): + + CACHE_EXPIRES = 30 + + def __init__(self, remote=None, cachedir=None): + if not remote: + remote = settings.CACHED_STORAGE_REMOTE + if not cachedir: + cachedir = settings.CACHED_STORAGE_DIR + self.remote = get_storage_class(remote)() + self.cachedir = cachedir + os.makedirs(self.cachedir, exist_ok=True) + self.clean() + + def _filepath(self, name): + base, ext = os.path.splitext(name) + filename = sha1(base.encode('utf8')).hexdigest() + ext + return os.path.join(self.cachedir, filename) + + def _cached(self, name): + p = self._filepath(name) + if not os.path.exists(p): + logger.debug("Caching %s to %s", name, p) + source = self.remote._open(name, 'rb') + dest = tempfile.NamedTemporaryFile(dir=self.cachedir, delete=False, prefix="_") + shutil.copyfileobj(source, dest) + source.close() + dest.close() + os.rename(dest.name, p) + now = time.time() + os.utime(p, (now, now)) + + if now > self.next_check: + self.clean() # wont get this file as we just touched it + + return p + + + def _open(self, name, mode='rb'): + p = self._cached(name) + return open(p, mode) + + def path(self, name): + return self._cached(name) + + def _save(self, name, content): + return self.remote._save(name, content) + + def delete(self, name): + return self.remote.delete(name) + + def exists(self, name): + return self.remote.exists(name) + + def listdir(self, name): + return self.remote.listdir(name) + + def size(self, name): + return self.remote.size(name) + + def url(self, name): + return self.remote.url(name) + + def get_valid_name(self, name): + return self.remote.get_valid_name(name) + + def get_available_name(self, name, max_length=None): + return self.remote.get_available_name(name, max_length) + + def get_alternative_name(self, file_root, file_ext): + return self.remote.get_alternative_name(file_root, file_ext) + + def clean(self): + now = time.time() + threshold = now - self.CACHE_EXPIRES + logger.info("Removing cached files older than %d seconds", self.CACHE_EXPIRES) + for f in os.listdir(self.cachedir): + f = os.path.join(self.cachedir, f) + s = os.stat(f) + if s.st_atime < threshold: + logger.debug("Removing %s", f) + os.unlink(f) + self.next_check = now + 300 \ No newline at end of file diff --git a/library/templates/library/document_annotate.html b/library/templates/library/document_annotate.html new file mode 100644 index 0000000..a1bc96c --- /dev/null +++ b/library/templates/library/document_annotate.html @@ -0,0 +1,193 @@ +{% extends "interface/project_base.html" %} + +{% block admin %} + Save  +{% endblock %} + +{% block page %} +

Works / {{ document.work.name }}

+
+
+ +
+
+
+

Page: /

+
+ + +
+
+
No tag
+
Score
+ {% for instrument in document.work.instruments %} +
{{ instrument.1 }}
+ {% endfor %} +
+
+
+
+

{{ document.upload.name }}

+ +{% endblock %} + +{% block scripts %} + +{{ json_data|json_script:"data" }} + +{% endblock %} \ No newline at end of file diff --git a/library/templates/library/folder_detail.html b/library/templates/library/folder_detail.html new file mode 100644 index 0000000..0780f21 --- /dev/null +++ b/library/templates/library/folder_detail.html @@ -0,0 +1,11 @@ +{% extends 'interface/project_base.html' %} + +{% block page %} +

{{ folder.name }}

+

Works

+ +{% endblock %} \ No newline at end of file diff --git a/library/templates/library/playlist_manage.html b/library/templates/library/playlist_manage.html new file mode 100644 index 0000000..4bdc2ca --- /dev/null +++ b/library/templates/library/playlist_manage.html @@ -0,0 +1,94 @@ +{% extends "interface/project_base.html" %} + +{% block admin %} +Save +Add +{% endblock %} + +{% block page %} + + + + + + + + + {% for item in object_list %} + + + + + + {% endfor %} + +
PiecePart
{{ item.work.name }} + + + +
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/library/templates/library/playlist_view.html b/library/templates/library/playlist_view.html new file mode 100644 index 0000000..a2c782e --- /dev/null +++ b/library/templates/library/playlist_view.html @@ -0,0 +1,84 @@ +{% extends "interface/project_base.html" %} + +{% block admin %} +Change works +{% endblock %} + +{% block page %} +

+ This page lets you download a complete set of music for your part by selecting your instrument + and optionally a part (e.g. Flute 1 or 2).
+ You can also tweak which parts you get in the list or click on a piece for more download options. +

+
+{% csrf_token %} + + + + + {% for item in object_list %} + + + + + {% endfor %} + +
+ {{ item.work.name }} + + + +
+
+ +

+ +

+ +
+
+ +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/library/templates/library/project_menu.html b/library/templates/library/project_menu.html new file mode 100644 index 0000000..940e22f --- /dev/null +++ b/library/templates/library/project_menu.html @@ -0,0 +1 @@ +My Music \ No newline at end of file diff --git a/library/templates/library/work_detail.html b/library/templates/library/work_detail.html new file mode 100644 index 0000000..0d16c0c --- /dev/null +++ b/library/templates/library/work_detail.html @@ -0,0 +1,28 @@ +{% extends 'interface/project_base.html' %} + +{% block admin %} + Upload file +{% endblock %} + +{% block page %} +

Works / {{ work.name }}

+

{{ work.notes }}

+

Parts

+ + +

Original Documents

+ +{% endblock %} \ No newline at end of file diff --git a/library/templates/library/work_list.html b/library/templates/library/work_list.html new file mode 100644 index 0000000..f7b379d --- /dev/null +++ b/library/templates/library/work_list.html @@ -0,0 +1,25 @@ +{% extends "interface/project_base.html" %} + +{% block admin %} + Add new +{% endblock %} + +{% block page %} +

Works

+ + + + + {% for work in object_list %} + {% with work.docs.count as doc_count %} + {% with work.parts.count as part_count %} + + + + + {% endwith %} + {% endwith %} + {% endfor %} + +
{{ work.name }}{{ doc_count }} file{{ doc_count|pluralize }} with {{ part_count }} part{{ part_count|pluralize }}
+{% endblock %} \ No newline at end of file diff --git a/library/tests.py b/library/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/library/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/library/urls.py b/library/urls.py new file mode 100644 index 0000000..bce8fbe --- /dev/null +++ b/library/urls.py @@ -0,0 +1,19 @@ +from django.urls import path +from django.contrib.auth import views as auth_views + +from . import views + +urlpatterns = [ + + path('projects//works', views.PlaylistView.as_view(), name="project_playlist"), + path('projects//works/manage', views.PlaylistManageView.as_view(), name="playlist_manage"), + path('projects//works/append', views.PlaylistAddView.as_view(), name="playlist_append"), + + path('library/works', views.WorkListView.as_view(), name="work_list"), + path('library/works/add', views.WorkAddView.as_view(), name="work_add"), + path('library/works/', views.WorkDetailView.as_view(), name="work_detail"), + path('library/works//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 diff --git a/library/views.py b/library/views.py new file mode 100644 index 0000000..4f67cc4 --- /dev/null +++ b/library/views.py @@ -0,0 +1,217 @@ +from django.shortcuts import render, redirect, resolve_url +from django.views.generic.detail import DetailView, SingleObjectMixin, View +from django.views.generic.list import ListView +from django.views.generic.edit import CreateView, FormView, UpdateView +from django.http import FileResponse, HttpResponse +from django.db import IntegrityError + +import json + +from interface.views import EnsembleMixin, ProjectMixin +from interface.models import Project +from .models import Work, Document, Part, Playlist, INSTRUMENTS +from . import forms +from .pdf_utils import extract_pages, extract_and_concat + +class PlaylistView(ProjectMixin, ListView): + template_name = "library/playlist_view.html" + model = Playlist + + def post(self, request, **kwargs): + + project = self.get_project() + + project_works = project.works.all() + + instruments = request.POST.getlist('instruments') + works = request.POST.getlist('works') + + self.request.session['part'] = request.POST.get('part', '') + self.request.session['instrument'] = request.POST.get('instrument') + + valid_pks = [ x.pk for x in project_works ] + + sections = [] + + for i, pk in enumerate(works): + + if int(pk) not in valid_pks: + raise Exception(f"Not a valid work pk: {pk}") + + tag = instruments[i] + if tag == '-': + continue + + part = Part.objects.filter(tag=tag, doc__work=pk).select_related('doc').get() + sections.append((part.doc.upload.path, part.doc.work.name, part.start, part.end)) + + result = extract_and_concat(sections) + + download_name = f'{project.name}.pdf' + + response = FileResponse(result, content_type="application/pdf") + response['Content-Disposition'] = f'inline; filename="{download_name}"' + return response + + + def get_queryset(self): + return super(PlaylistView, self).get_queryset().select_related('project', 'work') + + def get_context_data(self, **kwargs): + data = super(PlaylistView, self).get_context_data(**kwargs) + data['instruments'] = INSTRUMENTS + data['instrument'] = self.request.session.get('instrument', 'Score') + data['part'] = self.request.session.get('part', '0') + return data + +class PlaylistManageView(ProjectMixin, ListView): + template_name = "library/playlist_manage.html" + model = Playlist + + def post(self, request, **kwargs): + self.request = request + self.kwargs = kwargs + + data = json.loads(request.body) + + q = self.get_queryset() + for pk, order in data.items(): + i = q.filter(pk=pk).update(order=order) + + return HttpResponse(status=204) + + def get_queryset(self): + return super(PlaylistManageView, self).get_queryset().select_related('project', 'work') + +class PlaylistAddView(ProjectMixin, UpdateView): + form_class = forms.PlaylistAddForm + template_name = "interface/default_form.html" + + def get_success_url(self): + return resolve_url('playlist_manage', project=self.kwargs['project']) + + def get_object(self): + return self.get_project() + +class WorkListView(EnsembleMixin, ListView): + + def get_queryset(self): + return Work.objects.filter(ensemble=self.request.ensemble_id).order_by('name') + +class WorkAddView(EnsembleMixin, FormView): + template_name = "interface/default_form.html" + form_class = forms.WorkCreateForm + + 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) + + # handle the files + uploads = self.request.FILES.getlist('uploads') + docs = [] + for f in uploads: + docs.append(obj.docs.create(upload=f).pk) + + if len(docs) == 1: + return redirect('document_annotate', docs[0]) + else: + return redirect('work_detail', pk=obj.pk) + +class WorkDetailView(EnsembleMixin, DetailView): + + def get_queryset(self): + return Work.objects.filter(ensemble=self.request.ensemble_id) + + +class DocumentDetailView(EnsembleMixin, DetailView): + + def get_queryset(self): + return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work') + +class DocumentAddView(EnsembleMixin, CreateView): + template_name = "interface/default_form.html" + model = Document + fields = ['upload'] + + def form_valid(self, form): + self.object = form.save(commit=False) + self.object.work_id = self.kwargs['pk'] + self.object.save() + return redirect('document_annotate', self.object.pk) + +class DocumentDownloadView(EnsembleMixin, SingleObjectMixin, View): + + def get(self, request, **args): + self.request = request + self.args = args + self.object = self.get_object() + + #response = FileResponse(self.object.upload, content_type="application/pdf") + #return response + return redirect(self.object.upload.url) + + def get_queryset(self): + return Document.objects.filter(work__ensemble=self.request.ensemble_id) + +class DocumentAnnotateView(EnsembleMixin, DetailView): + template_name = 'library/document_annotate.html' + + def post(self, request, **args): + self.request = request + self.args = args + self.object = self.get_object() + + data = json.loads(request.body) + + tags = {} + for page, tag in data.items(): + tags.setdefault(tag, []).append(int(page)) + try: + del(tags['None']) + except KeyError: + pass + + self.object.parts.all().delete() + for tag, pages in tags.items(): + pages.sort() + end = pages[-1] if len(pages) > 1 else None + o = self.object.parts.create(tag=tag, start=pages[0], end=end) + + return HttpResponse(status=204) + + def get_context_data(self, **kwargs): + data = super(DocumentAnnotateView, self).get_context_data(**kwargs) + + pages = {} + for part in data['document'].parts.all(): + 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} + return data + + def get_queryset(self): + return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work') + +class PartDownloadView(EnsembleMixin, SingleObjectMixin, View): + + def get(self, request, **args): + self.request = request + self.args = args + self.object = self.get_object() + + result = extract_pages(self.object.doc.upload.path, self.object.doc.work.name, self.object.start, self.object.end) + + download_name = f'{self.object.doc.work.name}_{self.object.instrument}.pdf' + + response = FileResponse(result, content_type="application/pdf") + response['Content-Disposition'] = f'inline; filename="foo.pdf"' + 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