Author SHA1 Message Date
tris 4d964291b2 Fixed poetry deps 2025-08-29 12:51:01 +10:00
tris ee5305ba6c Merge branch 'master' of gitea.tfconsulting.com.au:projects/polyphonic 2025-08-29 11:39:58 +10:00
tris 147c84550c Switching to poetry 2025-08-29 11:37:57 +10:00
Tris Forster 596445061f Upgraded to Django 5.1 2024-08-26 12:06:18 +10:00
Tris Forster 02858a76c0 Cleanup 2024-08-05 15:49:39 +10:00
Tris Forster 1bcec919cf PEP Cleanup 2024-08-05 12:53:58 +10:00
Tris Forster 78789c02ed Minor UI Tweaks 2024-06-21 15:42:09 +10:00
14 changed files with 185 additions and 76 deletions
+2
View File
@@ -1,10 +1,12 @@
__pycache__
*.pyc
*.sqlite3
*.swp
credentials.json
credentials
local_settings.py
.coverage
Session.vim
/env
/data
/old
+19
View File
@@ -0,0 +1,19 @@
## Polyphonic TODO
## Core interface
* Shift from crispy forms to native component templates
* Make long running calls async (Django 5)
* Deprecate Django 4 portions
### Library App
* Remove music tags and replace with strings vn1 -> 'Violin 1'
* GDrive selector
* Move upload to modal from 'Upload' button
* Tagging app - migrate to AlpineJS
* Allow other tags (movements, sections, pieces)
### Submissions App
* None currently pending
+1 -1
View File
@@ -55,4 +55,4 @@
{% block scripts %}
{% endblock %}
</body>
</html>
</html>
@@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-06-18 05:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0014_auto_20230223_1422'),
]
operations = [
migrations.AddField(
model_name='collection',
name='settings',
field=models.JSONField(default=dict, help_text='Storage specific settings'),
),
migrations.AlterField(
model_name='document',
name='doctype',
field=models.PositiveSmallIntegerField(choices=[(1, 'PDF'), (2, 'Audio'), (3, 'Video'), (4, 'Misc')], default=1),
),
]
+21 -20
View File
@@ -5,7 +5,6 @@ from django.db import models
from django.utils.text import slugify
from django.utils.timezone import now
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 re
@@ -44,13 +43,13 @@ class Orchestration(models.Model):
def as_list(self):
tags = [ t.strip() for t in self.instruments.split(' ') ]
return [ (t, MusicTag.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):
@@ -79,7 +78,6 @@ class ProjectItem(models.Model):
approved_by = models.ForeignKey('auth.User', on_delete=models.CASCADE)
order = models.SmallIntegerField(default=0)
section = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['order', 'work']
@@ -93,16 +91,18 @@ class Collection(models.Model):
"""
name = models.CharField(max_length=255,
help_text="Name of the collection")
prefix = models.SlugField(max_length=30, default="default",
prefix = models.CharField(max_length=255, default="default",
help_text="Folder to store works in")
administrators = models.ManyToManyField('auth.User', related_name="collections",
administrators = models.ManyToManyField('auth.User', related_name="collections",
help_text="Administrators for this collection")
location = models.CharField(max_length=100,
location = models.CharField(max_length=100,
help_text="Physical location (institution, town...)", blank=True)
storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True,
storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True,
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)")
settings = models.JSONField(default=dict, blank=True,
help_text="Storage specific settings")
nonce = models.SmallIntegerField(default=1,
help_text="Increment this to reset the authentication links")
@@ -116,8 +116,8 @@ class Collection(models.Model):
@property
def genres(self):
return self.meta('genre')
return self.meta('genre')
def has_administrator(self, user):
if not user.is_authenticated:
return False
@@ -130,7 +130,7 @@ class Collection(models.Model):
def auth(self):
return sign_data(f'{self.pk}-{self.nonce}', 12)
def __str__(self):
return self.name
@@ -174,7 +174,7 @@ class Work(models.Model):
(LICENCE_PERUSAL, 'Perusal Licence'),
(LICENCE_NONE, 'Internal use only'),
)
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")
@@ -191,7 +191,7 @@ class Work(models.Model):
code = models.CharField(max_length=100, blank=True, help_text="Collection specific code or number. Will be auto-generated if not supplied")
licence = models.PositiveSmallIntegerField(choices=LICENCE_TYPES, default=6, help_text="Copyright status")
max_projects = models.IntegerField(default=1, help_text="How many active projects can this work be attached to")
# Extra info
running_time = models.DurationField(null=True, blank=True, help_text="Running time in mm:ss format")
notes = models.TextField(blank=True)
@@ -219,7 +219,7 @@ class Work(models.Model):
#return [ s[1] for s in sections ]
sections = list(dict(sections).items()) # primitive unique()
return sections
def pdfs(self):
return self.docs.filter(doctype=Document.DOCTYPE_PDF)
@@ -304,7 +304,7 @@ class WorkMeta(models.Model):
('style', 'Style'),
('orchestration', 'Orchestration'),
)
work = models.ForeignKey(Work, on_delete=models.CASCADE, related_name='meta_info')
name = models.SlugField(max_length=20, choices=META_CHOICES)
value = models.CharField(max_length=255)
@@ -348,7 +348,7 @@ class Document(models.Model):
def delete(self, *args, **kwargs):
self.upload.delete(save=False)
return super().delete(*args, **kwargs)
def filename(self):
return os.path.basename(self.upload.name)
@@ -359,7 +359,7 @@ class Section(models.Model):
"""
Section is a tagged portion of a Document
"""
PAGE_AUTO = 0
PAGE_LEFT = 1
PAGE_RIGHT = 2
@@ -396,7 +396,7 @@ class Section(models.Model):
def filename(self):
return slugify(f'{self.doc.work.name} - {self.name}').title() + '.pdf'
@property
@property
def pagerange(self):
if self.start:
if self.end:
@@ -405,4 +405,5 @@ class Section(models.Model):
return "all"
def __str__(self):
return self.name
return self.name
@@ -139,7 +139,7 @@
crossorigin="anonymous"></script>
{{ json_data|json_script:"data" }}
<script type="text/javascript">
let url = "{{ document.upload.url|safe }}";
let url = "{% url 'document_download' collection.pk document.pk %}";
// Loaded via <script> tag, create shortcut to access PDF.js exports.
let pdfjsLib = window['pdfjs-dist/build/pdf'];
@@ -447,4 +447,4 @@
window.addEventListener('beforeunload', checkSaved);
</script>
{% endblock %}
{% endblock %}
@@ -0,0 +1,12 @@
{% load path_filters %}
<div>
<h3>Listing for {{ collection }}</h3>
<ul>
{% for item in folders %}
<li><a href="{% url 'storage_browser_folder' collection=collection.pk folder=item %}">{{ item|basename }}</a></li>
{% endfor %}
{% for item in files %}
<li>{{ item|basename }}</li>
{% endfor %}
</ul>
</div>
+38 -40
View File
@@ -25,46 +25,46 @@
{% endfor %}
</h3>
<p class="subtitle">{% firstof work.composer "Unattributed" %}{% if work.edition %} - {{ work.edition }}{% endif %}</p>
<section class="block">
<p class="block">{{ work.notes }}</p>
<p class="block">
<table class="table">
<tr>
<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 %}
<a href="{% url 'collection_work_list' work.collection.pk %}?filter={{ meta.name}}:{{ meta.value }}" class="tag" >
{{ meta.get_name_display }}:
{{ meta.value }}
</a>
{% endfor %}
</td>
</tr>
</table>
{% if work.parent %}
<p>From <a href="{% url 'work_detail' work.parent.collection.pk work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a>
</p>
{% endif %}
{% if work.related_works.count %}
<h3>Related</h3>
<ul>
{% for related in work.related_works.all %}
<li><a href="{% url 'work_detail' related.collection.pk related.pk %}">{{ related.name }} - {{ related.composer }}</a></li>
{% endfor %}
</ul>
{% endif %}
</section>
<section class="block">
<div class="columns">
<div class="column is-half">
<p class="block">{{ work.notes }}</p>
<p class="block">
<table class="table">
<tr>
<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 %}
<a href="{% url 'collection_work_list' work.collection.pk %}?filter={{ meta.name}}:{{ meta.value }}" class="tag" >
{{ meta.get_name_display }}:
{{ meta.value }}
</a>
{% endfor %}
</td>
</tr>
</table>
{% if work.parent %}
<p>From <a href="{% url 'work_detail' work.parent.collection.pk work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a>
</p>
{% endif %}
{% if work.related_works.count %}
<h3>Related</h3>
<ul>
{% for related in work.related_works.all %}
<li><a href="{% url 'work_detail' related.collection.pk related.pk %}">{{ related.name }} - {{ related.composer }}</a></li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="column is-half">
<div class="box">
<h4 class="subtitle is-size-4">
@@ -79,8 +79,6 @@
{% endfor %}
</div>
</div>
</div>
<div class="column is-half">
<div class="box">
<h4 class="subtitle is-size-4">
<span class="icon"><i class="fas fa-print"></i></span>
@@ -206,4 +204,4 @@
</script>
{% endblock %}
{% endblock %}
{% endblock %}
@@ -1,10 +1,11 @@
<h3 class="subtitle">{{ work.name }}</h3>
<div class="block">
<div class="block tags">
{% for tag, name in work.digital_parts %}
<a class="tag is-info" href="{% url 'work_download' work.collection_id work.pk %}?tag={{ tag }}">{{ name }}</a>
<a class="tag is-info" href="{% url 'work_download' work.collection_id work.pk %}?tag={{ tag }}" target="polyphonic_parts">{{ name }}</a>
{% endfor %}
</div>
<!--
<h3 class="subtitle">Files</h3>
<table class="table is-narrow is-fullwidth">
<tbody>
@@ -19,6 +20,7 @@
{% endfor %}
</tbody>
</table>
-->
<p>
<a href="{% url 'work_detail' work.collection_id work.pk %}">More details...</a>
+3
View File
@@ -35,10 +35,13 @@ urlpatterns = [
path('collections/<int:collection>/docs/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
path('collections/<int:collection>/download/<int:section>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
path('collections/<int:collection>/browse', views.StorageBrowserView.as_view(), name="storage_browser"),
path('collections/<int:collection>/browse/<path:folder>', views.StorageBrowserView.as_view(), name="storage_browser_folder"),
#path('api/', include(router.urls))
path('api/collections/<int:pk>', api.CollectionExportView.as_view(), name="collection_export"),
path('api/collections/<int:collection>/works/<int:pk>', api.WorkExportView.as_view(), name="work_export"),
path('api/collections/<int:collection>/import', api.WorkImportView.as_view(), name="work_import"),
path('api/collections/<int:collection>/bulk_import', api.CollectionImportView.as_view(), name="collection_import"),
]
+21 -4
View File
@@ -1,4 +1,5 @@
from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.views.generic import TemplateView
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, DeleteView
@@ -18,6 +19,7 @@ import re
from interface.views import EnsembleMixin, ProjectMixin, AuthorizedResourceMixin
from interface.models import Project
from interface.utils import signed_url
from library.models import Collection, Work, Document, Section
from library.music_tags import MUSIC_TAGS, MusicTag, auto_tag
from library import forms, models
@@ -416,9 +418,11 @@ class DocumentDownloadView(DocumentMixin, SingleObjectMixin, View):
self.args = args
self.object = self.get_object()
#response = FileResponse(self.object.upload, content_type="application/pdf")
#return response
return redirect(self.object.upload.url)
if request.GET.get('method') == 'direct':
return redirect(self.object.upload.url)
response = FileResponse(self.object.upload.open('rb'), content_type="application/pdf")
return response
class DocumentAnnotateView(DocumentMixin, DetailView):
template_name = 'library/document_annotate.html'
@@ -446,6 +450,8 @@ class DocumentAnnotateView(DocumentMixin, DetailView):
for part in data['document'].sections.all():
pages.append((part.tag, part.start, part.end))
data['url'] = signed_url('document_download', collection=data['collection'].pk, pk=data['document'].pk)
data['json_data'] = {'pageTags': pages, 'instruments': dict(MUSIC_TAGS)}
return data
@@ -479,4 +485,15 @@ class PartDownloadView(CollectionMixin, SingleObjectMixin, View):
def get_object(self):
return Section.objects.filter(doc__work__collection=self.collection).select_related('doc', 'doc__work').get(pk=self.kwargs['section'])
return Section.objects.filter(doc__work__collection=self.collection).select_related('doc', 'doc__work').get(pk=self.kwargs['section'])
class StorageBrowserView(CollectionMixin, TemplateView):
template_name = "library/storage_browser.html"
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
folder = self.kwargs.get('folder') or data['collection'].prefix
data['folders'], data['files'] = data['collection'].storage.instance().listdir(folder)
return data
+3 -3
View File
@@ -1,7 +1,7 @@
from .default_settings import *
try:
from .local_settings import *
except ImportError:
from .default_settings import *
pass
INSTALLED_APPS += POLYPHONIC_MODULES
INSTALLED_APPS += POLYPHONIC_MODULES
+6 -4
View File
@@ -1,12 +1,11 @@
asgiref==3.4.1
asgiref==3.8.1
boto3==1.18.34
botocore==1.21.34
certifi==2022.12.7
charset-normalizer==2.1.1
crispy-bulma==0.8.0
Django==3.2.7
#-e git+ssh://git@gitea.tfconsulting.com.au/tris/django-byostorage.git@227ce5fccb5864657f080c25528263b655c2b6b7#egg=django_byostorage
git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git
Django==5.1
django-byostorage @ git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@9903bb00888f20dfd2d39754e5ee22eeb5f36298
django-crispy-forms==1.14.0
django-markdown2==0.3.1
django-rest-framework==0.1.0
@@ -14,11 +13,14 @@ django-storages==1.13.1
django_debug_toolbar==3.8.1
djangorestframework==3.14.0
djantic==0.7.0
greenlet==3.0.3
idna==3.4
jmespath==0.10.0
markdown2==2.4.1
msgpack==1.0.8
pydantic==1.10.2
pydantic-django==0.1.1
pynvim==0.5.0
python-dateutil==2.8.2
pytz==2021.1
requests==2.28.1
+30
View File
@@ -0,0 +1,30 @@
[project]
name = "polyphonic"
version = "0.1.0"
description = "Polyphonic Ensemble Manager"
authors = [
{name = "Tris Forster",email = "tris@tfconsulting.com.au"}
]
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"django (==5.1)",
"django-crispy-forms",
"django-markdown2",
"django-rest-framework",
"crispy-bulma (>=0.12.0,<0.13.0)",
"django-byostorage @ git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@9903bb00888f20dfd2d39754e5ee22eeb5f36298",
"requests (>=2.32.5,<3.0.0)",
"django-storages (>=1.14.6,<2.0.0)",
"boto3 (>=1.40.20,<2.0.0)"
]
[tool.poetry]
packages = [{include = "*", from="app"}]
[tool.poetry.group.dev.dependencies]
django-debug-toolbar = "5.2"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"