Improved upload

This commit is contained in:
Tris Forster 2022-12-02 13:24:31 +11:00
parent 53ec846f98
commit 59deeffefe
16 changed files with 183 additions and 53 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
local_settings.py
db.sqlite3

5
.gitignore vendored
View File

@ -2,11 +2,12 @@ __pycache__
*.pyc *.pyc
db.sqlite3 db.sqlite3
credentials credentials
polyphonic/settings.py local_settings.py
env env
old
test.* test.*
static static
teststore teststore
cache cache
local_storage local_storage
media media

20
Dockerfile Normal file
View File

@ -0,0 +1,20 @@
FROM alpine:3.14
RUN apk add --no-cache python3 git ghostscript sqlite
WORKDIR /root
RUN python3 -m ensurepip
RUN pip3 install -U pip --no-cache-dir
COPY app /opt/polyphonic
WORKDIR /opt/polyphonic
COPY docker_settings.py polyphonic/local_settings.py
RUN pip3 install -r requirements.txt --no-cache-dir
RUN mkdir /var/polyphonic
RUN SECRET_KEY=_ python3 manage.py collectstatic --noinput
ENTRYPOINT ["python3", "manage.py"]
CMD ["runserver", "0.0.0.0:8000", "--insecure"]

View File

@ -42,14 +42,14 @@
{% endif %} {% endif %}
<p class="menu-label">Admin</p> <p class="menu-label">Admin</p>
{% if request.is_admin %}
<ul class="menu-list"> <ul class="menu-list">
{% if request.is_admin %}
<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 %}
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<li><a class="admin-link" href="/admin" target="polyphonic_admin" rel="noopener noreferrer">Django Admin</a></li> <li><a class="admin-link" href="/admin" target="polyphonic_admin" rel="noopener noreferrer">Django Admin</a></li>
{% endif %} {% endif %}
</ul> </ul>
{% endif %}
<ul class="menu-list"> <ul class="menu-list">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}

View File

@ -27,4 +27,9 @@ urlpatterns = [
path('projects/<int:project>/resources/add', views.ResourceCreateView.as_view(), name="resource_create"), path('projects/<int:project>/resources/add', views.ResourceCreateView.as_view(), name="resource_create"),
path('projects/<int:project>/resources/<int:pk>/upload', views.ResourceUploadView.as_view(), name="resource_upload"), path('projects/<int:project>/resources/<int:pk>/upload', views.ResourceUploadView.as_view(), name="resource_upload"),
path('projects/<int:project>/resources/<int:pk>/edit', views.ResourceEditView.as_view(), name="resource_edit"), path('projects/<int:project>/resources/<int:pk>/edit', views.ResourceEditView.as_view(), name="resource_edit"),
] ]
from django.conf import settings
if settings.DEBUG:
from django.views.static import serve
urlpatterns.append(path('local_storage/<path:path>', serve, {'document_root': 'local_storage'}))

View File

@ -9,7 +9,7 @@ class WorkCreateForm(forms.ModelForm, BaseForm):
class Meta: class Meta:
model = Work model = Work
fields = ['name', 'code', 'running_time', 'notes'] fields = ['name', 'composer', 'edition', 'code', 'running_time', 'notes']
class PlaylistAddForm(forms.Form): class PlaylistAddForm(forms.Form):
work = forms.ModelChoiceField(queryset=Work.objects.all()) work = forms.ModelChoiceField(queryset=Work.objects.all())

View File

@ -0,0 +1,28 @@
# Generated by Django 3.2.7 on 2022-11-30 22:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='work',
name='slug',
),
migrations.AddField(
model_name='work',
name='path',
field=models.CharField(default='', editable=False, help_text='Used as folder name', max_length=255),
preserve_default=False,
),
migrations.AlterField(
model_name='work',
name='composer',
field=models.CharField(default='Anon', help_text='Surname, Initials', max_length=255),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.7 on 2022-12-01 04:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0002_auto_20221201_0934'),
]
operations = [
migrations.AlterUniqueTogether(
name='work',
unique_together={('collection', 'composer', 'name', 'edition')},
),
migrations.RemoveField(
model_name='work',
name='path',
),
]

View File

@ -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 os.path
from byostorage.user import BYOStorage from byostorage.user import BYOStorage
from .imslp import Instrument from .imslp import Instrument
@ -97,7 +99,7 @@ class ProjectItem(models.Model):
ordering = ['order', 'work'] ordering = ['order', 'work']
def __str__(self): def __str__(self):
return f"<{self.project_id}:{self.work.slug}>" return f"<{self.project_id}:{slugify(self.work.name)}>"
class Collection(models.Model): class Collection(models.Model):
""" """
@ -143,15 +145,13 @@ 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.
""" """
slug = models.SlugField(max_length=100, editable=False,
help_text="Used as folder name")
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")
parent = models.ForeignKey('Work', null=True, blank=True, on_delete=models.SET_NULL, related_name="related_works", parent = models.ForeignKey('Work', null=True, blank=True, on_delete=models.SET_NULL, related_name="related_works",
help_text="Arrangement of another work or part of an anthology") help_text="Arrangement of another work or part of an anthology")
composer = models.CharField(max_length=255, blank=True, composer = models.CharField(max_length=255, default='Anon',
help_text="Surname, First Name/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, blank=True, help_text="Original printed parts (IMSLP format)")
@ -168,6 +168,10 @@ class Work(models.Model):
# Allocation to projects # Allocation to projects
projects = models.ManyToManyField('interface.Project', through='ProjectItem', related_name="works", help_text="Current usage") projects = models.ManyToManyField('interface.Project', through='ProjectItem', related_name="works", help_text="Current usage")
@property
def folder(self):
return f"{slugify(self.composer)}/{slugify(self.name)}-{self.pk:04d}"
def extract(self, *tags): def extract(self, *tags):
qs = self.docs.filter(sections__tag__in=tags) qs = self.docs.filter(sections__tag__in=tags)
@ -194,11 +198,6 @@ class Work(models.Model):
def meta(self): def meta(self):
return self.meta_info.exclude(name='tag') return self.meta_info.exclude(name='tag')
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super(Work, self).save(*args, **kwargs)
@property @property
def active_projects(self): def active_projects(self):
return self.projects.filter(active=True) return self.projects.filter(active=True)
@ -235,10 +234,11 @@ class Work(models.Model):
composer = self.composer or "Anon" composer = self.composer or "Anon"
words = self.name.split() words = self.name.split()
if len(words) > 2: #if len(words) > 2:
work = ''.join([ x[0] for x in self.name.split() ]) # work = ''.join([ x[0] for x in self.name.split() ])
else: #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:03d}".upper()
@ -256,7 +256,7 @@ def doc_upload_filename(doc, filename):
storage = collection.storage storage = collection.storage
if not storage: if not storage:
raise RuntimeError("Collection has no storage attached") raise RuntimeError("Collection has no storage attached")
return f'{storage}:library/{collection.prefix}/{doc.work.slug}-{doc.work.pk}/{filename}' return f'{storage}:library/{collection.prefix}/{doc.work.folder}/{filename}'
class Document(models.Model): class Document(models.Model):
""" """
@ -268,6 +268,10 @@ class Document(models.Model):
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)
def delete(self, *args, **kwargs):
self.upload.delete(save=False)
return super().delete(*args, **kwargs)
def __str__(self): def __str__(self):
return self.upload.name return self.upload.name

View File

@ -13,7 +13,10 @@
</a> </a>
</header> </header>
<div class="card-content"> <div class="card-content">
<p>{{ collection.location }}, {{ collection.works.count }} items.</p> <p>
{% if collection.location %}{{ collection.location }},{% endif %}
{{ collection.works.count }} items.
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,22 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="columns">
<div class="column is-half is-centered">
<div class="block">
<p>Are you sure you want to delete<br>
<b>"{{ object.upload.name }}"</b>?</p>
</div>
<form method="post">{% csrf_token %}
<div class="field is-grouped">
<div class="control">
<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>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% load path_filters %}
<tr>
<td><a href="{% url 'document_download' pk=doc.pk %}" target="_blank">
{{ doc.upload.name|basename }}</a></td>
<td>
{% for part in doc.sections.all %}
<a class="tag is-info" target="_blank" href="{% url 'part_download' pk=part.pk filename=part.filename %}">{{ part.instrument }}</a>
{% endfor %}
</td>
<td class="has-text-right">
{% if request.is_admin %}
<a href="{% url 'document_annotate' 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>
{% endif %}
</td>
</tr>

View File

@ -110,22 +110,7 @@
</thead> </thead>
<tbody id="doc-list"> <tbody id="doc-list">
{% for doc in work.docs.all %} {% for doc in work.docs.all %}
<tr> {% include 'library/document_entry.html' %}
<td><a href="{% url 'document_download' pk=doc.pk %}" target="_blank">
{{ doc.upload.name|basename }}</a></td>
<td>
{% for part in doc.sections.all %}
<a class="tag is-info" href="{% url 'part_download' pk=part.pk filename=part.filename %}">{{ part.instrument }}</a>
{% endfor %}
</td>
<td class="has-text-right">
{% if request.is_admin %}
<a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-tags"
title="Manage Tags"></i></a>
<a href=""><i class="fas fa-trash-alt" title="Delete Document"></i></a>
{% endif %}
</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@ -188,7 +173,7 @@
<script> <script>
Dropzone.options.docUpload = { // camelized version of the `id` Dropzone.options.docUpload = { // camelized version of the `id`
paramName: "upload", // The name that will be used to transfer the file paramName: "upload", // The name that will be used to transfer the file
maxFilesize: 12, // MB maxFilesize: 50, // MB
createImageThumbnails: false, createImageThumbnails: false,
thumbnailWidth: 60, thumbnailWidth: 60,
thumbnailHeight: 60, thumbnailHeight: 60,

View File

@ -20,10 +20,8 @@ urlpatterns = [
path('library/works/<int:pk>/add_to_project', views.WorkAddToProject.as_view(), name="work_add_to_project"), path('library/works/<int:pk>/add_to_project', views.WorkAddToProject.as_view(), name="work_add_to_project"),
path('library/works/<int:pk>/upload', views.WorkAddDocumentView.as_view(), name="document_add"), path('library/works/<int:pk>/upload', views.WorkAddDocumentView.as_view(), name="document_add"),
path('library/documents/<int:pk>/delete', views.DocumentDeleteView.as_view(), name="document_delete"),
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"),
] ]
from django.views.static import serve
urlpatterns.append(path('docs/<path:path>', serve, {'document_root': 'local_storage'}))

View File

@ -1,20 +1,22 @@
from django.shortcuts import render, redirect, resolve_url from django.shortcuts import render, 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 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.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
import json import json
import os.path import os.path
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 .models import Collection, Work, Document, Section
from .imslp import INSTRUMENTS from .imslp import INSTRUMENTS, TAG_LOOKUP
from . import forms, models from . import forms, models
from .pdf_utils import extract_pages, extract_and_concat from .pdf_utils import extract_pages, extract_and_concat
@ -193,6 +195,8 @@ class WorkUpdateView(EnsembleMixin, WorkMixin, UpdateView):
def get_success_url(self): def get_success_url(self):
return resolve_url('work_detail', self.kwargs['pk']) return resolve_url('work_detail', self.kwargs['pk'])
class WorkAddToProject(EnsembleMixin, FormView): class WorkAddToProject(EnsembleMixin, FormView):
admin_required = True admin_required = True
form_class = forms.ProjectSelectForm form_class = forms.ProjectSelectForm
@ -269,20 +273,25 @@ class WorkAddDocumentView(EnsembleMixin, CreateView):
doc = form.save(commit=False) doc = form.save(commit=False)
doc.work_id = self.kwargs['pk'] doc.work_id = self.kwargs['pk']
doc.save() doc.save()
# auto tag the document
name, _ = os.path.splitext(os.path.basename(doc.upload.name))
parts = re.split(r'[^A-Za-z]+', name.lower())
parts.reverse()
for word in parts:
print(word)
if word in TAG_LOOKUP:
doc.sections.create(tag=TAG_LOOKUP[word])
break
if self.request.headers['Accept'] == 'application/json': if self.request.headers['Accept'] == 'application/json':
filename = os.path.basename(doc.upload.name) filename = os.path.basename(doc.upload.name)
return JsonResponse({ return JsonResponse({
"message": "created", "message": "created",
"id": doc.pk, "id": doc.pk,
"entry": f""" "entry": render_to_string('library/document_entry.html', {'doc': doc, 'request': self.request})
<td><a href="{reverse('document_download', args=[doc.pk])}">{filename}</a></td>
<td/>
<td class="has-text-right">
<a href="{reverse('document_annotate', args=[doc.pk])}"><i class="fas fa-tags"
title="Manage Tags"></i></a>
<a href=""><i class="fas fa-trash-alt" title="Delete Document"></i></a>
</td>
"""
}, status=201) }, status=201)
return redirect('document_annotate', doc.pk) return redirect('document_annotate', doc.pk)
@ -336,6 +345,13 @@ 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):
#def get_template_names(self):
# return ["interface/default_form.html"]
def get_success_url(self):
return resolve_url('work_detail', self.object.work.pk)
class PartDownloadView(EnsembleMixin, SingleObjectMixin, View): class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):

8
docker_settings.py Normal file
View File

@ -0,0 +1,8 @@
from .default_settings import *
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/var/polyphonic/db.sqlite3',
}
}