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
db.sqlite3
credentials
polyphonic/settings.py
local_settings.py
env
old
test.*
static
teststore
cache
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 %}
<p class="menu-label">Admin</p>
{% if request.is_admin %}
<ul class="menu-list">
{% if request.is_admin %}
<li><a class="admin-link" href="{% url 'collection_list' %}">Collections</a></li>
{% endif %}
{% if request.user.is_superuser %}
<li><a class="admin-link" href="/admin" target="polyphonic_admin" rel="noopener noreferrer">Django Admin</a></li>
{% endif %}
</ul>
{% endif %}
<ul class="menu-list">
{% 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/<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"),
]
]
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:
model = Work
fields = ['name', 'code', 'running_time', 'notes']
fields = ['name', 'composer', 'edition', 'code', 'running_time', 'notes']
class PlaylistAddForm(forms.Form):
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.db.models import Q, Count, Min, Max
import os.path
from byostorage.user import BYOStorage
from .imslp import Instrument
@ -97,7 +99,7 @@ class ProjectItem(models.Model):
ordering = ['order', 'work']
def __str__(self):
return f"<{self.project_id}:{self.work.slug}>"
return f"<{self.project_id}:{slugify(self.work.name)}>"
class Collection(models.Model):
"""
@ -143,15 +145,13 @@ class Work(models.Model):
"""
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")
edition = models.CharField(max_length=255, blank=True,
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",
help_text="Arrangement of another work or part of an anthology")
composer = models.CharField(max_length=255, blank=True,
help_text="Surname, First Name/Initials")
composer = models.CharField(max_length=255, default='Anon',
help_text="Surname, Initials")
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
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):
qs = self.docs.filter(sections__tag__in=tags)
@ -194,11 +198,6 @@ class Work(models.Model):
def meta(self):
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
def active_projects(self):
return self.projects.filter(active=True)
@ -235,10 +234,11 @@ class Work(models.Model):
composer = self.composer or "Anon"
words = self.name.split()
if len(words) > 2:
work = ''.join([ x[0] for x in self.name.split() ])
else:
work = words[0][:3]
#if len(words) > 2:
# work = ''.join([ x[0] for x in self.name.split() ])
#else:
# work = words[0][:3]
work = words[0][:3]
return f"{composer[:4]}-{work}-{self.pk:03d}".upper()
@ -256,7 +256,7 @@ def doc_upload_filename(doc, filename):
storage = collection.storage
if not storage:
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):
"""
@ -268,6 +268,10 @@ class Document(models.Model):
created = models.DateTimeField(auto_now_add=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):
return self.upload.name

View File

@ -13,7 +13,10 @@
</a>
</header>
<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>

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>
<tbody id="doc-list">
{% for doc in work.docs.all %}
<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" 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>
{% include 'library/document_entry.html' %}
{% endfor %}
</tbody>
</table>
@ -188,7 +173,7 @@
<script>
Dropzone.options.docUpload = { // camelized version of the `id`
paramName: "upload", // The name that will be used to transfer the file
maxFilesize: 12, // MB
maxFilesize: 50, // MB
createImageThumbnails: false,
thumbnailWidth: 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>/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>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
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.views.generic.detail import DetailView, SingleObjectMixin, View
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.db import IntegrityError
from django.db.models import Q, Count, Sum
from django.utils.timezone import now
from django.urls import reverse
from django.template.loader import render_to_string
import json
import os.path
import re
from interface.views import EnsembleMixin, ProjectMixin
from interface.models import Project
from .models import Collection, Work, Document, Section
from .imslp import INSTRUMENTS
from .imslp import INSTRUMENTS, TAG_LOOKUP
from . import forms, models
from .pdf_utils import extract_pages, extract_and_concat
@ -193,6 +195,8 @@ class WorkUpdateView(EnsembleMixin, WorkMixin, UpdateView):
def get_success_url(self):
return resolve_url('work_detail', self.kwargs['pk'])
class WorkAddToProject(EnsembleMixin, FormView):
admin_required = True
form_class = forms.ProjectSelectForm
@ -269,20 +273,25 @@ class WorkAddDocumentView(EnsembleMixin, CreateView):
doc = form.save(commit=False)
doc.work_id = self.kwargs['pk']
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':
filename = os.path.basename(doc.upload.name)
return JsonResponse({
"message": "created",
"id": doc.pk,
"entry": f"""
<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>
"""
"entry": render_to_string('library/document_entry.html', {'doc': doc, 'request': self.request})
}, status=201)
return redirect('document_annotate', doc.pk)
@ -336,6 +345,13 @@ class DocumentAnnotateView(EnsembleMixin, DocumentMixin, DetailView):
data['json_data'] = {'pageTags': pages, 'instruments': dict(INSTRUMENTS)}
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):

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',
}
}