Improved upload
This commit is contained in:
parent
53ec846f98
commit
59deeffefe
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,8 +2,9 @@ __pycache__
|
||||
*.pyc
|
||||
db.sqlite3
|
||||
credentials
|
||||
polyphonic/settings.py
|
||||
local_settings.py
|
||||
env
|
||||
old
|
||||
test.*
|
||||
static
|
||||
teststore
|
||||
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal 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"]
|
||||
@ -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 %}
|
||||
|
||||
@ -28,3 +28,8 @@ urlpatterns = [
|
||||
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'}))
|
||||
@ -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())
|
||||
|
||||
28
app/library/migrations/0002_auto_20221201_0934.py
Normal file
28
app/library/migrations/0002_auto_20221201_0934.py
Normal 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),
|
||||
),
|
||||
]
|
||||
21
app/library/migrations/0003_auto_20221201_1540.py
Normal file
21
app/library/migrations/0003_auto_20221201_1540.py
Normal 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',
|
||||
),
|
||||
]
|
||||
@ -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,9 +234,10 @@ 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:
|
||||
#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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
22
app/library/templates/library/document_confirm_delete.html
Normal file
22
app/library/templates/library/document_confirm_delete.html
Normal 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 %}
|
||||
17
app/library/templates/library/document_entry.html
Normal file
17
app/library/templates/library/document_entry.html
Normal 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>
|
||||
@ -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,
|
||||
|
||||
@ -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'}))
|
||||
@ -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
8
docker_settings.py
Normal file
@ -0,0 +1,8 @@
|
||||
from .default_settings import *
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': '/var/polyphonic/db.sqlite3',
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user