Got library working again

This commit is contained in:
Tris Forster 2023-02-16 17:22:18 +11:00
parent b85440d25c
commit 13f466228b
17 changed files with 427 additions and 140 deletions

View File

@ -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>

View File

@ -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):

View File

@ -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)

View 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"
}
}
]

View File

@ -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):
"""

View 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'),
),
]

View 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),
]

View 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'),
),
]

View 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),
),
]

View File

@ -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]

View File

@ -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">&lt;</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">&gt;</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)
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"),
]

View File

@ -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

View File

@ -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'])