Got library working again
This commit is contained in:
parent
b85440d25c
commit
13f466228b
@ -6,27 +6,19 @@
|
||||
<div class="card">
|
||||
<a class="" href="{% url 'project_detail' project=project.id %}">
|
||||
<header class="card-header{% if not project.active %} has-background-light{% endif %}">
|
||||
<p class="card-header-title">{% if not ensemble %}{{ project.ensemble }}{% endif%} {{ project.name }}</p>
|
||||
<p class="card-header-title">
|
||||
{{ project.name }}
|
||||
</p>
|
||||
<p class="card-header-icon" style="color: black;">{{ project.rough_date }}</p>
|
||||
</header>
|
||||
</a>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
{{ project.description | markdown }}
|
||||
<div class="content" style="height: 100px; overflow: hidden">
|
||||
{{ project.description | markdown }}
|
||||
{% if not ensemble %}
|
||||
<div class="has-text-centered"><i><small>With {{ project.ensemble }}</small></i></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p><small>
|
||||
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
|
||||
{% if project.submissions.count %}{{ project.submissions.count }} submissions<br/>{% endif %}
|
||||
</small></p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{% if 'library' in project.active_modules %}
|
||||
{% with project.works.count as c %}
|
||||
<a class="card-footer-item" href="{% url 'item_list' project=project.pk %}">
|
||||
{{ c }} work{{ c | pluralize }}
|
||||
</a>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
admin.site.register(models.EnsembleAccess, EnsembleAccessAdmin)
|
||||
|
||||
class OrchestrationAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'instruments']
|
||||
|
||||
admin.site.register(models.Orchestration, OrchestrationAdmin)
|
||||
38
app/library/fixtures/default_orchestrations.json
Normal file
38
app/library/fixtures/default_orchestrations.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
28
app/library/migrations/0006_auto_20230202_0804.py
Normal file
28
app/library/migrations/0006_auto_20230202_0804.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
||||
33
app/library/migrations/0007_orchestration.py
Normal file
33
app/library/migrations/0007_orchestration.py
Normal file
@ -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),
|
||||
]
|
||||
19
app/library/migrations/0010_work_orchestration.py
Normal file
19
app/library/migrations/0010_work_orchestration.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
||||
18
app/library/migrations/0011_section_page.py
Normal file
18
app/library/migrations/0011_section_page.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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]
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<span class="icon"><i class="fas fa-save"></i></span>
|
||||
<span>Save</span>
|
||||
</a>
|
||||
<a href="{% url 'work_detail' pk=object.work.pk %}" class="button is-link is-light">
|
||||
<a href="{% url 'work_detail' collection=collection.pk pk=object.work_id %}" class="button is-link is-light">
|
||||
<span>Cancel</span>
|
||||
</a>
|
||||
{% 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;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<h3 class="subtitle"><a href="{% url 'work_detail' document.work.pk %}">{{ document.work.name }}</a></h3>
|
||||
<h3 class="subtitle"><a href="{% url 'work_detail' collection.pk document.work_id %}">{{ document.work.name }}</a></h3>
|
||||
<div id="annotation-area" class="columns is-centered">
|
||||
<div class="column is-narrow">
|
||||
<div class="columns">
|
||||
<div class="column is-narrow">
|
||||
<button class="button is-small is-primary" id="prev"><</button>
|
||||
</div>
|
||||
<div class="column">
|
||||
<span id="page-num">-</span> / <span id="page-count">-</span>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<button class="button is-small is-primary" id="next">></button>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="unassigned-area">
|
||||
{% for tag, inst in document.work.unassigned_instruments %}
|
||||
<li class="is-clickable" onclick="assignInstrument('{{tag}}', this)")>{{ inst }}</li>
|
||||
{% endfor %}
|
||||
<li><a onclick="document.getElementById('add-modal').classList.add('is-active')">Add instrument</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="has-text-centered">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<p>Page <span id="page-num">-</span> / <span id="page-count">-</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<div class="field has-addons">
|
||||
<span class="control">
|
||||
<input type="text" class="input" list="instrument-list" id="add-instrument-name"/>
|
||||
<datalist id="instrument-list">
|
||||
{% for inst in json_data.instruments.values %}
|
||||
<option value="{{inst}}"/>
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</span>
|
||||
<span class="control">
|
||||
<input type="number" class="input" max="4" min="1" size="3" id="add-instrument-variant"/>
|
||||
</span>
|
||||
<span class="control">
|
||||
<button class="button is-primary" onclick="addInstrument()">Add</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" style="display: inline-block;">
|
||||
<canvas id="inline-viewer" style="width: 500px;"></canvas>
|
||||
</div>
|
||||
@ -97,9 +96,39 @@
|
||||
<div class="grid-column" id="tag-area">
|
||||
</div>
|
||||
</div>
|
||||
<button class="button is-primary" style="width: 100%" onclick="expandEntries()">Expand</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="add-modal">
|
||||
<div class="modal-background" onclick="closeAddModal()"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Add Instrument</p>
|
||||
<button class="delete" aria-label="close" onclick="closeAddModal()"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<div class="field has-addons">
|
||||
<span class="control">
|
||||
<input type="text" class="input" list="instrument-list" id="add-instrument-name"/>
|
||||
<datalist id="instrument-list">
|
||||
{% for inst in json_data.instruments.values %}
|
||||
<option value="{{inst}}"/>
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</span>
|
||||
<span class="control">
|
||||
<input type="number" class="input" max="4" min="1" size="3" id="add-instrument-variant"/>
|
||||
</span>
|
||||
<span class="control">
|
||||
<button class="button is-primary" onclick="addInstrument()">Add</button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>{{ document.upload.name }}</p>
|
||||
|
||||
{% 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 = '<i class="fas fa-trash-alt" title="Remove this tag"></i>';
|
||||
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<c-1; i++) {
|
||||
entries[i].dataset.end = entries[i+1].dataset.start - 1;
|
||||
updateTag(entries[i]);
|
||||
|
||||
}
|
||||
entries[c-1].dataset.end = pdfDoc.numPages;
|
||||
updateTag(entries[c-1]);
|
||||
}
|
||||
|
||||
function saveTags() {
|
||||
const pageTags = [];
|
||||
for (let pageTag of tagArea.children ) {
|
||||
@ -361,7 +420,7 @@
|
||||
}
|
||||
).then((response) => {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
<button class="button is-link">Yes</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-link is-light" href="{% url 'work_detail' object.work.pk %}">No</a>
|
||||
<a class="button is-link is-light" href="{% url 'work_detail' collection.pk object.work.pk %}">No</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -4,14 +4,14 @@
|
||||
{{ doc.upload.name|basename }}</a></td>
|
||||
<td>
|
||||
{% for section in doc.sections.all %}
|
||||
<a class="tag is-{{ section.bulma_class }}" target="_blank" href="{% url 'part_download' pk=section.pk filename=section.filename %}">{{ section.name }}</a>
|
||||
<a class="tag is-{{ section.bulma_class }}" target="_blank" href="{% url 'part_download' collection.pk section.pk section.filename %}">{{ section.name }}</a>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="has-text-right" style="white-space: nowrap;">
|
||||
{% if request.is_admin %}
|
||||
<a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-tags"
|
||||
<a href="{% url 'document_annotate' collection.pk doc.pk %}"><i class="fas fa-tags"
|
||||
title="Manage Tags"></i></a>
|
||||
<a href="{% url 'document_delete' pk=doc.pk %}"><i class="fas fa-trash-alt" title="Delete Document"></i></a>
|
||||
<a href="{% url 'document_delete' collection.pk doc.pk %}"><i class="fas fa-trash-alt" title="Delete Document"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -7,11 +7,11 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block admin %}
|
||||
<a href="{% url 'work_edit' work.pk %}" class="button is-link">
|
||||
<a href="{% url 'work_edit' collection.pk work.pk %}" class="button is-link">
|
||||
<span class="icon"><i class="fas fa-edit"></i></span>
|
||||
<span>Edit</span>
|
||||
</a>
|
||||
<a href="{% url 'work_add_to_project' work.pk %}" class="button is-link">
|
||||
<a href="{% url 'work_add_to_project' collection.pk work.pk %}" class="button is-link">
|
||||
<span class="icon"><i class="fas fa-plus-circle"></i></span>
|
||||
<span>Add to project</span>
|
||||
</a>
|
||||
@ -29,16 +29,27 @@
|
||||
<p class="block">{{ work.notes }}</p>
|
||||
|
||||
<p class="block">
|
||||
Location: <a href="{% url 'collection_work_list' work.collection.pk %}">{{ work.collection }}</a> [{{ work.identifier }}]<br/>
|
||||
Running time: {% firstof work.duration 'Unknown' %}<br/>
|
||||
Licence: {{ work.get_licence_display }}<br/>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Location:</th><td><a href="{% url 'collection_work_list' work.collection.pk %}">{{ work.collection }}</a> [{{ work.identifier }}]</td>
|
||||
<th>Orchestration:</th><td>{{ work.orchestration }}</td>
|
||||
</tr><tr>
|
||||
<th>Running time:</th><td>{% firstof work.duration 'Unknown' %}</td>
|
||||
<th>Licence:</th><td>{{ work.get_licence_display }}</td>
|
||||
</tr><tr>
|
||||
<td colspan="4">
|
||||
{% for meta in work.meta %}
|
||||
{{ meta.get_name_display }}: <a href="{% url 'collection_work_list' work.collection.pk %}?filter={{ meta.name}}:{{ meta.value }}">{{ meta.value }}</a><br/>
|
||||
<a href="{% url 'collection_work_list' work.collection.pk %}?filter={{ meta.name}}:{{ meta.value }}" class="tag" >
|
||||
{{ meta.get_name_display }}:
|
||||
{{ meta.value }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if work.parent %}
|
||||
<p>From <a href="{% url 'work_detail' work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a>
|
||||
<p>From <a href="{% url 'work_detail' collection.pk work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
@ -62,7 +73,7 @@
|
||||
</h4>
|
||||
<div class="tags">
|
||||
{% for inst, c in work.physical_parts %}
|
||||
<span class="tag is-warning">{{ inst }} ({{ c }})</span>
|
||||
<span class="tag is-warning">{{ inst }}{% if c > 1 %} [<strong>{{ c }}</strong>]{% endif %}</span>
|
||||
{% empty %}
|
||||
<p class="is-italic">No printed parts listed</p>
|
||||
{% endfor %}
|
||||
@ -77,10 +88,10 @@
|
||||
</h4>
|
||||
<div class="tags">
|
||||
{% if work.digital_parts %}
|
||||
<a class="tag is-danger" href="{% url 'work_partset' pk=work.pk %}">Full Set</a>
|
||||
<a class="tag is-danger" href="{% url 'work_partset' collection.pk work.pk %}">Full Set</a>
|
||||
{% endif %}
|
||||
{% for section in work.digital_parts %}
|
||||
<a class="tag is-info" href="{% url 'part_download' pk=section.pk filename=section.filename %}"
|
||||
<a class="tag is-info" href="{% url 'part_download' collection.pk section.pk section.filename %}"
|
||||
target="section_{{ section.pk }}" rel="">{{ section.name }}</a>
|
||||
{% empty %}
|
||||
<p class="is-italic">No digital parts available</p>
|
||||
@ -118,7 +129,7 @@
|
||||
{% if request.is_admin %}
|
||||
<div class="column is-one-quarter">
|
||||
<h4 class="is-size-5">Upload files</h4>
|
||||
<form action="{% url 'document_add' object.pk %}" class="dropzone" id="doc-upload">
|
||||
<form action="{% url 'document_add' collection.pk object.pk %}" class="dropzone" id="doc-upload">
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
</div>
|
||||
@ -135,7 +146,7 @@
|
||||
Loans
|
||||
</h4>
|
||||
<span class="level-right">
|
||||
<a class="icon-text" href="{% url 'work_add_to_project' work.pk %}"><span class="icon"><i
|
||||
<a class="icon-text" href="{% url 'work_add_to_project' collection.pk work.pk %}"><span class="icon"><i
|
||||
class="fas fa-plus-circle"></i></span> Checkout</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -31,10 +31,12 @@ urlpatterns = [
|
||||
path('collections/<int:collection>/docs/<int:pk>/delete', views.DocumentDeleteView.as_view(), name="document_delete"),
|
||||
path('collections/<int:collection>/docs/<int:pk>/download', views.DocumentDownloadView.as_view(), name="document_download"),
|
||||
path('collections/<int:collection>/docs/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
|
||||
path('collections/<int:collection>/docs/<int:pk>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
|
||||
|
||||
path('collections/<int:collection>/download/<int:section>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
|
||||
|
||||
#path('api/', include(router.urls))
|
||||
path('api/library/collections/<int:pk>/export', api.CollectionExportView.as_view(), name="collection_export"),
|
||||
path('api/library/works/<int:pk>/export', api.WorkExportView.as_view(), name="work_export"),
|
||||
path('api/library/collections/<int:pk>/import', api.WorkImportView.as_view(), name="work_import"),
|
||||
path('api/library/collections/<int:pk>/bulk_import', api.CollectionImportView.as_view(), name="collection_import"),
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'])
|
||||
Loading…
x
Reference in New Issue
Block a user