Got library working again
This commit is contained in:
parent
b85440d25c
commit
13f466228b
@ -6,27 +6,19 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<a class="" href="{% url 'project_detail' project=project.id %}">
|
<a class="" href="{% url 'project_detail' project=project.id %}">
|
||||||
<header class="card-header{% if not project.active %} has-background-light{% endif %}">
|
<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>
|
<p class="card-header-icon" style="color: black;">{{ project.rough_date }}</p>
|
||||||
</header>
|
</header>
|
||||||
</a>
|
</a>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="content">
|
<div class="content" style="height: 100px; overflow: hidden">
|
||||||
{{ project.description | markdown }}
|
{{ project.description | markdown }}
|
||||||
|
{% if not ensemble %}
|
||||||
|
<div class="has-text-centered"><i><small>With {{ project.ensemble }}</small></i></div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
||||||
from django.views.generic import RedirectView
|
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.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.core.exceptions import SuspiciousOperation
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -99,6 +99,8 @@ class AuthorizedResourceMixin(object):
|
|||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# TODO: RevokeResourceView - increment nonce
|
||||||
|
|
||||||
class ForgetResourceView(AuthorizedResourceMixin, RedirectView):
|
class ForgetResourceView(AuthorizedResourceMixin, RedirectView):
|
||||||
|
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
@ -239,6 +241,11 @@ class EnsembleDetailView(EnsembleMixin, DetailView):
|
|||||||
data['ensemble_link'] = self.request.path + "?auth=" + self.ensemble.auth()
|
data['ensemble_link'] = self.request.path + "?auth=" + self.ensemble.auth()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
class EnsembleRevokeView(SingleObjectMixin, RedirectView):
|
||||||
|
|
||||||
|
def get_redirect_url(self):
|
||||||
|
return
|
||||||
|
|
||||||
""" PROJECT VIEWS """
|
""" PROJECT VIEWS """
|
||||||
|
|
||||||
class ProjectListView(ProjectMixin, ListView):
|
class ProjectListView(ProjectMixin, ListView):
|
||||||
|
|||||||
@ -27,13 +27,14 @@ class MetaInline(admin.TabularInline):
|
|||||||
class WorkAdmin(admin.ModelAdmin):
|
class WorkAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'composer', 'edition', 'identifier', 'running_time']
|
list_display = ['name', 'composer', 'edition', 'identifier', 'running_time']
|
||||||
list_filter = ['collection']
|
list_filter = ['collection']
|
||||||
|
search_fields = ['name', 'composer']
|
||||||
inlines = [MetaInline, DocInline, ItemInline]
|
inlines = [MetaInline, DocInline, ItemInline]
|
||||||
|
|
||||||
admin.site.register(models.Work, WorkAdmin)
|
admin.site.register(models.Work, WorkAdmin)
|
||||||
|
|
||||||
class SectionInline(admin.TabularInline):
|
class SectionInline(admin.TabularInline):
|
||||||
model = models.Section
|
model = models.Section
|
||||||
fields = ['type', 'tag', 'ordinal', 'start', 'end']
|
fields = ['type', 'tag', 'ordinal', 'start', 'end', 'page']
|
||||||
|
|
||||||
class DocumentAdmin(admin.ModelAdmin):
|
class DocumentAdmin(admin.ModelAdmin):
|
||||||
list_display = ['work', '__str__']
|
list_display = ['work', '__str__']
|
||||||
@ -52,4 +53,9 @@ class EnsembleAccessAdmin(admin.ModelAdmin):
|
|||||||
list_display = ['ensemble', 'collection', 'access_type']
|
list_display = ['ensemble', 'collection', 'access_type']
|
||||||
list_filter = ['ensemble']
|
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 = """
|
ABBREVIATIONS = """
|
||||||
score Score
|
score Score
|
||||||
cb Double bass
|
cb Double bass
|
||||||
|
mall Mallet percussion
|
||||||
|
|
||||||
acc Accordion
|
acc Accordion
|
||||||
afl Alto flute
|
afl Alto flute
|
||||||
@ -183,7 +184,11 @@ class Instrument(namedtuple('Instrument', ('name', 'variant'), defaults=[None]))
|
|||||||
if variant:
|
if variant:
|
||||||
return cls(name, variant)
|
return cls(name, variant)
|
||||||
return cls(name, None)
|
return cls(name, None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tag(self):
|
||||||
|
l = self.name.lower()
|
||||||
|
return INSTRUMENT_TAGS.get(l, l)
|
||||||
|
|
||||||
def abbreviate(self):
|
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.core.files.storage import get_storage_class
|
||||||
from django.db.models import Q, Count, Min, Max
|
from django.db.models import Q, Count, Min, Max
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from byostorage.user import BYOStorage
|
from byostorage.user import BYOStorage
|
||||||
from byostorage.cached import CachedStorage
|
from byostorage.cached import CachedStorage
|
||||||
from .imslp import Instrument
|
from .imslp import Instrument
|
||||||
@ -26,10 +28,6 @@ logger = logging.getLogger(__name__)
|
|||||||
# FIXME: move back to settings
|
# FIXME: move back to settings
|
||||||
library_storage = CachedStorage(BYOStorage())
|
library_storage = CachedStorage(BYOStorage())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
class Orchestration(models.Model):
|
class Orchestration(models.Model):
|
||||||
"""
|
"""
|
||||||
Stores a list of instrument codes as a single entry (space delimited).
|
Stores a list of instrument codes as a single entry (space delimited).
|
||||||
@ -41,7 +39,21 @@ class Orchestration(models.Model):
|
|||||||
|
|
||||||
def as_list(self):
|
def as_list(self):
|
||||||
tags = [ t.strip() for t in self.instruments.split(' ') ]
|
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):
|
def save(self):
|
||||||
self.as_list()
|
self.as_list()
|
||||||
@ -49,7 +61,6 @@ class Orchestration(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
'''
|
|
||||||
|
|
||||||
class ProjectItem(models.Model):
|
class ProjectItem(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -160,7 +171,8 @@ class Work(models.Model):
|
|||||||
composer = models.CharField(max_length=255, default='Anon',
|
composer = models.CharField(max_length=255, default='Anon',
|
||||||
help_text="Surname, Initials")
|
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 details
|
||||||
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works")
|
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works")
|
||||||
@ -189,13 +201,17 @@ class Work(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def digital_parts(self):
|
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
|
@property
|
||||||
def physical_parts(self):
|
def physical_parts(self):
|
||||||
if not self.original_parts:
|
if not self.original_parts:
|
||||||
return []
|
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
|
@property
|
||||||
def tags(self):
|
def tags(self):
|
||||||
@ -240,15 +256,18 @@ class Work(models.Model):
|
|||||||
return self.code;
|
return self.code;
|
||||||
|
|
||||||
composer = self.composer or "Anon"
|
composer = self.composer or "Anon"
|
||||||
|
composer = re.sub('[^\w]', '', composer)
|
||||||
words = self.name.split()
|
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]
|
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):
|
def __str__(self):
|
||||||
return f"{self.name} ({self.composer})"
|
return f"{self.name} ({self.composer})"
|
||||||
@ -326,6 +345,16 @@ class Section(models.Model):
|
|||||||
TYPE_EXCERPT: 'warning',
|
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)
|
type = models.SmallIntegerField(choices=SECTION_TYPES)
|
||||||
doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="sections")
|
doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="sections")
|
||||||
@ -333,6 +362,7 @@ class Section(models.Model):
|
|||||||
ordinal = models.IntegerField(default=0)
|
ordinal = models.IntegerField(default=0)
|
||||||
start = models.SmallIntegerField(null=True, blank=True)
|
start = models.SmallIntegerField(null=True, blank=True)
|
||||||
end = 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:
|
class Meta:
|
||||||
ordering = ['type', 'ordinal', 'doc', 'start', 'pk']
|
ordering = ['type', 'ordinal', 'doc', 'start', 'pk']
|
||||||
@ -346,10 +376,6 @@ class Section(models.Model):
|
|||||||
return str(instr)
|
return str(instr)
|
||||||
return f"{self.ordinal} - {self.tag}"
|
return f"{self.ordinal} - {self.tag}"
|
||||||
|
|
||||||
#@property
|
|
||||||
#def instrument(self):
|
|
||||||
# return Instrument.from_tag(self.tag)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bulma_class(self):
|
def bulma_class(self):
|
||||||
return self.SECTION_CLASSES[self.type]
|
return self.SECTION_CLASSES[self.type]
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<span class="icon"><i class="fas fa-save"></i></span>
|
<span class="icon"><i class="fas fa-save"></i></span>
|
||||||
<span>Save</span>
|
<span>Save</span>
|
||||||
</a>
|
</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>
|
<span>Cancel</span>
|
||||||
</a>
|
</a>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
.grid-page {
|
.grid-page {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 5px;
|
||||||
border: 1px solid #999;
|
border: 1px solid #999;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -44,46 +44,45 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: start;
|
||||||
}
|
}
|
||||||
#tag-area {
|
#tag-area {
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
|
.instrument {
|
||||||
|
display: list-item !important;
|
||||||
|
}
|
||||||
|
.instrument:hover {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% 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 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="column is-narrow">
|
||||||
<div class="has-text-centered">
|
<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;">
|
<div class="box" style="display: inline-block;">
|
||||||
<canvas id="inline-viewer" style="width: 500px;"></canvas>
|
<canvas id="inline-viewer" style="width: 500px;"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@ -97,9 +96,39 @@
|
|||||||
<div class="grid-column" id="tag-area">
|
<div class="grid-column" id="tag-area">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="button is-primary" style="width: 100%" onclick="expandEntries()">Expand</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<p>{{ document.upload.name }}</p>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -194,10 +223,9 @@
|
|||||||
if (pageNum <= 1) {
|
if (pageNum <= 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pageNum--;
|
queueRenderPage(pageNum-1);
|
||||||
queueRenderPage(pageNum);
|
|
||||||
}
|
}
|
||||||
//document.getElementById('prev').addEventListener('click', onPrevPage);
|
document.getElementById('prev').addEventListener('click', onPrevPage);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays next page.
|
* Displays next page.
|
||||||
@ -206,10 +234,9 @@
|
|||||||
if (pageNum >= pdfDoc.numPages) {
|
if (pageNum >= pdfDoc.numPages) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pageNum++;
|
queueRenderPage(pageNum+1);
|
||||||
queueRenderPage(pageNum);
|
|
||||||
}
|
}
|
||||||
//document.getElementById('next').addEventListener('click', onNextPage);
|
document.getElementById('next').addEventListener('click', onNextPage);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously downloads PDF.
|
* Asynchronously downloads PDF.
|
||||||
@ -243,6 +270,12 @@
|
|||||||
dirty = false;
|
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() {
|
function addInstrument() {
|
||||||
let name = document.getElementById('add-instrument-name');
|
let name = document.getElementById('add-instrument-name');
|
||||||
let variant = document.getElementById('add-instrument-variant');
|
let variant = document.getElementById('add-instrument-variant');
|
||||||
@ -271,6 +304,11 @@
|
|||||||
addTag(tag, pageNum, pageNum);
|
addTag(tag, pageNum, pageNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assignInstrument(tag, el) {
|
||||||
|
addTag(tag, pageNum, pageNum);
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
|
||||||
function addTag(tag, start, end) {
|
function addTag(tag, start, end) {
|
||||||
console.log("addTag", tag, start, end);
|
console.log("addTag", tag, start, end);
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
@ -295,7 +333,15 @@
|
|||||||
let del = document.createElement('span');
|
let del = document.createElement('span');
|
||||||
del.className = "icon is-action";
|
del.className = "icon is-action";
|
||||||
del.innerHTML = '<i class="fas fa-trash-alt" title="Remove this tag"></i>';
|
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)
|
label.appendChild(del)
|
||||||
|
|
||||||
el.appendChild(label);
|
el.appendChild(label);
|
||||||
@ -315,8 +361,8 @@
|
|||||||
let start = tag.dataset.start;
|
let start = tag.dataset.start;
|
||||||
let end = tag.dataset.end;
|
let end = tag.dataset.end;
|
||||||
let span = end-start+1;
|
let span = end-start+1;
|
||||||
let height = span * 25 + (span-1) * 10;
|
let height = span * 25 + (span-1) * 5;
|
||||||
let top = (start-1) * 35;
|
let top = (start-1) * 30;
|
||||||
|
|
||||||
tag.style.height = height + 'px';
|
tag.style.height = height + 'px';
|
||||||
tag.style.marginTop = top + 'px';
|
tag.style.marginTop = top + 'px';
|
||||||
@ -341,6 +387,19 @@
|
|||||||
updateTag(el);
|
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() {
|
function saveTags() {
|
||||||
const pageTags = [];
|
const pageTags = [];
|
||||||
for (let pageTag of tagArea.children ) {
|
for (let pageTag of tagArea.children ) {
|
||||||
@ -361,7 +420,7 @@
|
|||||||
}
|
}
|
||||||
).then((response) => {
|
).then((response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.location = "{% url 'work_detail' document.work.pk %}"
|
window.location = "{% url 'work_detail' collection.pk document.work_id %}"
|
||||||
} else {
|
} else {
|
||||||
alert("Failed: " + response.statusText)
|
alert("Failed: " + response.statusText)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
<button class="button is-link">Yes</button>
|
<button class="button is-link">Yes</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -4,14 +4,14 @@
|
|||||||
{{ doc.upload.name|basename }}</a></td>
|
{{ doc.upload.name|basename }}</a></td>
|
||||||
<td>
|
<td>
|
||||||
{% for section in doc.sections.all %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="has-text-right" style="white-space: nowrap;">
|
<td class="has-text-right" style="white-space: nowrap;">
|
||||||
{% if request.is_admin %}
|
{% 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>
|
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 %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -7,11 +7,11 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block admin %}
|
{% 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 class="icon"><i class="fas fa-edit"></i></span>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</a>
|
</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 class="icon"><i class="fas fa-plus-circle"></i></span>
|
||||||
<span>Add to project</span>
|
<span>Add to project</span>
|
||||||
</a>
|
</a>
|
||||||
@ -29,16 +29,27 @@
|
|||||||
<p class="block">{{ work.notes }}</p>
|
<p class="block">{{ work.notes }}</p>
|
||||||
|
|
||||||
<p class="block">
|
<p class="block">
|
||||||
Location: <a href="{% url 'collection_work_list' work.collection.pk %}">{{ work.collection }}</a> [{{ work.identifier }}]<br/>
|
<table class="table">
|
||||||
Running time: {% firstof work.duration 'Unknown' %}<br/>
|
<tr>
|
||||||
Licence: {{ work.get_licence_display }}<br/>
|
<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 %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</p>
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
{% if work.parent %}
|
{% 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>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -62,7 +73,7 @@
|
|||||||
</h4>
|
</h4>
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
{% for inst, c in work.physical_parts %}
|
{% 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 %}
|
{% empty %}
|
||||||
<p class="is-italic">No printed parts listed</p>
|
<p class="is-italic">No printed parts listed</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -77,10 +88,10 @@
|
|||||||
</h4>
|
</h4>
|
||||||
<div class="tags">
|
<div class="tags">
|
||||||
{% if work.digital_parts %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% for section in work.digital_parts %}
|
{% 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>
|
target="section_{{ section.pk }}" rel="">{{ section.name }}</a>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p class="is-italic">No digital parts available</p>
|
<p class="is-italic">No digital parts available</p>
|
||||||
@ -118,7 +129,7 @@
|
|||||||
{% if request.is_admin %}
|
{% if request.is_admin %}
|
||||||
<div class="column is-one-quarter">
|
<div class="column is-one-quarter">
|
||||||
<h4 class="is-size-5">Upload files</h4>
|
<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 %}
|
{% csrf_token %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -135,7 +146,7 @@
|
|||||||
Loans
|
Loans
|
||||||
</h4>
|
</h4>
|
||||||
<span class="level-right">
|
<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>
|
class="fas fa-plus-circle"></i></span> Checkout</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>/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>/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>/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/', include(router.urls))
|
||||||
path('api/library/collections/<int:pk>/export', api.CollectionExportView.as_view(), name="collection_export"),
|
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/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>/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.detail import DetailView, SingleObjectMixin, View
|
||||||
from django.views.generic.list import ListView, MultipleObjectMixin
|
from django.views.generic.list import ListView, MultipleObjectMixin
|
||||||
from django.views.generic.edit import CreateView, FormView, UpdateView, DeleteView
|
from django.views.generic.edit import CreateView, FormView, UpdateView, DeleteView
|
||||||
from django.http import FileResponse, HttpResponse, JsonResponse
|
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Q, Count, Sum
|
from django.db.models import Q, Count, Sum
|
||||||
|
from django.db import transaction
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
@ -119,40 +120,41 @@ class ProjectItemAddView(ProjectMixin, UpdateView):
|
|||||||
|
|
||||||
class CollectionMixin(AuthorizedResourceMixin):
|
class CollectionMixin(AuthorizedResourceMixin):
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
super().is_authorized()
|
collection_id = self.kwargs['collection']
|
||||||
try:
|
self.collection = get_object_or_404(models.Collection, pk=collection_id)
|
||||||
self.collection = self.get_collection()
|
|
||||||
|
if super().is_authorized():
|
||||||
return True
|
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):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(collection=self.collection)
|
return super().get_queryset().filter(collection=self.collection)
|
||||||
|
|
||||||
def get_collection(self):
|
|
||||||
collection = self.get_collection_queryset().get(pk=self.kwargs['collection'])
|
class CollectionListView(ListView):
|
||||||
self.request.is_admin = collection.has_administrator(self.request.user)
|
paginate_by = 20
|
||||||
return collection
|
|
||||||
|
|
||||||
def get_collection_queryset(self):
|
def get_queryset(self):
|
||||||
collections = models.Collection.objects.all()
|
collections = models.Collection.objects.all()
|
||||||
|
|
||||||
if self.request.user.is_anonymous:
|
if self.request.user.is_anonymous:
|
||||||
return models.Collection.objects.none()
|
return models.Collection.objects.none()
|
||||||
|
|
||||||
if self.request.is_admin:
|
if self.request.user.is_staff:
|
||||||
return collections
|
return collections
|
||||||
|
|
||||||
return collections.filter(Q(administrators=self.request.user) | Q(allowed_ensembles__ensemble__admins=self.request.user))
|
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):
|
class WorkListView(CollectionMixin, ListView):
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
@ -178,7 +180,7 @@ class WorkListView(CollectionMixin, ListView):
|
|||||||
else:
|
else:
|
||||||
works = works.filter(Q(name__contains=q) | Q(composer__contains=q) | Q(meta_info__value__contains=q))
|
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):
|
class CollectionWorkListView(WorkListView):
|
||||||
|
|
||||||
@ -237,7 +239,7 @@ class WorkUpdateView(CollectionMixin, WorkMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WorkAddToProject(EnsembleMixin, FormView):
|
class WorkAddToProject(ProjectMixin, FormView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
form_class = forms.ProjectSelectForm
|
form_class = forms.ProjectSelectForm
|
||||||
template_name = "interface/default_form.html"
|
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)
|
works = works.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id)
|
||||||
return works
|
return works
|
||||||
|
|
||||||
class WorkAddDocumentView(EnsembleMixin, CreateView):
|
class WorkAddDocumentView(CollectionMixin, CreateView):
|
||||||
template_name = "interface/default_form.html"
|
template_name = "interface/default_form.html"
|
||||||
model = Document
|
model = Document
|
||||||
fields = ['upload']
|
fields = ['upload']
|
||||||
@ -333,22 +335,28 @@ class WorkAddDocumentView(EnsembleMixin, CreateView):
|
|||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
"message": "created",
|
"message": "created",
|
||||||
"id": doc.pk,
|
"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)
|
}, 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):
|
def get_queryset(self):
|
||||||
if self.request.is_admin:
|
return models.Document.objects.filter(work__collection=self.collection)
|
||||||
return Document.objects.select_related('work')
|
|
||||||
return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work')
|
|
||||||
|
|
||||||
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
|
pass
|
||||||
|
|
||||||
class DocumentDownloadView(EnsembleMixin, DocumentMixin, SingleObjectMixin, View):
|
class DocumentDownloadView(DocumentMixin, SingleObjectMixin, View):
|
||||||
|
|
||||||
def get(self, request, **args):
|
def get(self, request, **args):
|
||||||
self.request = request
|
self.request = request
|
||||||
@ -359,7 +367,7 @@ class DocumentDownloadView(EnsembleMixin, DocumentMixin, SingleObjectMixin, View
|
|||||||
#return response
|
#return response
|
||||||
return redirect(self.object.upload.url)
|
return redirect(self.object.upload.url)
|
||||||
|
|
||||||
class DocumentAnnotateView(EnsembleMixin, DocumentMixin, DetailView):
|
class DocumentAnnotateView(DocumentMixin, DetailView):
|
||||||
template_name = 'library/document_annotate.html'
|
template_name = 'library/document_annotate.html'
|
||||||
|
|
||||||
def post(self, request, **args):
|
def post(self, request, **args):
|
||||||
@ -369,11 +377,12 @@ class DocumentAnnotateView(EnsembleMixin, DocumentMixin, DetailView):
|
|||||||
|
|
||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
|
|
||||||
self.object.sections.all().delete()
|
with transaction.atomic():
|
||||||
for tag, start, end in data:
|
self.object.sections.all().delete()
|
||||||
#pages.sort()
|
for tag, start, end in data:
|
||||||
#end = pages[-1] if len(pages) > 1 else None
|
#pages.sort()
|
||||||
o = self.object.sections.create(tag=tag, start=start, end=end)
|
#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)
|
return HttpResponse(status=204)
|
||||||
|
|
||||||
@ -387,15 +396,17 @@ class DocumentAnnotateView(EnsembleMixin, DocumentMixin, DetailView):
|
|||||||
data['json_data'] = {'pageTags': pages, 'instruments': dict(INSTRUMENTS)}
|
data['json_data'] = {'pageTags': pages, 'instruments': dict(INSTRUMENTS)}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class DocumentDeleteView(EnsembleMixin, DocumentMixin, DeleteView):
|
class DocumentDeleteView(DocumentMixin, DeleteView):
|
||||||
|
|
||||||
#def get_template_names(self):
|
#def get_template_names(self):
|
||||||
# return ["interface/default_form.html"]
|
# return ["interface/default_form.html"]
|
||||||
|
|
||||||
def get_success_url(self):
|
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):
|
def get(self, request, **args):
|
||||||
self.request = request
|
self.request = request
|
||||||
|
|||||||
@ -37,7 +37,7 @@ from interface.views import EnsembleMixin
|
|||||||
|
|
||||||
from rest_framework import routers, serializers, viewsets
|
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
|
import requests
|
||||||
@ -48,6 +48,17 @@ import shutil
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.core.files.uploadedfile import TemporaryUploadedFile
|
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 SectionSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -111,6 +122,7 @@ class DocumentSerializer(serializers.ModelSerializer):
|
|||||||
class WorkSerializer(serializers.ModelSerializer):
|
class WorkSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
docs = DocumentSerializer(many=True)
|
docs = DocumentSerializer(many=True)
|
||||||
|
meta_info = WorkMetaSerializer(many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Work
|
model = Work
|
||||||
@ -119,6 +131,7 @@ class WorkSerializer(serializers.ModelSerializer):
|
|||||||
def create(self, validated):
|
def create(self, validated):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
docs = validated.pop('docs', [])
|
docs = validated.pop('docs', [])
|
||||||
|
meta = validated.pop('meta_info', [])
|
||||||
work = Work.objects.create(**validated)
|
work = Work.objects.create(**validated)
|
||||||
|
|
||||||
for d in docs:
|
for d in docs:
|
||||||
@ -134,11 +147,25 @@ class WorkSerializer(serializers.ModelSerializer):
|
|||||||
for s in sections:
|
for s in sections:
|
||||||
Section.objects.create(doc_id=doc.pk, **s)
|
Section.objects.create(doc_id=doc.pk, **s)
|
||||||
|
|
||||||
|
for m in meta:
|
||||||
|
WorkMeta.objects.create(work_id=work.pk, **m)
|
||||||
|
|
||||||
return work
|
return work
|
||||||
|
|
||||||
class CollectionSerializer(serializers.Serializer):
|
class CollectionSerializer(serializers.Serializer):
|
||||||
works = WorkSerializer(many=True)
|
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
|
from rest_framework import generics
|
||||||
|
|
||||||
class CollectionExportView(generics.RetrieveAPIView):
|
class CollectionExportView(generics.RetrieveAPIView):
|
||||||
@ -159,3 +186,8 @@ class WorkImportView(generics.CreateAPIView):
|
|||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(collection_id=self.kwargs['pk'])
|
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