Created library app
This commit is contained in:
parent
98fe7b77ce
commit
526ae4f59b
0
library/__init__.py
Normal file
0
library/__init__.py
Normal file
26
library/admin.py
Normal file
26
library/admin.py
Normal file
@ -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)
|
||||||
5
library/apps.py
Normal file
5
library/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryConfig(AppConfig):
|
||||||
|
name = 'library'
|
||||||
24
library/forms.py
Normal file
24
library/forms.py
Normal file
@ -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'])
|
||||||
76
library/migrations/0001_initial.py
Normal file
76
library/migrations/0001_initial.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
library/migrations/__init__.py
Normal file
0
library/migrations/__init__.py
Normal file
138
library/models.py
Normal file
138
library/models.py
Normal file
@ -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}]'
|
||||||
46
library/pdf_utils.py
Normal file
46
library/pdf_utils.py
Normal file
@ -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
|
||||||
96
library/storage.py
Normal file
96
library/storage.py
Normal file
@ -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
|
||||||
193
library/templates/library/document_annotate.html
Normal file
193
library/templates/library/document_annotate.html
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
{% extends "interface/project_base.html" %}
|
||||||
|
|
||||||
|
{% block admin %}
|
||||||
|
<a href="#" onclick="saveTags()"><i class="fas fa-save"></i> Save</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<h2><a href="{% url 'work_list' %}">Works</a> / <a href="{% url 'work_detail' document.work.pk %}">{{ document.work.name }}</a></h2>
|
||||||
|
<div id="annotation-area">
|
||||||
|
<div style="display: flex">
|
||||||
|
<canvas id="inline-viewer" style="width: 600px;"></canvas>
|
||||||
|
<div style="margin: 0px 20px">
|
||||||
|
<div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<p>Page: <span id="page_num"></span> / <span id="page_count"></span></p>
|
||||||
|
</div>
|
||||||
|
<button id="prev"><i class="fas fa-backward"></i></button>
|
||||||
|
<button id="next"><i class="fas fa-forward"></i></button>
|
||||||
|
</div>
|
||||||
|
<div id="tag-list" style="cursor: pointer; margin-top: 20px;">
|
||||||
|
<div id="tag-None" data-tag="None">No tag</div>
|
||||||
|
<div id="tag-Score" data-tag="Score">Score</div>
|
||||||
|
{% for instrument in document.work.instruments %}
|
||||||
|
<div id="tag-{{ instrument.0 }}" data-tag="{{ instrument.0 }}">{{ instrument.1 }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>{{ document.upload.name }}</p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.min.js" integrity="sha512-g4FwCPWM/fZB1Eie86ZwKjOP+yBIxSBM/b2gQAiSVqCgkyvZ0XxYPDEcN2qqaKKEvK6a05+IPL1raO96RrhYDQ==" crossorigin="anonymous"></script>
|
||||||
|
{{ json_data|json_script:"data" }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
let url = "{{ document.upload.url|safe }}";
|
||||||
|
|
||||||
|
// Loaded via <script> tag, create shortcut to access PDF.js exports.
|
||||||
|
let pdfjsLib = window['pdfjs-dist/build/pdf'];
|
||||||
|
|
||||||
|
// The workerSrc property shall be specified.
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.worker.min.js';
|
||||||
|
|
||||||
|
// get current page tags
|
||||||
|
let data = JSON.parse(document.getElementById('data').textContent);
|
||||||
|
let currentTags = document.getElementById('current-tags');
|
||||||
|
var selectedTag = document.getElementById('tag-None');
|
||||||
|
var dirty = false;
|
||||||
|
|
||||||
|
document.getElementById('tag-list').onclick = (e) => setTag(e.target.dataset.tag);
|
||||||
|
|
||||||
|
var pdfDoc = null,
|
||||||
|
pageNum = 1,
|
||||||
|
pageRendering = false,
|
||||||
|
pageNumPending = null,
|
||||||
|
scale = 1,
|
||||||
|
canvas = document.getElementById('inline-viewer'),
|
||||||
|
ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page info from document, resize canvas accordingly, and render page.
|
||||||
|
* @param num Page number.
|
||||||
|
*/
|
||||||
|
function renderPage(num) {
|
||||||
|
pageRendering = true;
|
||||||
|
// Using promise to fetch the page
|
||||||
|
pdfDoc.getPage(num).then(function(page) {
|
||||||
|
var viewport = page.getViewport({scale: scale});
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
|
// Render PDF page into canvas context
|
||||||
|
var renderContext = {
|
||||||
|
canvasContext: ctx,
|
||||||
|
viewport: viewport
|
||||||
|
};
|
||||||
|
var renderTask = page.render(renderContext);
|
||||||
|
|
||||||
|
// Wait for rendering to finish
|
||||||
|
renderTask.promise.then(function() {
|
||||||
|
pageRendering = false;
|
||||||
|
if (pageNumPending !== null) {
|
||||||
|
// New page rendering is pending
|
||||||
|
renderPage(pageNumPending);
|
||||||
|
pageNumPending = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update page counters
|
||||||
|
document.getElementById('page_num').textContent = num;
|
||||||
|
|
||||||
|
// get the page tags
|
||||||
|
let tag = data.pageTags[num];
|
||||||
|
if(tag) {
|
||||||
|
selectTag(tag);
|
||||||
|
} else {
|
||||||
|
if (selectedTag) {
|
||||||
|
setTag(selectedTag.dataset.tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If another page rendering in progress, waits until the rendering is
|
||||||
|
* finised. Otherwise, executes rendering immediately.
|
||||||
|
*/
|
||||||
|
function queueRenderPage(num) {
|
||||||
|
if (pageRendering) {
|
||||||
|
pageNumPending = num;
|
||||||
|
} else {
|
||||||
|
renderPage(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays previous page.
|
||||||
|
*/
|
||||||
|
function onPrevPage() {
|
||||||
|
if (pageNum <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pageNum--;
|
||||||
|
queueRenderPage(pageNum);
|
||||||
|
}
|
||||||
|
document.getElementById('prev').addEventListener('click', onPrevPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays next page.
|
||||||
|
*/
|
||||||
|
function onNextPage() {
|
||||||
|
if (pageNum >= pdfDoc.numPages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pageNum++;
|
||||||
|
queueRenderPage(pageNum);
|
||||||
|
}
|
||||||
|
document.getElementById('next').addEventListener('click', onNextPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously downloads PDF.
|
||||||
|
*/
|
||||||
|
pdfjsLib.getDocument(url).promise.then(function(pdfDoc_) {
|
||||||
|
pdfDoc = pdfDoc_;
|
||||||
|
document.getElementById('page_count').textContent = pdfDoc.numPages;
|
||||||
|
|
||||||
|
// Initial/first page rendering
|
||||||
|
renderPage(pageNum);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setTag(tag) {
|
||||||
|
data.pageTags[pageNum] = tag;
|
||||||
|
dirty = true;
|
||||||
|
selectTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTag(tag) {
|
||||||
|
if( selectedTag ) {
|
||||||
|
selectedTag.classList.remove('selected');
|
||||||
|
}
|
||||||
|
selectedTag = document.getElementById('tag-' + tag);
|
||||||
|
if( selectedTag) {
|
||||||
|
selectedTag.classList.add('selected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTags() {
|
||||||
|
console.log(data.pageTags);
|
||||||
|
fetch("", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json", "X-CSRFToken": "{{ csrf_token }}"},
|
||||||
|
body: JSON.stringify(data.pageTags)}
|
||||||
|
).then((response) => {
|
||||||
|
if(response.ok) {
|
||||||
|
window.location = "{% url 'work_detail' document.work.pk %}"
|
||||||
|
} else {
|
||||||
|
alert("Failed: " + response.statusText)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSaved(e) {
|
||||||
|
if (dirty) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('beforeunload', checkSaved);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
11
library/templates/library/folder_detail.html
Normal file
11
library/templates/library/folder_detail.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'interface/project_base.html' %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<h2>{{ folder.name }}</h2>
|
||||||
|
<h3>Works</h3>
|
||||||
|
<ul>
|
||||||
|
{% for work in folder.works.all %}
|
||||||
|
<li>{{ work.name }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
94
library/templates/library/playlist_manage.html
Normal file
94
library/templates/library/playlist_manage.html
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{% extends "interface/project_base.html" %}
|
||||||
|
|
||||||
|
{% block admin %}
|
||||||
|
<a href="#" onclick="save()"><i class="fas fa-save"></i><span class="smhide action">Save</span></a>
|
||||||
|
<a href="{% url 'playlist_append' project.pk %}"><i class="fas fa-plus-circle"></i><span class="smhide action">Add</a></a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<table style="max-width: 600px; margin: 10pt auto;" class="item-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Piece</th>
|
||||||
|
<th>Part</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="work-list">
|
||||||
|
{% for item in object_list %}
|
||||||
|
<tr data-pk="{{ item.pk }}" data-order="{{ forloop.counter }}">
|
||||||
|
<td>{{ item.work.name }}</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
<i class="fas fa-arrow-up clickable" title="Move up" onclick="moveItem({{ item.pk }}, -1)"></i>
|
||||||
|
<i class="fas fa-arrow-down clickable" title="Move down" onclick="moveItem({{ item.pk }}, 1)"></i>
|
||||||
|
<i class="fas fa-trash clickable" title="Remove"></i>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
let workList = document.getElementById('work-list');
|
||||||
|
var dirty = false;
|
||||||
|
|
||||||
|
function moveItem(pk, dir) {
|
||||||
|
let items = Array.prototype.slice.call(workList.children, 0);
|
||||||
|
var i=0;
|
||||||
|
pk = "" + pk;
|
||||||
|
for(i=0; i<items.length; i++) {
|
||||||
|
if(items[i].dataset.pk === pk) break;
|
||||||
|
}
|
||||||
|
if(i >= items.length) return;
|
||||||
|
|
||||||
|
// check the direction is sensible
|
||||||
|
if (i + dir < 0 || i + dir >= items.length) return;
|
||||||
|
|
||||||
|
items[i].dataset.order = parseInt(items[i].dataset.order) + dir;
|
||||||
|
items[i+dir].dataset.order = parseInt(items[i+dir].dataset.order) - dir;
|
||||||
|
|
||||||
|
items.sort((a, b) => parseInt(a.dataset.order) - parseInt(b.dataset.order))
|
||||||
|
|
||||||
|
workList.innerHTML = ""
|
||||||
|
for(let j=0; j<items.length; j++) {
|
||||||
|
workList.appendChild(items[j]);
|
||||||
|
}
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
|
||||||
|
let items = Array.prototype.slice.call(workList.children, 0);
|
||||||
|
let data = {};
|
||||||
|
for(var i=0; i<items.length; i++) {
|
||||||
|
let item = items[i].dataset;
|
||||||
|
data[item.pk] = item.order;
|
||||||
|
}
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
fetch("", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json", "X-CSRFToken": "{{ csrf_token }}"},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
dirty=false;
|
||||||
|
window.location = "{% url 'project_playlist' project=project.pk %}";
|
||||||
|
} else {
|
||||||
|
alert("Failed: " + response.statusText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSaved(e) {
|
||||||
|
if(dirty) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('beforeunload', checkSaved);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
84
library/templates/library/playlist_view.html
Normal file
84
library/templates/library/playlist_view.html
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{% extends "interface/project_base.html" %}
|
||||||
|
|
||||||
|
{% block admin %}
|
||||||
|
<a href="{% url 'playlist_manage' project.pk %}"><i class="fas fa-list"></i><span class="smhide admin">Change works</span></a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<p>
|
||||||
|
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).<br/>
|
||||||
|
You can also tweak which parts you get in the list or click on a piece for more download options.
|
||||||
|
</p>
|
||||||
|
<form action="" method="post" target="_blank" style="display: flex;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table style="max-width: 600px; margin: 10pt auto;" class="item-table">
|
||||||
|
<thead>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="select-cell">
|
||||||
|
<input type="hidden" name="works" value="{{ item.work.pk }}"/>
|
||||||
|
<select name="instruments">
|
||||||
|
<option value='-'>None</option>
|
||||||
|
{% for part in item.work.parts %}
|
||||||
|
<option value='{{ part.tag }}'>{{ part.instrument }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div style="margin: 0px 20px; text-align: right;">
|
||||||
|
<label for="instrument-select">Pick your instrument</label>
|
||||||
|
<select id="instrument-select" name="instrument" onchange="updateParts()">
|
||||||
|
{% for tag, instrument in instruments %}
|
||||||
|
<option value="{{ tag }}">{{ instrument }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select><br/><br/>
|
||||||
|
<label for="part-preference">Part preference</label>
|
||||||
|
<input id="part-preference" name="part" type="number" value="0" min="0" max="4" onchange="updateParts()" size="1"/><br/><br/>
|
||||||
|
<button type="submit" style="height: 32px;"><i class="fas fa-copy"></i> Get My Parts!</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function updateParts() {
|
||||||
|
let inst = document.getElementById("instrument-select").value;
|
||||||
|
let part = document.getElementById("part-preference").value;
|
||||||
|
let prefix = inst + part;
|
||||||
|
|
||||||
|
let instruments = document.getElementsByName("instruments");
|
||||||
|
for(let i=0; i<instruments.length; i++) {
|
||||||
|
var result = "-"
|
||||||
|
let options = instruments[i].children;
|
||||||
|
for(let j=0; j<options.length; j++) {
|
||||||
|
let value = options[j].value;
|
||||||
|
if (value.startsWith(prefix)) {
|
||||||
|
result = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (result==="-" && value.startsWith(inst)) {
|
||||||
|
result = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instruments[i].value = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("instrument-select").value="{{instrument}}";
|
||||||
|
document.getElementById("part-preference").value="{{part}}";
|
||||||
|
updateParts();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
1
library/templates/library/project_menu.html
Normal file
1
library/templates/library/project_menu.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<a href="{% url 'project_playlist' project=project.pk %}">My Music</a>
|
||||||
28
library/templates/library/work_detail.html
Normal file
28
library/templates/library/work_detail.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'interface/project_base.html' %}
|
||||||
|
|
||||||
|
{% block admin %}
|
||||||
|
<a href="{% url 'document_add' work.pk %}"><i class="fas fa-plus-circle"></i> Upload file</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<h2><a href="{% url 'work_list' %}">Works</a> / {{ work.name }}</h2>
|
||||||
|
<p>{{ work.notes }}</p>
|
||||||
|
<h3>Parts</h3>
|
||||||
|
<ul>
|
||||||
|
{% for part in work.parts %}
|
||||||
|
<li><a href="{% url 'part_download' pk=part.pk filename=part.filename %}" target="part_{{ part.pk }}" rel="">{{ part.instrument }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Original Documents</h3>
|
||||||
|
<ul>
|
||||||
|
{% for doc in work.docs.all %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'document_download' pk=doc.pk %}" target="_blank">{{ doc.upload.name }}</a> [{{ doc.parts.count }} parts]
|
||||||
|
{% if request.is_admin %}
|
||||||
|
<a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-edit"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
25
library/templates/library/work_list.html
Normal file
25
library/templates/library/work_list.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends "interface/project_base.html" %}
|
||||||
|
|
||||||
|
{% block admin %}
|
||||||
|
<a href="{% url 'work_add' %}"><i class="fas fa-plus-circle"></i> Add new</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<h2>Works</h2>
|
||||||
|
<table style="max-width: 800px; margin: 10pt auto;">
|
||||||
|
<thead>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for work in object_list %}
|
||||||
|
{% with work.docs.count as doc_count %}
|
||||||
|
{% with work.parts.count as part_count %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url 'work_detail' pk=work.pk %}">{{ work.name }}</a></td>
|
||||||
|
<td>{{ doc_count }} file{{ doc_count|pluralize }} with {{ part_count }} part{{ part_count|pluralize }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
3
library/tests.py
Normal file
3
library/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
19
library/urls.py
Normal file
19
library/urls.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from django.contrib.auth import views as auth_views
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
|
||||||
|
path('projects/<int:project>/works', views.PlaylistView.as_view(), name="project_playlist"),
|
||||||
|
path('projects/<int:project>/works/manage', views.PlaylistManageView.as_view(), name="playlist_manage"),
|
||||||
|
path('projects/<int:project>/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/<int:pk>', views.WorkDetailView.as_view(), name="work_detail"),
|
||||||
|
path('library/works/<int:pk>/upload', views.DocumentAddView.as_view(), name="document_add"),
|
||||||
|
path('library/documents/<int:pk>/download', views.DocumentDownloadView.as_view(), name="document_download"),
|
||||||
|
path('library/documents/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
|
||||||
|
path('library/parts/<int:pk>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
|
||||||
|
]
|
||||||
217
library/views.py
Normal file
217
library/views.py
Normal file
@ -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')
|
||||||
Loading…
x
Reference in New Issue
Block a user