Tweeking section handling
This commit is contained in:
parent
7e47eec4ae
commit
8a249de51c
@ -6,15 +6,16 @@ WORKDIR /root
|
|||||||
RUN python3 -m ensurepip
|
RUN python3 -m ensurepip
|
||||||
RUN pip3 install -U pip --no-cache-dir
|
RUN pip3 install -U pip --no-cache-dir
|
||||||
|
|
||||||
|
COPY app/requirements.txt .
|
||||||
|
RUN pip3 install -r requirements.txt --no-cache-dir
|
||||||
|
|
||||||
COPY app /opt/polyphonic
|
COPY app /opt/polyphonic
|
||||||
WORKDIR /opt/polyphonic
|
WORKDIR /opt/polyphonic
|
||||||
|
|
||||||
COPY docker_settings.py polyphonic/local_settings.py
|
COPY docker_settings.py polyphonic/local_settings.py
|
||||||
|
|
||||||
RUN pip3 install -r requirements.txt --no-cache-dir
|
|
||||||
|
|
||||||
RUN mkdir /var/polyphonic
|
RUN mkdir /var/polyphonic
|
||||||
RUN SECRET_KEY=_ python3 manage.py collectstatic --noinput
|
RUN SECRET_KEY=_ python3 manage.py collectstatic --noinput
|
||||||
|
|
||||||
ENTRYPOINT ["python3", "manage.py"]
|
ENTRYPOINT ["python3", "manage.py"]
|
||||||
CMD ["runserver", "0.0.0.0:8000", "--insecure"]
|
CMD ["runserver", "0.0.0.0:8000", "--insecure"]
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
<p class="menu-label">Admin</p>
|
<p class="menu-label">Admin</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
{% if request.is_admin %}
|
{% if request.is_admin or request.user.is_superuser %}
|
||||||
<li><a class="admin-link" href="{% url 'collection_list' %}">Collections</a></li>
|
<li><a class="admin-link" href="{% url 'collection_list' %}">Collections</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.user.is_superuser %}
|
{% if request.user.is_superuser %}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ admin.site.register(models.Work, WorkAdmin)
|
|||||||
|
|
||||||
class SectionInline(admin.TabularInline):
|
class SectionInline(admin.TabularInline):
|
||||||
model = models.Section
|
model = models.Section
|
||||||
fields = ['tag', 'start', 'end']
|
fields = ['type', 'tag', 'ordinal', 'start', 'end']
|
||||||
|
|
||||||
class DocumentAdmin(admin.ModelAdmin):
|
class DocumentAdmin(admin.ModelAdmin):
|
||||||
list_display = ['work', '__str__']
|
list_display = ['work', '__str__']
|
||||||
|
|||||||
@ -2,9 +2,12 @@
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
# taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_Instruments
|
# taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_Instruments
|
||||||
|
# Place any extra abbreviations at the top
|
||||||
|
|
||||||
ABBREVIATIONS = """
|
ABBREVIATIONS = """
|
||||||
score Score
|
score Score
|
||||||
|
cb Double bass
|
||||||
|
|
||||||
acc Accordion
|
acc Accordion
|
||||||
afl Alto flute
|
afl Alto flute
|
||||||
alt Alto (voice) (contralto)
|
alt Alto (voice) (contralto)
|
||||||
|
|||||||
40
app/library/migrations/0004_auto_20230101_1535.py
Normal file
40
app/library/migrations/0004_auto_20230101_1535.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2023-01-01 04:35
|
||||||
|
|
||||||
|
import byostorage.cached
|
||||||
|
import byostorage.user
|
||||||
|
from django.db import migrations, models
|
||||||
|
import library.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('library', '0003_auto_20221201_1540'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='section',
|
||||||
|
options={'ordering': ['doc', 'type', 'start', 'pk']},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='section',
|
||||||
|
name='type',
|
||||||
|
field=models.SmallIntegerField(choices=[(1, 'Instrument'), (2, 'Movement')], default=1),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='upload',
|
||||||
|
field=models.FileField(storage=byostorage.cached.CachedStorage(byostorage.user.BYOStorage()), upload_to=library.models.doc_upload_filename),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='work',
|
||||||
|
name='original_parts',
|
||||||
|
field=models.JSONField(default=dict, help_text='Original printed parts (IMSLP format)'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='work',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
]
|
||||||
22
app/library/migrations/0005_auto_20230101_1547.py
Normal file
22
app/library/migrations/0005_auto_20230101_1547.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2023-01-01 04:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('library', '0004_auto_20230101_1535'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='section',
|
||||||
|
options={'ordering': ['type', 'ordinal', 'doc', 'start', 'pk']},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='section',
|
||||||
|
name='ordinal',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -7,9 +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 os.path
|
|
||||||
|
|
||||||
from byostorage.user import BYOStorage
|
from byostorage.user import BYOStorage
|
||||||
|
from byostorage.cached import CachedStorage
|
||||||
from .imslp import Instrument
|
from .imslp import Instrument
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -24,26 +23,10 @@ logger = logging.getLogger(__name__)
|
|||||||
# library_storage = get_storage_class()()
|
# library_storage = get_storage_class()()
|
||||||
#logger.info("Library storage: %s", library_storage.__class__.__name__)
|
#logger.info("Library storage: %s", library_storage.__class__.__name__)
|
||||||
|
|
||||||
|
# FIXME: move back to settings
|
||||||
|
library_storage = CachedStorage(BYOStorage())
|
||||||
|
|
||||||
|
|
||||||
DOCTYPES = [
|
|
||||||
(1, 'PDF'),
|
|
||||||
(2, 'Audio'),
|
|
||||||
(3, 'Video'),
|
|
||||||
(4, 'Source'),
|
|
||||||
]
|
|
||||||
|
|
||||||
LICENCE_TYPES = [
|
|
||||||
(2, 'Public Domain'),
|
|
||||||
(4, 'Copyright Expired'),
|
|
||||||
(6, 'Copyrighted'),
|
|
||||||
(10, 'Internal use only'),
|
|
||||||
]
|
|
||||||
|
|
||||||
ACCESS_TYPES = [
|
|
||||||
(1, 'Unlimited'),
|
|
||||||
(2, 'Approval required'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
@ -105,7 +88,20 @@ class Collection(models.Model):
|
|||||||
help_text="User storage for documents")
|
help_text="User storage for documents")
|
||||||
notes = models.TextField(blank=True,
|
notes = models.TextField(blank=True,
|
||||||
help_text="Publicly visible notes about collection and loans policy (markdown format)")
|
help_text="Publicly visible notes about collection and loans policy (markdown format)")
|
||||||
|
|
||||||
|
def meta(self, name):
|
||||||
|
items = WorkMeta.objects.filter(work__collection=self.pk, name=name).values_list('value', flat=True).distinct()
|
||||||
|
return items
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags(self):
|
||||||
|
return self.meta('tag')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def genres(self):
|
||||||
|
return self.meta('genre')
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@ -113,6 +109,15 @@ class EnsembleAccess(models.Model):
|
|||||||
"""
|
"""
|
||||||
Can have different access levels to a collection
|
Can have different access levels to a collection
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
ACCESS_UNLIMITED = 1
|
||||||
|
ACCESS_APPROVED = 2
|
||||||
|
|
||||||
|
ACCESS_TYPES = (
|
||||||
|
(ACCESS_UNLIMITED, 'Unlimited'),
|
||||||
|
(ACCESS_APPROVED, 'Approval required'),
|
||||||
|
)
|
||||||
|
|
||||||
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="allowed_collections")
|
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="allowed_collections")
|
||||||
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="allowed_ensembles")
|
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="allowed_ensembles")
|
||||||
access_type = models.PositiveSmallIntegerField(choices=ACCESS_TYPES, default=2)
|
access_type = models.PositiveSmallIntegerField(choices=ACCESS_TYPES, default=2)
|
||||||
@ -120,19 +125,27 @@ class EnsembleAccess(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "Ensemble access"
|
verbose_name_plural = "Ensemble access"
|
||||||
|
|
||||||
META_TAGS = (
|
|
||||||
('tag', 'Tag'),
|
|
||||||
('arr', 'Arranger'),
|
|
||||||
('lyrics', 'Lyracist'),
|
|
||||||
('genre', 'Genre'),
|
|
||||||
('style', 'Style'),
|
|
||||||
('orchestration', 'Orchestration'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Work(models.Model):
|
class Work(models.Model):
|
||||||
"""
|
"""
|
||||||
A musical work 'owned' by a collection from a licencing perspective.
|
A musical work 'owned' by a collection from a licencing perspective.
|
||||||
"""
|
"""
|
||||||
|
LICENCE_PUBLIC = 2
|
||||||
|
LICENCE_EXPIRED = 4
|
||||||
|
LICENCE_RECORDING = 5
|
||||||
|
LICENCE_PERFORMANCE = 6
|
||||||
|
LICENCE_PERUSAL = 8
|
||||||
|
LICENCE_NONE = 10
|
||||||
|
|
||||||
|
LICENCE_TYPES = (
|
||||||
|
(LICENCE_PUBLIC, 'Public Domain'),
|
||||||
|
(LICENCE_EXPIRED, 'Copyright Expired'),
|
||||||
|
(LICENCE_RECORDING, 'Recording Licence'),
|
||||||
|
(LICENCE_PERFORMANCE, 'Performance Licence'),
|
||||||
|
(LICENCE_PERUSAL, 'Perusal Licence'),
|
||||||
|
(LICENCE_NONE, 'Internal use only'),
|
||||||
|
)
|
||||||
|
|
||||||
name = models.CharField(max_length=255, help_text="Original name of the work")
|
name = models.CharField(max_length=255, help_text="Original name of the work")
|
||||||
edition = models.CharField(max_length=255, blank=True,
|
edition = models.CharField(max_length=255, blank=True,
|
||||||
help_text="Edition details to distinguish multiple versions")
|
help_text="Edition details to distinguish multiple versions")
|
||||||
@ -141,7 +154,7 @@ 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, blank=True, help_text="Original printed parts (IMSLP format)")
|
original_parts = models.JSONField(default=dict, 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")
|
||||||
@ -235,8 +248,18 @@ class Work(models.Model):
|
|||||||
return f"{self.name} ({self.composer})"
|
return f"{self.name} ({self.composer})"
|
||||||
|
|
||||||
class WorkMeta(models.Model):
|
class WorkMeta(models.Model):
|
||||||
|
|
||||||
|
META_CHOICES = (
|
||||||
|
('tag', 'Tag'),
|
||||||
|
('arr', 'Arranger'),
|
||||||
|
('lyrics', 'Lyracist'),
|
||||||
|
('genre', 'Genre'),
|
||||||
|
('style', 'Style'),
|
||||||
|
('orchestration', 'Orchestration'),
|
||||||
|
)
|
||||||
|
|
||||||
work = models.ForeignKey(Work, on_delete=models.CASCADE, related_name='meta_info')
|
work = models.ForeignKey(Work, on_delete=models.CASCADE, related_name='meta_info')
|
||||||
name = models.SlugField(max_length=20, choices=META_TAGS)
|
name = models.SlugField(max_length=20, choices=META_CHOICES)
|
||||||
value = models.CharField(max_length=255)
|
value = models.CharField(max_length=255)
|
||||||
|
|
||||||
def doc_upload_filename(doc, filename):
|
def doc_upload_filename(doc, filename):
|
||||||
@ -250,9 +273,22 @@ class Document(models.Model):
|
|||||||
"""
|
"""
|
||||||
Document represents a single file stored in the storage backend.
|
Document represents a single file stored in the storage backend.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
DOCTYPE_PDF = 1
|
||||||
|
DOCTYPE_AUDIO = 2
|
||||||
|
DOCTYPE_VIDEO = 3
|
||||||
|
DOCTYPE_SOURCE = 4
|
||||||
|
|
||||||
|
DOCTYPES = (
|
||||||
|
(DOCTYPE_PDF, 'PDF'),
|
||||||
|
(DOCTYPE_AUDIO, 'Audio'),
|
||||||
|
(DOCTYPE_VIDEO, 'Video'),
|
||||||
|
(DOCTYPE_SOURCE, 'Source'),
|
||||||
|
)
|
||||||
|
|
||||||
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs")
|
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs")
|
||||||
doctype = models.PositiveSmallIntegerField(choices=DOCTYPES, default=1)
|
doctype = models.PositiveSmallIntegerField(choices=DOCTYPES, default=1)
|
||||||
upload = models.FileField(upload_to=doc_upload_filename, storage=BYOStorage())
|
upload = models.FileField(upload_to=doc_upload_filename, storage=library_storage)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
version = models.CharField(max_length=30, blank=True)
|
version = models.CharField(max_length=30, blank=True)
|
||||||
|
|
||||||
@ -267,21 +303,54 @@ class Section(models.Model):
|
|||||||
"""
|
"""
|
||||||
Section is a tagged portion of a Document
|
Section is a tagged portion of a Document
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
TYPE_INSTRUMENT = 1
|
||||||
|
TYPE_MOVEMENT = 2
|
||||||
|
TYPE_EXCERPT = 3
|
||||||
|
|
||||||
|
SECTION_TYPES = (
|
||||||
|
(TYPE_INSTRUMENT, "Instrument"),
|
||||||
|
(TYPE_MOVEMENT, "Movement"),
|
||||||
|
(TYPE_EXCERPT, "Excerpt"),
|
||||||
|
)
|
||||||
|
|
||||||
|
SECTION_CLASSES = {
|
||||||
|
TYPE_INSTRUMENT: 'info',
|
||||||
|
TYPE_MOVEMENT: 'success',
|
||||||
|
TYPE_EXCERPT: 'warning',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
tag = models.CharField(max_length=50)
|
tag = models.CharField(max_length=50, blank=True)
|
||||||
|
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)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['doc', 'start', 'pk']
|
ordering = ['type', 'ordinal', 'doc', 'start', 'pk']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def instrument(self):
|
def name(self):
|
||||||
return Instrument.from_tag(self.tag)
|
if self.type == self.TYPE_INSTRUMENT:
|
||||||
|
instr = Instrument.from_tag(self.tag)
|
||||||
|
if self.ordinal:
|
||||||
|
return f'{instr} {self.ordinal}'
|
||||||
|
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]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self):
|
def filename(self):
|
||||||
return slugify(f'{self.doc.work.name}_{self.instrument}') + '.pdf'
|
return slugify(f'{self.doc.work.name} - {self.name}') + '.pdf'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pagerange(self):
|
def pagerange(self):
|
||||||
@ -292,4 +361,4 @@ class Section(models.Model):
|
|||||||
return "all"
|
return "all"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.doc.upload} [{self.pagerange}]'
|
return self.name
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{% extends "interface/project_base.html" %}
|
{% extends "interface/project_base.html" %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
<h3 class="title">Library collections for {{ request.user }}</h3>
|
<h3 class="title">Library collections for {% firstof request.user.first_name request.user.username %}</h3>
|
||||||
|
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
{% for collection in object_list %}
|
{% for collection in object_list %}
|
||||||
@ -17,6 +17,14 @@
|
|||||||
{% if collection.location %}{{ collection.location }},{% endif %}
|
{% if collection.location %}{{ collection.location }},{% endif %}
|
||||||
{{ collection.works.count }} items.
|
{{ collection.works.count }} items.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
{% for tag in collection.tags %}
|
||||||
|
<a href="{% url 'collection_work_list' collection.pk %}?filter=tag:{{ tag }}" class="tag is-success">{{ tag }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% for genre in collection.genres %}
|
||||||
|
<a href="{% url 'collection_work_list' collection.pk %}?filter=genre:{{ genre }}" class="tag is-warning">{{ genre }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
{% load path_filters %}
|
{% load path_filters %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'document_download' pk=doc.pk %}" target="_blank">
|
<td><a href="{{ doc.upload.url }}" target="_blank">
|
||||||
{{ doc.upload.name|basename }}</a></td>
|
{{ doc.upload.name|basename }}</a></td>
|
||||||
<td>
|
<td>
|
||||||
{% for part in doc.sections.all %}
|
{% for section in doc.sections.all %}
|
||||||
<a class="tag is-info" target="_blank" href="{% url 'part_download' pk=part.pk filename=part.filename %}">{{ part.instrument }}</a>
|
<a class="tag is-{{ section.bulma_class }}" target="_blank" href="{% url 'part_download' pk=section.pk filename=section.filename %}">{{ section.name }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="has-text-right">
|
<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' pk=doc.pk %}"><i class="fas fa-tags"
|
||||||
title="Manage Tags"></i></a>
|
title="Manage Tags"></i></a>
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<h3 class="title">
|
<h3 class="title">
|
||||||
{{ work.name }}
|
{{ work.name }}
|
||||||
{% for tag in work.tags %}
|
{% for tag in work.tags %}
|
||||||
<span class="tag is-success">{{ tag }}</span>
|
<a href="{% url 'collection_work_list' work.collection.pk %}?filter=tag:{{ tag }}" class="tag is-success">{{ tag }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="subtitle">{% firstof work.composer "Unattributed" %}{% if work.edition %} - {{ work.edition }}{% endif %}</p>
|
<p class="subtitle">{% firstof work.composer "Unattributed" %}{% if work.edition %} - {{ work.edition }}{% endif %}</p>
|
||||||
@ -30,10 +30,10 @@
|
|||||||
|
|
||||||
<p class="block">
|
<p class="block">
|
||||||
Location: <a href="{% url 'collection_work_list' work.collection.pk %}">{{ work.collection }}</a> [{{ work.identifier }}]<br/>
|
Location: <a href="{% url 'collection_work_list' work.collection.pk %}">{{ work.collection }}</a> [{{ work.identifier }}]<br/>
|
||||||
Running time: {{ work.duration }}<br/>
|
Running time: {% firstof work.duration 'Unknown' %}<br/>
|
||||||
Licence: {{ work.get_licence_display }}<br/>
|
Licence: {{ work.get_licence_display }}<br/>
|
||||||
{% for meta in work.meta %}
|
{% for meta in work.meta %}
|
||||||
{{ meta.get_name_display }}: {{ meta.value }}<br/>
|
{{ meta.get_name_display }}: <a href="{% url 'collection_work_list' work.collection.pk %}?filter={{ meta.name}}:{{ meta.value }}">{{ meta.value }}</a><br/>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -79,9 +79,9 @@
|
|||||||
{% 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' pk=work.pk %}">Full Set</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for part in work.digital_parts %}
|
{% for section in work.digital_parts %}
|
||||||
<a class="tag is-info" href="{% url 'part_download' pk=part.pk filename=part.filename %}"
|
<a class="tag is-info" href="{% url 'part_download' pk=section.pk filename=section.filename %}"
|
||||||
target="part_{{ part.pk }}" rel="">{{ part.instrument }}</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>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@ -1,8 +1,15 @@
|
|||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
from django.contrib.auth import views as auth_views
|
from django.contrib.auth import views as auth_views
|
||||||
|
from rest_framework import routers
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
from library.views import api
|
||||||
|
|
||||||
|
#router = routers.DefaultRouter()
|
||||||
|
#router.register(r'collection', external.CollectionViewSet, basename="collection")
|
||||||
|
#router.register(r'work', external.WorkViewSet, basename="work")
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
path('projects/<int:project>/items', views.ProjectItemListView.as_view(), name="item_list"),
|
path('projects/<int:project>/items', views.ProjectItemListView.as_view(), name="item_list"),
|
||||||
@ -11,7 +18,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('library/collections', views.CollectionListView.as_view(), name="collection_list"),
|
path('library/collections', views.CollectionListView.as_view(), name="collection_list"),
|
||||||
path('library/collections/<int:pk>', views.CollectionWorkListView.as_view(), name="collection_work_list"),
|
path('library/collections/<int:pk>', views.CollectionWorkListView.as_view(), name="collection_work_list"),
|
||||||
path('library/collection/<int:pk>/create', views.WorkAddView.as_view(), name="work_add"),
|
path('library/collections/<int:pk>/create', views.WorkAddView.as_view(), name="work_add"),
|
||||||
|
|
||||||
path('library/works', views.WorkListView.as_view(), name="work_list"),
|
path('library/works', views.WorkListView.as_view(), name="work_list"),
|
||||||
path('library/works/<int:pk>', views.WorkDetailView.as_view(), name="work_detail"),
|
path('library/works/<int:pk>', views.WorkDetailView.as_view(), name="work_detail"),
|
||||||
@ -24,4 +31,9 @@ urlpatterns = [
|
|||||||
path('library/documents/<int:pk>/download', views.DocumentDownloadView.as_view(), name="document_download"),
|
path('library/documents/<int:pk>/download', views.DocumentDownloadView.as_view(), name="document_download"),
|
||||||
path('library/documents/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
|
path('library/documents/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
|
||||||
path('library/parts/<int:pk>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
|
path('library/parts/<int:pk>/<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"),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -15,10 +15,10 @@ import re
|
|||||||
|
|
||||||
from interface.views import EnsembleMixin, ProjectMixin
|
from interface.views import EnsembleMixin, ProjectMixin
|
||||||
from interface.models import Project
|
from interface.models import Project
|
||||||
from .models import Collection, Work, Document, Section
|
from library.models import Collection, Work, Document, Section
|
||||||
from .imslp import INSTRUMENT_TAGS, INSTRUMENTS
|
from library.imslp import INSTRUMENT_TAGS, INSTRUMENTS
|
||||||
from . import forms, models
|
from library import forms, models
|
||||||
from .pdf_utils import extract_pages, extract_and_concat
|
from library.pdf_utils import extract_pages, extract_and_concat
|
||||||
|
|
||||||
class ProjectItemListView(ProjectMixin, ListView):
|
class ProjectItemListView(ProjectMixin, ListView):
|
||||||
template_name = "library/item_list.html"
|
template_name = "library/item_list.html"
|
||||||
@ -364,10 +364,10 @@ class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
|||||||
|
|
||||||
result = extract_pages(self.object.doc.upload.path, self.object.doc.work.name, self.object.start, self.object.end)
|
result = extract_pages(self.object.doc.upload.path, self.object.doc.work.name, self.object.start, self.object.end)
|
||||||
|
|
||||||
download_name = f'{self.object.doc.work.name}_{self.object.instrument}.pdf'
|
#download_name = f'{self.object.doc.work.name}_{self.object.instrument}.pdf'
|
||||||
|
|
||||||
response = FileResponse(result, content_type="application/pdf")
|
response = FileResponse(result, content_type="application/pdf")
|
||||||
response['Content-Disposition'] = f'inline; filename="foo.pdf"'
|
response['Content-Disposition'] = f'inline; filename="{self.args["filename"]}"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
161
app/library/views/api.py
Normal file
161
app/library/views/api.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
"""
|
||||||
|
Views relating to importing and exporting collection items
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
from interface.views import EnsembleMixin
|
||||||
|
from library.views import WorkMixin
|
||||||
|
from django.views.generic import View
|
||||||
|
from django.http import JsonResponse
|
||||||
|
|
||||||
|
from djantic import ModelSchema
|
||||||
|
from library.models import Work, Document, Section
|
||||||
|
|
||||||
|
class DocumentSchema(ModelSchema):
|
||||||
|
class Config:
|
||||||
|
model = Document
|
||||||
|
|
||||||
|
|
||||||
|
class WorkSchema(ModelSchema):
|
||||||
|
|
||||||
|
docs: DocumentSchema
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
model = Work
|
||||||
|
exclude = ['licence']
|
||||||
|
|
||||||
|
class WorkExportView(EnsembleMixin, WorkMixin, View):
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
obj = self.get_queryset().get(pk=kwargs['pk'])
|
||||||
|
schema = WorkSchema.from_orm(obj)
|
||||||
|
return JsonResponse(schema.dict())
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from library.views import WorkMixin
|
||||||
|
from interface.views import EnsembleMixin
|
||||||
|
|
||||||
|
from rest_framework import routers, serializers, viewsets
|
||||||
|
|
||||||
|
from library.models import Collection, Work, Document, Section
|
||||||
|
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from io import BytesIO
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.core.files.uploadedfile import TemporaryUploadedFile
|
||||||
|
|
||||||
|
|
||||||
|
class SectionSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Section
|
||||||
|
exclude = ['id', 'doc']
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
start = instance.start or 0
|
||||||
|
end = instance.end or 0
|
||||||
|
return f"{instance.tag}:{instance.type}:{start}:{end}"
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
tag, section_type, start, end = data.split(":")
|
||||||
|
try:
|
||||||
|
start = int(start)
|
||||||
|
except:
|
||||||
|
start = 0
|
||||||
|
try:
|
||||||
|
end = int(end)
|
||||||
|
except:
|
||||||
|
end = 0
|
||||||
|
return super().to_internal_value({'tag': tag, 'type': int(section_type), 'start': start, 'end': end})
|
||||||
|
|
||||||
|
class DocumentSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
upload = serializers.URLField()
|
||||||
|
sections = SectionSerializer(many=True)
|
||||||
|
#doctype = serializers.CharField(source='get_doctype_display')
|
||||||
|
|
||||||
|
#def to_internal_value(self, data):
|
||||||
|
# r = requests.get(data['upload'], stream=True)
|
||||||
|
# with tempfile.NamedTemporaryFile('wb') as f:
|
||||||
|
# shutil.copyfileobj(r.raw, f)
|
||||||
|
# data['upload'] = f.name
|
||||||
|
# print(repr(data))
|
||||||
|
# return super().to_internal_value(data)
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
data = super().to_representation(instance)
|
||||||
|
if data['upload'][0] == '/':
|
||||||
|
data['upload'] = 'http://localhost:8000' + (data['upload'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
print("CREATE", validated_data)
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
print("VALIDATE", data)
|
||||||
|
return super().validate(data)
|
||||||
|
|
||||||
|
def validate_upload(self, value):
|
||||||
|
print("VALIDATE", value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Document
|
||||||
|
exclude = ["id", "work", "version", "created"]
|
||||||
|
|
||||||
|
# Serializers define the API representation.
|
||||||
|
class WorkSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
docs = DocumentSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Work
|
||||||
|
exclude = ['id', 'collection', 'projects', 'parent']
|
||||||
|
|
||||||
|
def create(self, validated):
|
||||||
|
with transaction.atomic():
|
||||||
|
docs = validated.pop('docs', [])
|
||||||
|
work = Work.objects.create(**validated)
|
||||||
|
|
||||||
|
for d in docs:
|
||||||
|
sections = d.pop('sections', [])
|
||||||
|
|
||||||
|
r = requests.get(d['upload'], stream=True)
|
||||||
|
f = TemporaryUploadedFile(d['upload'], r.headers['content-type'], r.headers['content-length'], r.encoding)
|
||||||
|
shutil.copyfileobj(r.raw, f.file)
|
||||||
|
r.close()
|
||||||
|
d['upload'] = f
|
||||||
|
doc = Document.objects.create(work_id=work.pk, **d)
|
||||||
|
|
||||||
|
for s in sections:
|
||||||
|
Section.objects.create(doc_id=doc.pk, **s)
|
||||||
|
|
||||||
|
return work
|
||||||
|
|
||||||
|
class CollectionSerializer(serializers.Serializer):
|
||||||
|
works = WorkSerializer(many=True)
|
||||||
|
|
||||||
|
from rest_framework import generics
|
||||||
|
|
||||||
|
class CollectionExportView(generics.RetrieveAPIView):
|
||||||
|
serializer_class = CollectionSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Collection.objects.filter(administrators=self.request.user)
|
||||||
|
|
||||||
|
class WorkExportView(generics.RetrieveAPIView):
|
||||||
|
serializer_class = WorkSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Work.objects.filter(collection__administrators=self.request.user)
|
||||||
|
|
||||||
|
class WorkImportView(generics.CreateAPIView):
|
||||||
|
serializer_class = WorkSerializer
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(collection_id=self.kwargs['pk'])
|
||||||
|
|
||||||
@ -41,6 +41,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django_markdown2',
|
'django_markdown2',
|
||||||
|
'rest_framework',
|
||||||
'crispy_forms',
|
'crispy_forms',
|
||||||
'crispy_bulma',
|
'crispy_bulma',
|
||||||
'byostorage',
|
'byostorage',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user