Library working:

This commit is contained in:
Tris 2021-03-22 10:30:13 +11:00
parent 35555c3321
commit 1581f56b74
32 changed files with 511 additions and 211 deletions

View File

@ -1,6 +1,18 @@
## Polyphonic ## Polyphonic
A simple web app for managing video uploads to an S3 bucket for virtual ensembles. A simple web app managing scores, large files and submissions in a manor tailored to musical ensembles.
No registration required for ensemble participants - just a one time code and passphrase.
### Library App
Store all your scores on your own cloud account (Amazon S3, Google Files etc). Tag up the scores so you can generate
custom part sets and assign them to projects so people can easily print just their parts.
### Submissions App
Accept video/audio submissions direct to your cloud storage. Was developed and used during 2020 lockdown period for
virtual choirs/orchestras but could have more uses.
### S3 Setup ### S3 Setup

View File

@ -7,7 +7,7 @@ class EnsembleAdmin(admin.ModelAdmin):
class ProjectAdmin(admin.ModelAdmin): class ProjectAdmin(admin.ModelAdmin):
list_display = ['name', 'ensemble', 'deadline', 'active', 'slug'] list_display = ['name', 'ensemble', 'event_date', 'active', 'slug']
list_filter = ['ensemble', 'active'] list_filter = ['ensemble', 'active']
class SubmissionAdmin(admin.ModelAdmin): class SubmissionAdmin(admin.ModelAdmin):

View File

@ -0,0 +1,93 @@
# Generated by Django 3.1.1 on 2021-03-21 23:10
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import interface.models
class Migration(migrations.Migration):
replaces = [('interface', '0001_initial'), ('interface', '0002_auto_20200904_1004'), ('interface', '0003_auto_20200905_0118'), ('interface', '0004_auto_20200905_0127'), ('interface', '0005_auto_20200905_0638'), ('interface', '0006_submission_key'), ('interface', '0007_auto_20200906_1009'), ('interface', '0008_auto_20200906_1122'), ('interface', '0009_auto_20200907_0103'), ('interface', '0010_auto_20200907_0148'), ('interface', '0011_auto_20200907_0234'), ('interface', '0012_remove_ensemble_bucket'), ('interface', '0013_auto_20200907_1455'), ('interface', '0014_auto_20200909_1016'), ('interface', '0015_resource_media_type'), ('interface', '0016_auto_20200910_2025'), ('interface', '0017_auto_20200914_0943'), ('interface', '0018_auto_20200914_1009'), ('interface', '0019_project_owner'), ('interface', '0020_auto_20201003_2103'), ('interface', '0021_project_description')]
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Project',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('active', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='WikiPage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('markdown', models.TextField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiki_pages', to='interface.project')),
('title', models.CharField(default='', max_length=255)),
],
),
migrations.CreateModel(
name='Ensemble',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('code', models.CharField(default=interface.models.generate_code, max_length=9)),
('passphrase', models.CharField(max_length=100)),
('admins', models.ManyToManyField(related_name='ensembles', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='project',
name='ensemble',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='interface.ensemble'),
),
migrations.AddField(
model_name='project',
name='deadline',
field=models.DateField(blank=True, null=True),
),
migrations.CreateModel(
name='Resource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('key', models.CharField(blank=True, max_length=255)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resources', to='interface.project')),
('description', models.TextField(blank=True)),
('media_type', models.CharField(choices=[('audio', 'Audio'), ('video', 'Video'), ('general', 'General')], default='*', max_length=10)),
('visible', models.BooleanField(default=True)),
],
),
migrations.AddField(
model_name='project',
name='owner',
field=models.CharField(blank=True, max_length=255),
),
migrations.CreateModel(
name='Submission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('instrument', models.CharField(max_length=100, verbose_name='Instrument / Voice')),
('notes', models.TextField(blank=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_submissions', to='interface.project')),
('date', models.DateTimeField(auto_now_add=True)),
('complete', models.BooleanField(default=False)),
('url', models.CharField(blank=True, max_length=512)),
('private', models.BooleanField(default=False)),
],
),
migrations.AddField(
model_name='project',
name='description',
field=models.TextField(blank=True),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 3.1.1 on 2021-03-12 02:56
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('interface', '0022_auto_20210303_2043'),
]
operations = [
migrations.AddField(
model_name='ensemble',
name='storage',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='byostorage.userstorage'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.1 on 2021-03-12 06:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0023_ensemble_storage'),
]
operations = [
migrations.AddField(
model_name='project',
name='accept_submissions',
field=models.BooleanField(default=False, help_text='Allow media submissions from participants'),
),
migrations.AddField(
model_name='project',
name='has_items',
field=models.BooleanField(default=True, help_text='Enable items to be added from the library'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.1 on 2021-03-12 06:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0024_auto_20210312_1712'),
]
operations = [
migrations.RenameField(
model_name='project',
old_name='has_items',
new_name='enable_library',
),
migrations.RenameField(
model_name='project',
old_name='accept_submissions',
new_name='enable_submissions',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.1 on 2021-03-12 22:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0025_auto_20210312_1713'),
]
operations = [
migrations.RenameField(
model_name='project',
old_name='deadline',
new_name='event_date',
),
]

View File

@ -23,6 +23,18 @@ MEDIA_TYPES = [
('general', "General"), ('general', "General"),
] ]
def rough_date(d):
days = (self.event_date - timezone.now().date()).days
in_past = days < 0
if in_past:
days = abs(days)
if days ==0:
return "today!"
if days >= 7:
return in_past, "{0:d} weeks, {1:d} days".format(days / 7, days % 7)
return in_past, f"{days} days"
def generate_code(length=9): def generate_code(length=9):
return "".join([ random.choice('0123456789') for _ in range(length) ]) return "".join([ random.choice('0123456789') for _ in range(length) ])
@ -32,6 +44,7 @@ class Ensemble(models.Model):
passphrase = models.CharField(max_length=100) passphrase = models.CharField(max_length=100)
admins = models.ManyToManyField('auth.User', related_name='ensembles') admins = models.ManyToManyField('auth.User', related_name='ensembles')
slug = models.SlugField(max_length=100, editable=False) slug = models.SlugField(max_length=100, editable=False)
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL)
def active_projects(self): def active_projects(self):
return self.projects.filter(active=True) return self.projects.filter(active=True)
@ -53,9 +66,11 @@ class Project(models.Model):
ensemble = models.ForeignKey(Ensemble, related_name='projects', on_delete=models.CASCADE, null=True) ensemble = models.ForeignKey(Ensemble, related_name='projects', on_delete=models.CASCADE, null=True)
description = models.TextField(blank=True) description = models.TextField(blank=True)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
deadline =models.DateField(null=True, blank=True) event_date =models.DateField(null=True, blank=True)
owner = models.CharField(max_length=255, blank=True) owner = models.CharField(max_length=255, blank=True)
slug = models.SlugField(max_length=100, editable=False) slug = models.SlugField(max_length=100, editable=False)
enable_library = models.BooleanField(default=True, help_text="Enable items to be added from the library")
enable_submissions = models.BooleanField(default=False, help_text="Allow media submissions from participants")
@property @property
def submissions(self): def submissions(self):
@ -65,6 +80,14 @@ class Project(models.Model):
key = os.path.join(self.slug, object_name) key = os.path.join(self.slug, object_name)
return s3client.generate_presigned_post(BUCKET, key, Fields=fields or {}, Conditions=conditions or [], ExpiresIn=expires) return s3client.generate_presigned_post(BUCKET, key, Fields=fields or {}, Conditions=conditions or [], ExpiresIn=expires)
@property
def days(self):
return (self.event_date - timezone.now().date()).days
@property
def has_happened(self):
return self.event_date < timezone.now()
def save(self): def save(self):
if not self.slug: if not self.slug:
self.slug = slugify(self.name) self.slug = slugify(self.name)

View File

@ -313,7 +313,8 @@ TD.select-cell {
} }
DIV.selected { DIV.selected {
background-color: #EEE; background-color: var(--gray-blue);
color: white;
border: 1px solid #999; border: 1px solid #999;
border-radius: 5px; border-radius: 5px;
} }

View File

@ -5,6 +5,7 @@
<!-- Required meta tags --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" type="image/png" href="{% static 'interface/icon.png' %}" />
<link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link> <link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link>
<title>{% block title %}Polyphonic{% endblock %}</title> <title>{% block title %}Polyphonic{% endblock %}</title>

View File

@ -1,11 +1,11 @@
{% extends "base.html" %} {% extends "interface/project_base.html" %}
{% block content %} {% block page %}
<div style="flex-grow: 1"> <div style="flex-grow: 1">
<h1>Projects for {{ ensemble.name }}</h1> <h1>Projects for {{ ensemble.name }}</h1>
<div class="list-group narrow"> <div class="list-group narrow">
{% for project in ensemble.active_projects %} {% for project in ensemble.active_projects %}
<a class="" href="{% url 'project_detail' pk=project.id %}"> <a class="" href="{% url 'project_detail' project=project.id %}">
<h3>{{ project.name }}</h3> <h3>{{ project.name }}</h3>
<p><small> <p><small>
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %} {% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
@ -15,7 +15,7 @@
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
<div style="text-align: right; margin-top: 10px; color: #999;"> <div style="float: right; margin-top: 10px; color: #999;">
<small>{{ ensemble.ensemble_code }}</small> <small>{{ ensemble.ensemble_code }}</small>
</div> </div>
</div> </div>

View File

@ -10,25 +10,32 @@
{% endif %} {% endif %}
<h1>{{ project.name }}</h1> <h1>{{ project.name }}</h1>
{% block page %} {% block page %}
No content No content
{% endblock %} {% endblock %}
{% if project %}
<div class="project-links"> <div class="project-links">
<div class="pills" role="tablist"> <div class="pills" role="tablist">
<a role="tab" href="{% url 'project_detail' pk=project.id %}">Project info</a>
{% if project %}
<a role="tab" href="{% url 'project_detail' project=project.id %}">Project info</a>
{% for page in project.wiki_pages.all %} {% for page in project.wiki_pages.all %}
<a class="nav-link {% if page.id == wiki_id %}active{% endif %}" <a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a> href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a>
{% endfor %} {% endfor %}
{% endif %}
{% if project and project.enable_submissions%}
<a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a> <a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a>
<!--a role="tab" href="">Record a submission</a--> <!--a role="tab" href="">Record a submission</a-->
{% if request.is_admin %} {% if request.is_admin %}
<a role="tab" href="{% url 'submission_list' project=project.id %}">Submissions</a> <a role="tab" href="{% url 'submission_list' project=project.id %}">Submissions</a>
{% endif %} {% endif %}
<a role="tab" href="{% url 'submission_create' project=project.id %}">Send a file</a> <a role="tab" href="{% url 'submission_create' project=project.id %}">Send a file</a>
{% endif %}
{% include "library/project_menu.html" %} {% include "library/project_menu.html" %}
</div> </div>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -2,30 +2,25 @@
{% load md2 %} {% load md2 %}
{% block page %} {% block page %}
<div class="narrow"> <div class="narrow">
{% if project.deadline %} {% if project.event_date %}
<h3 class="text-center">In {{ project.deadline|timeuntil }}</h3> <h3 class="text-center">{{ project.when }}</h3>
{% endif %} {% endif %}
<p>{{ project.description|markdown }}</p>
<p>{{ project.description|markdown }}</p>
{% if project.owner %} {% if project.owner %}
<p>Project email: <a href="mailto:{{ project.owner }}">{{ project.owner }}</a></p> <p>Project email: <a href="mailto:{{ project.owner }}">{{ project.owner }}</a></p>
{% endif %} {% endif %}
{% with sub_count=project.submissions.count %}
<p>There have been {{ sub_count }} submission{{ sub_count|pluralize }} so far...</p> {% if project.enable_submissions %}
{% if sub_count %} {% include 'submissions/project_detail.html' %}
<h4>Recent submissions</h4>
<table style="width: 100%">
<tbody">
{% for submission in project.submissions|slice:":5" %}
<tr>
<td>{{ submission.date|timesince }} ago</td>
<td>{{ submission.name }} ({{ submission.instrument }})</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %} {% endif %}
{% endwith %}
{% if project.enable_library %}
{% include 'library/project_detail.html' %}
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,9 @@
from django import template
import os.path
register = template.Library()
def basename(value):
return os.path.basename(value)
register.filter('basename', basename)

View File

@ -11,8 +11,8 @@ urlpatterns = [
path('manage', views.ManageView.as_view(), name="manage"), path('manage', views.ManageView.as_view(), name="manage"),
path('', views.EnsembleDetailView.as_view(), name='ensemble_detail'), path('', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
path('projects/<int:pk>', views.ProjectDetailView.as_view(), name="project_detail"), path('projects/<int:project>', views.ProjectDetailView.as_view(), name="project_detail"),
path('projects/<int:pk>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"), path('projects/<int:project>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"),
path('projects/<int:project>/page/<int:pk>', views.WikiView.as_view(), name="wiki"), path('projects/<int:project>/page/<int:pk>', views.WikiView.as_view(), name="wiki"),
path('projects/<int:project>/page/<int:pk>/edit', views.WikiEditView.as_view(), name="wiki_edit"), path('projects/<int:project>/page/<int:pk>/edit', views.WikiEditView.as_view(), name="wiki_edit"),

View File

@ -207,10 +207,10 @@ class EnsembleDetailView(EnsembleMixin, DetailView):
def get_object(self): def get_object(self):
return models.Ensemble.objects.get(pk=self.request.ensemble_id) return models.Ensemble.objects.get(pk=self.request.ensemble_id)
class ProjectDetailView(EnsembleMixin, DetailView): class ProjectDetailView(ProjectMixin, DetailView):
def get_queryset(self): def get_object(self):
return models.Project.objects.filter(ensemble=self.request.ensemble_id) return self.get_project()
class ProjectMakefileView(EnsembleMixin, DetailView): class ProjectMakefileView(EnsembleMixin, DetailView):
template_name = 'interface/project_submissions.mk' template_name = 'interface/project_submissions.mk'

View File

@ -2,12 +2,17 @@ from django.contrib import admin
from . import models from . import models
class PlaylistInline(admin.TabularInline): class OrchestrationAdmin(admin.ModelAdmin):
model = models.Playlist list_display = ['name', 'ensemble']
list_filter = ['ensemble']
class ItemInline(admin.TabularInline):
model = models.Item
class WorkAdmin(admin.ModelAdmin): class WorkAdmin(admin.ModelAdmin):
list_display = ['name', 'orchestration'] list_display = ['name', 'orchestration']
inlines = [PlaylistInline] list_filter = ['ensemble']
inlines = [ItemInline]
class PartInline(admin.TabularInline): class PartInline(admin.TabularInline):
model = models.Part model = models.Part
@ -15,12 +20,14 @@ class PartInline(admin.TabularInline):
class DocumentAdmin(admin.ModelAdmin): class DocumentAdmin(admin.ModelAdmin):
list_display = ['work', '__str__'] list_display = ['work', '__str__']
list_filter = ['work__ensemble']
inlines = [PartInline] inlines = [PartInline]
class PlaylistAdmin(admin.ModelAdmin): class ItemAdmin(admin.ModelAdmin):
list_display = ['project', 'work', 'order'] list_display = ['project', 'work', 'order']
list_filter = ['project'] list_filter = ['project']
admin.site.register(models.Orchestration, OrchestrationAdmin)
admin.site.register(models.Work, WorkAdmin) admin.site.register(models.Work, WorkAdmin)
admin.site.register(models.Document, DocumentAdmin) admin.site.register(models.Document, DocumentAdmin)
admin.site.register(models.Playlist, PlaylistAdmin) admin.site.register(models.Item, ItemAdmin)

View File

@ -1,9 +1,9 @@
# Generated by Django 3.1.1 on 2021-03-10 22:20 # Generated by Django 3.1.1 on 2021-03-11 07:07
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import library.models import library.models
import library.storage import byostorage.cached
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -19,11 +19,11 @@ class Migration(migrations.Migration):
name='Document', name='Document',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('upload', models.FileField(storage=library.storage.RemoteCachedStorage(), upload_to=library.models.upload_filename)), ('upload', models.FileField(storage=byostorage.cached.CachedStorage(), upload_to=library.models.doc_upload_filename)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Playlist', name='Item',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.SmallIntegerField(default=0)), ('order', models.SmallIntegerField(default=0)),
@ -33,27 +33,31 @@ class Migration(migrations.Migration):
'ordering': ['order', 'work'], 'ordering': ['order', 'work'],
}, },
), ),
migrations.CreateModel(
name='Orchestration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('instruments', models.TextField()),
('ensemble', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='orchestrations', to='interface.ensemble')),
],
),
migrations.CreateModel( migrations.CreateModel(
name='Work', name='Work',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(editable=False, max_length=100)), ('slug', models.SlugField(editable=False, max_length=100)),
('name', models.CharField(max_length=255)), ('name', models.CharField(max_length=255)),
('orchestration', models.CharField(choices=[('SATB', 'SATB'), ('String Quartet', 'String Quartet'), ('Chamber Orchestra', 'Chamber Orchestra'), ('RWE', 'RWE'), ('Custom', 'Custom')], max_length=100)),
('running_time', models.IntegerField(blank=True, null=True)), ('running_time', models.IntegerField(blank=True, null=True)),
('notes', models.TextField(blank=True)), ('notes', models.TextField(blank=True)),
('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='interface.ensemble')), ('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='interface.ensemble')),
('projects', models.ManyToManyField(related_name='works', through='library.Playlist', to='interface.Project')), ('orchestration', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.orchestration')),
('projects', models.ManyToManyField(related_name='works', through='library.Item', to='interface.Project')),
], ],
options={ options={
'unique_together': {('ensemble', 'slug')}, 'unique_together': {('ensemble', 'slug')},
}, },
), ),
migrations.AddField(
model_name='playlist',
name='work',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.work'),
),
migrations.CreateModel( migrations.CreateModel(
name='Part', name='Part',
fields=[ fields=[
@ -68,6 +72,11 @@ class Migration(migrations.Migration):
'ordering': ['doc', 'start', 'pk'], 'ordering': ['doc', 'start', 'pk'],
}, },
), ),
migrations.AddField(
model_name='item',
name='work',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.work'),
),
migrations.AddField( migrations.AddField(
model_name='document', model_name='document',
name='work', name='work',

View File

@ -0,0 +1,27 @@
# Generated by Django 3.1.1 on 2021-03-11 06:26
from django.db import migrations
ORCHESTRATIONS = {
'SATB': ('S', 'A', 'T', 'B'),
'String Quartet': ('Vln1', 'Vln2', 'Vla', 'Vc'),
'Chamber Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb',
'Fl1', 'Fl2', 'Cl1', 'Cl2', 'Ob1', 'Ob2', 'Hn1', 'Hn2',
'Tpt1', 'Tpt2', 'Tbn1', 'Tbn2', 'Tuba',
'Timp', 'Drum', 'Perc'),
}
def add_orchestrations(apps, schema_editor):
q = apps.get_model('library', 'Orchestration').objects
for name, instruments in ORCHESTRATIONS.items():
q.create(name=name, instruments=", ".join(instruments))
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.RunPython(add_orchestrations, lambda apps, schema_editor: None),
]

View File

@ -34,7 +34,7 @@ INSTRUMENTS = [
('Tbn', 'Trombone'), ('Tbn', 'Trombone'),
('Tuba', 'Tuba'), ('Tuba', 'Tuba'),
('Timp', 'Timpani'), ('Timp', 'Timpani'),
('Drum', 'Drupset'), ('Drum', 'Drumset'),
('Perc', 'Percussion'), ('Perc', 'Percussion'),
('Pno', 'Piano'), ('Pno', 'Piano'),
('Hp', 'Harp'), ('Hp', 'Harp'),
@ -58,14 +58,30 @@ def tag_to_instrument(tag):
l = m.groups() l = m.groups()
return "{0} {1}".format(dict(INSTRUMENTS).get(l[0],l[0]), l[1]).strip() return "{0} {1}".format(dict(INSTRUMENTS).get(l[0],l[0]), l[1]).strip()
class Orchestration(models.Model):
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="orchestrations", null=True, blank=True)
name = models.CharField(max_length=100)
instruments = models.TextField()
def as_list(self):
tags = [ t.strip() for t in self.instruments.split(',') ]
return [ (t, tag_to_instrument(t)) for t in tags if t ]
def save(self):
self.as_list()
super(Orchestration, self).save()
def __str__(self):
return self.name
class Work(models.Model): class Work(models.Model):
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="works") ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="works")
slug = models.SlugField(max_length=100, editable=False) slug = models.SlugField(max_length=100, editable=False)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
orchestration = models.CharField(max_length=100, choices=[ (k, k) for k, v in ORCHESTRATIONS.items() ]) orchestration = models.ForeignKey(Orchestration, null=True, on_delete=models.SET_NULL, related_name='works')
running_time = models.IntegerField(null=True, blank=True) running_time = models.IntegerField(null=True, blank=True)
notes = models.TextField(blank=True) notes = models.TextField(blank=True)
projects = models.ManyToManyField('interface.Project', through='Playlist', related_name="works") projects = models.ManyToManyField('interface.Project', through='Item', related_name="works")
class Meta: class Meta:
unique_together = ['ensemble', 'slug'] unique_together = ['ensemble', 'slug']
@ -76,8 +92,7 @@ class Work(models.Model):
@property @property
def instruments(self): def instruments(self):
tags = ORCHESTRATIONS.get(self.orchestration, 'Custom') return self.orchestration.as_list()
return [ (tag, tag_to_instrument(tag)) for tag in tags ]
def save(self): def save(self):
if not self.slug: if not self.slug:
@ -87,7 +102,11 @@ class Work(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class Playlist(models.Model): class Item(models.Model):
"""
Item represents a Work in a Project e.g. item in set list or programme
It also allows works to be shared from one ensemble to another on a per-project basis.
"""
project = models.ForeignKey('interface.Project', on_delete=models.CASCADE) project = models.ForeignKey('interface.Project', on_delete=models.CASCADE)
work = models.ForeignKey('Work', on_delete=models.CASCADE) work = models.ForeignKey('Work', on_delete=models.CASCADE)
order = models.SmallIntegerField(default=0) order = models.SmallIntegerField(default=0)
@ -98,12 +117,15 @@ class Playlist(models.Model):
def __str__(self): def __str__(self):
return f"<{self.project.slug}:{self.work.slug}>" return f"<{self.project.slug}:{self.work.slug}>"
def upload_filename(instance, filename): def doc_upload_filename(doc, filename):
return f'{instance.work.ensemble.slug}/works/{instance.work.slug}/{filename}' storage = doc.work.ensemble.storage
if not storage:
raise RuntimeError("Storage not set")
return f'{storage}:{doc.work.ensemble.slug}/works/{doc.work.slug}/{filename}'
class Document(models.Model): class Document(models.Model):
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs") work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs")
upload = models.FileField(upload_to=upload_filename, storage=library_storage) upload = models.FileField(upload_to=doc_upload_filename, storage=library_storage)
def __str__(self): def __str__(self):
return self.upload.name return self.upload.name

View File

@ -1,19 +1,28 @@
import tempfile import tempfile
import subprocess import subprocess
import os.path import os.path
import string
def extract_pages(source, bookmark, start=None, end=None): SAFECHARS = string.ascii_letters + string.digits + " _-"
return extract_and_concat([(source, bookmark, start, end)]) def extract_pages(source, bookmark, start=None, end=None, count=1):
return extract_and_concat([(source, bookmark, start, end, count)])
def extract_and_concat(items): def extract_and_concat(items):
# create a temporary directory for our sections # create a temporary directory for our sections
d = tempfile.TemporaryDirectory(prefix="polyphonic_") d = tempfile.TemporaryDirectory(prefix="polyphonic_")
pdfmarks = os.path.join(d.name, 'pdfmarks.txt')
marks = open(pdfmarks, 'w')
sections = [] sections = []
current_page = 1
for i, (source, bookmark, start, end) in enumerate(items): for i, (source, bookmark, start, end, count) in enumerate(items):
if count == 0:
continue
if start is None: if start is None:
sections.append(source) sections.append(source)
@ -25,13 +34,22 @@ def extract_and_concat(items):
dest = os.path.join(d.name, f'section_{i}.pdf') dest = os.path.join(d.name, f'section_{i}.pdf')
cmd = ['gs', '-sDEVICE=pdfwrite', '-q', '-dBATCH', '-dNOPAUSE', cmd = ['gs', '-sDEVICE=pdfwrite', '-dBATCH', '-dNOPAUSE',
f'-dFirstPage={start}', f'-dLastPage={end}', f'-dFirstPage={start}', f'-dLastPage={end}',
f'-sOutputFile={dest}', f'-sOutputFile={dest}',
source] source]
subprocess.run(cmd, check=True) bookmark = "".join(filter(lambda c: c in SAFECHARS, bookmark))
sections.append(dest)
marks.write(f'[/Title ({bookmark}) /Page {current_page} /OUT pdfmark\n')
p = subprocess.run(cmd, check=True, capture_output=True)
pages = len([ x for x in p.stdout.splitlines() if x.decode('utf8').startswith('Page ')])
for j in range(count):
sections.append(dest)
current_page += pages
marks.close()
# concat the items # concat the items
output = tempfile.NamedTemporaryFile(prefix="polyphonic_", suffix='.pdf') output = tempfile.NamedTemporaryFile(prefix="polyphonic_", suffix='.pdf')
@ -39,6 +57,7 @@ def extract_and_concat(items):
cmd = ['gs', '-sDEVICE=pdfwrite', '-q', '-dBATCH', '-dNOPAUSE', cmd = ['gs', '-sDEVICE=pdfwrite', '-q', '-dBATCH', '-dNOPAUSE',
'-sOutputFile=-'] '-sOutputFile=-']
cmd.extend(sections) cmd.extend(sections)
cmd.append(pdfmarks)
subprocess.run(cmd, stdout=output) subprocess.run(cmd, stdout=output)

View File

@ -1,96 +0,0 @@
from django.core.files.storage import Storage, get_storage_class
from django.utils.deconstruct import deconstructible
from django.conf import settings
from hashlib import sha1
import os.path
import shutil
import tempfile
import time
import logging
logger = logging.getLogger(__name__)
@deconstructible
class RemoteCachedStorage(Storage):
CACHE_EXPIRES = 30
def __init__(self, remote=None, cachedir=None):
if not remote:
remote = settings.CACHED_STORAGE_REMOTE
if not cachedir:
cachedir = settings.CACHED_STORAGE_DIR
self.remote = get_storage_class(remote)()
self.cachedir = cachedir
os.makedirs(self.cachedir, exist_ok=True)
self.clean()
def _filepath(self, name):
base, ext = os.path.splitext(name)
filename = sha1(base.encode('utf8')).hexdigest() + ext
return os.path.join(self.cachedir, filename)
def _cached(self, name):
p = self._filepath(name)
if not os.path.exists(p):
logger.debug("Caching %s to %s", name, p)
source = self.remote._open(name, 'rb')
dest = tempfile.NamedTemporaryFile(dir=self.cachedir, delete=False, prefix="_")
shutil.copyfileobj(source, dest)
source.close()
dest.close()
os.rename(dest.name, p)
now = time.time()
os.utime(p, (now, now))
if now > self.next_check:
self.clean() # wont get this file as we just touched it
return p
def _open(self, name, mode='rb'):
p = self._cached(name)
return open(p, mode)
def path(self, name):
return self._cached(name)
def _save(self, name, content):
return self.remote._save(name, content)
def delete(self, name):
return self.remote.delete(name)
def exists(self, name):
return self.remote.exists(name)
def listdir(self, name):
return self.remote.listdir(name)
def size(self, name):
return self.remote.size(name)
def url(self, name):
return self.remote.url(name)
def get_valid_name(self, name):
return self.remote.get_valid_name(name)
def get_available_name(self, name, max_length=None):
return self.remote.get_available_name(name, max_length)
def get_alternative_name(self, file_root, file_ext):
return self.remote.get_alternative_name(file_root, file_ext)
def clean(self):
now = time.time()
threshold = now - self.CACHE_EXPIRES
logger.info("Removing cached files older than %d seconds", self.CACHE_EXPIRES)
for f in os.listdir(self.cachedir):
f = os.path.join(self.cachedir, f)
s = os.stat(f)
if s.st_atime < threshold:
logger.debug("Removing %s", f)
os.unlink(f)
self.next_check = now + 300

View File

@ -1,27 +1,37 @@
{% extends "interface/project_base.html" %} {% extends "interface/project_base.html" %}
{% block admin %} {% block admin %}
<a href="{% url 'playlist_manage' project.pk %}"><i class="fas fa-list"></i><span class="smhide admin">Change works</span></a> <a href="{% url 'item_list_manage' project.pk %}"><i class="fas fa-list"></i><span class="smhide admin">Change items</span></a>
{% endblock %} {% endblock %}
{% block page %} {% block page %}
<p> <p>
This page lets you download a complete set of music for your part by selecting your instrument This page lets you download a complete set of music for your part by selecting your instrument
and optionally a part (e.g. Flute 1 or 2).<br/> and optionally a part (e.g. Flute 1 or 2).<br/>
You can also tweak which parts you get in the list or click on a piece for more download options. You can also tweak which parts you get for each piece, or click on a piece for more download options.
</p> </p>
<form action="" method="post" target="_blank" style="display: flex;"> <form action="" method="post" target="_blank" style="display: flex;">
{% csrf_token %} {% csrf_token %}
<table style="max-width: 600px; margin: 10pt auto;" class="item-table"> <div style="margin: 0px 20px; text-align: right;">
<thead> <label for="instrument-select">Pick your instrument</label>
</thead> <select id="instrument-select" name="instrument" onchange="updateParts()">
{% for tag, instrument in instruments %}
<option value="{{ tag }}">{{ instrument }}</option>
{% endfor %}
</select><br/><br/>
<label for="part-preference">Part preference</label>
<input id="part-preference" name="part" type="number" value="0" min="0" max="4" onchange="updateParts()" size="1"/><br/><br/>
<button type="submit" style="height: 32px;"><i class="fas fa-copy"></i> Get My Parts!</button>
</div>
<table style="max-width: 600px; margin: 10pt auto;" class="">
<tbody> <tbody>
{% for item in object_list %} {% for item in object_list %}
<tr> <tr>
<td>{{ forloop.counter }})</td>
<td> <td>
<a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a> <a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a>
</td> </td>
<td class="select-cell"> <td class="">
<input type="hidden" name="works" value="{{ item.work.pk }}"/> <input type="hidden" name="works" value="{{ item.work.pk }}"/>
<select name="instruments"> <select name="instruments">
<option value='-'>None</option> <option value='-'>None</option>
@ -34,17 +44,6 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<div style="margin: 0px 20px; text-align: right;">
<label for="instrument-select">Pick your instrument</label>
<select id="instrument-select" name="instrument" onchange="updateParts()">
{% for tag, instrument in instruments %}
<option value="{{ tag }}">{{ instrument }}</option>
{% endfor %}
</select><br/><br/>
<label for="part-preference">Part preference</label>
<input id="part-preference" name="part" type="number" value="0" min="0" max="4" onchange="updateParts()" size="1"/><br/><br/>
<button type="submit" style="height: 32px;"><i class="fas fa-copy"></i> Get My Parts!</button>
</div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -2,28 +2,31 @@
{% block admin %} {% block admin %}
<a href="#" onclick="save()"><i class="fas fa-save"></i><span class="smhide action">Save</span></a> <a href="#" onclick="save()"><i class="fas fa-save"></i><span class="smhide action">Save</span></a>
<a href="{% url 'playlist_append' project.pk %}"><i class="fas fa-plus-circle"></i><span class="smhide action">Add</a></a> <a href="{% url 'item_list_append' project.pk %}"><i class="fas fa-plus-circle"></i><span class="smhide action">Add</a></a>
{% endblock %} {% endblock %}
{% block page %} {% block page %}
<table style="max-width: 600px; margin: 10pt auto;" class="item-table"> <table style="max-width: 600px; margin: 10pt auto;" class="item-table">
<thead> <thead>
<tr> <tr>
<th>Piece</th> <th>Item</th>
<th>Part</th> <th>Time</th>
</tr> </tr>
</thead> </thead>
<tbody id="work-list"> <tbody id="work-list">
{% for item in object_list %} {% for item in object_list %}
<tr data-pk="{{ item.pk }}" data-order="{{ forloop.counter }}"> <tr data-pk="{{ item.pk }}" data-order="{{ forloop.counter }}">
<td>{{ item.work.name }}</td> <td>{{ item.work.name }}</td>
<td>{% firstof item.work.running_time '?' %}</td>
<td style="text-align: center;"> <td style="text-align: center;">
<i class="fas fa-arrow-up clickable" title="Move up" onclick="moveItem({{ item.pk }}, -1)"></i> <i class="fas fa-arrow-up clickable" title="Move up" onclick="moveItem({{ item.pk }}, -1)"></i>
<i class="fas fa-arrow-down clickable" title="Move down" onclick="moveItem({{ item.pk }}, 1)"></i> <i class="fas fa-arrow-down clickable" title="Move down" onclick="moveItem({{ item.pk }}, 1)"></i>
<i class="fas fa-trash clickable" title="Remove"></i> <i class="fas fa-trash clickable" title="Remove" onClick="moveItem({{ item.pk }}, 0)"></i>
</td> </td>
</tr> </tr>
{% empty %}
<tr><td colspan="2">No items</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@ -47,8 +50,13 @@ function moveItem(pk, dir) {
// check the direction is sensible // check the direction is sensible
if (i + dir < 0 || i + dir >= items.length) return; if (i + dir < 0 || i + dir >= items.length) return;
items[i].dataset.order = parseInt(items[i].dataset.order) + dir; if (dir === 0) {
items[i+dir].dataset.order = parseInt(items[i+dir].dataset.order) - dir; items[i].dataset.order = -1;
items[i].style = "display: none";
} else {
items[i].dataset.order = parseInt(items[i].dataset.order) + dir;
items[i+dir].dataset.order = parseInt(items[i+dir].dataset.order) - dir;
}
items.sort((a, b) => parseInt(a.dataset.order) - parseInt(b.dataset.order)) items.sort((a, b) => parseInt(a.dataset.order) - parseInt(b.dataset.order))
@ -76,7 +84,7 @@ function save() {
}).then((response) => { }).then((response) => {
if (response.ok) { if (response.ok) {
dirty=false; dirty=false;
window.location = "{% url 'project_playlist' project=project.pk %}"; window.location = "{% url 'item_list' project=project.pk %}";
} else { } else {
alert("Failed: " + response.statusText) alert("Failed: " + response.statusText)
} }

View File

@ -0,0 +1 @@
<p>Click the '<a href="{% url 'item_list' project=project.pk %}">My Music</a>' button below to access your parts...</p>

View File

@ -1 +1,6 @@
<a href="{% url 'project_playlist' project=project.pk %}">My Music</a> {% if project %}
<a href="{% url 'item_list' project=project.pk %}">My Music</a>
{% endif %}
{% if request.is_admin %}
<a href="{% url 'work_list' %}">Works</a>
{% endif %}

View File

@ -1,4 +1,5 @@
{% extends 'interface/project_base.html' %} {% extends 'interface/project_base.html' %}
{% load path_filters %}
{% block admin %} {% block admin %}
<a href="{% url 'document_add' work.pk %}"><i class="fas fa-plus-circle"></i> Upload file</a> <a href="{% url 'document_add' work.pk %}"><i class="fas fa-plus-circle"></i> Upload file</a>
@ -13,14 +14,17 @@
<li><a href="{% url 'part_download' pk=part.pk filename=part.filename %}" target="part_{{ part.pk }}" rel="">{{ part.instrument }}</a></li> <li><a href="{% url 'part_download' pk=part.pk filename=part.filename %}" target="part_{{ part.pk }}" rel="">{{ part.instrument }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
<a href="{% url 'work_partset' pk=work.pk %}"><i class="fas fa-print"></i> Print part set</a>
<h3>Original Documents</h3> <h3>Original Documents</h3>
<ul> <ul>
{% for doc in work.docs.all %} {% for doc in work.docs.all %}
<li> <li>
<a href="{% url 'document_download' pk=doc.pk %}" target="_blank">{{ doc.upload.name }}</a> [{{ doc.parts.count }} parts] <a href="{% url 'document_download' pk=doc.pk %}" target="_blank">{{ doc.upload.name|basename }}</a>
&nbsp;
[{{ doc.parts.count }} parts]
{% if request.is_admin %} {% if request.is_admin %}
<a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-edit"></i></a> <a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-tags"></i></a>
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}

View File

@ -0,0 +1,31 @@
{% extends "interface/project_base.html" %}
{% block page %}
<h2><a href="{% url 'work_list' %}">Works</a> / <a href="{% url 'work_detail' pk=work.pk %}">{{ work.name }}</a></h2>
<form action="" method="post" target="_blank">
{% csrf_token %}
<table style="margin: 0px auto">
<thead>
<tr>
<th>Part</th>
<th>Copies</th>
</tr>
</thead>
<tbody>
{% for part in work.parts %}
<tr>
<td>{{ part.instrument }}</td>
<td>
<input name="parts" type="hidden" value="{{ part.tag }}">
<input name="copies" type="number" value="{% if part.tag == 'Score' %}0{% else %}1{% endif %}" size="1">
</td>
</tr>
{% endfor %}
<tr>
<td colspan="2" style="text-align: center;"><button type="submit"><i class="fas fa-print"></i> Print</button></td>
</tr>
</tbody>
</table>
</form>
{% endblock %}

View File

@ -5,13 +5,14 @@ from . import views
urlpatterns = [ urlpatterns = [
path('projects/<int:project>/works', views.PlaylistView.as_view(), name="project_playlist"), path('projects/<int:project>/items', views.ProjectItemListView.as_view(), name="item_list"),
path('projects/<int:project>/works/manage', views.PlaylistManageView.as_view(), name="playlist_manage"), path('projects/<int:project>/items/manage', views.ProjectItemManageView.as_view(), name="item_list_manage"),
path('projects/<int:project>/works/append', views.PlaylistAddView.as_view(), name="playlist_append"), path('projects/<int:project>/items/append', views.ProjectItemAddView.as_view(), name="item_list_append"),
path('library/works', views.WorkListView.as_view(), name="work_list"), path('library/works', views.WorkListView.as_view(), name="work_list"),
path('library/works/add', views.WorkAddView.as_view(), name="work_add"), path('library/works/create', views.WorkAddView.as_view(), name="work_add"),
path('library/works/<int:pk>', views.WorkDetailView.as_view(), name="work_detail"), path('library/works/<int:pk>', views.WorkDetailView.as_view(), name="work_detail"),
path('library/works/<int:pk>/partset', views.WorkPartSetView.as_view(), name="work_partset"),
path('library/works/<int:pk>/upload', views.DocumentAddView.as_view(), name="document_add"), path('library/works/<int:pk>/upload', views.DocumentAddView.as_view(), name="document_add"),
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"),

View File

@ -1,6 +1,6 @@
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 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
from django.http import FileResponse, HttpResponse from django.http import FileResponse, HttpResponse
from django.db import IntegrityError from django.db import IntegrityError
@ -9,13 +9,13 @@ import json
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 Work, Document, Part, Playlist, INSTRUMENTS from .models import Work, Document, Part, INSTRUMENTS
from . import forms from . import forms, models
from .pdf_utils import extract_pages, extract_and_concat from .pdf_utils import extract_pages, extract_and_concat
class PlaylistView(ProjectMixin, ListView): class ProjectItemListView(ProjectMixin, ListView):
template_name = "library/playlist_view.html" template_name = "library/item_list.html"
model = Playlist model = models.Item
def post(self, request, **kwargs): def post(self, request, **kwargs):
@ -43,7 +43,7 @@ class PlaylistView(ProjectMixin, ListView):
continue continue
part = Part.objects.filter(tag=tag, doc__work=pk).select_related('doc').get() part = Part.objects.filter(tag=tag, doc__work=pk).select_related('doc').get()
sections.append((part.doc.upload.path, part.doc.work.name, part.start, part.end)) sections.append((part.doc.upload.path, part.doc.work.name, part.start, part.end, 1))
result = extract_and_concat(sections) result = extract_and_concat(sections)
@ -55,18 +55,18 @@ class PlaylistView(ProjectMixin, ListView):
def get_queryset(self): def get_queryset(self):
return super(PlaylistView, self).get_queryset().select_related('project', 'work') return super(ProjectItemListView, self).get_queryset().select_related('project', 'work')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super(PlaylistView, self).get_context_data(**kwargs) data = super(ProjectItemListView, self).get_context_data(**kwargs)
data['instruments'] = INSTRUMENTS data['instruments'] = INSTRUMENTS
data['instrument'] = self.request.session.get('instrument', 'Score') data['instrument'] = self.request.session.get('instrument', 'Score')
data['part'] = self.request.session.get('part', '0') data['part'] = self.request.session.get('part', '0')
return data return data
class PlaylistManageView(ProjectMixin, ListView): class ProjectItemManageView(ProjectMixin, ListView):
template_name = "library/playlist_manage.html" template_name = "library/item_list_manage.html"
model = Playlist model = models.Item
def post(self, request, **kwargs): def post(self, request, **kwargs):
self.request = request self.request = request
@ -76,19 +76,23 @@ class PlaylistManageView(ProjectMixin, ListView):
q = self.get_queryset() q = self.get_queryset()
for pk, order in data.items(): for pk, order in data.items():
i = q.filter(pk=pk).update(order=order) order = int(order)
if order == -1:
q.filter(pk=pk).delete()
else:
i = q.filter(pk=pk).update(order=order)
return HttpResponse(status=204) return HttpResponse(status=204)
def get_queryset(self): def get_queryset(self):
return super(PlaylistManageView, self).get_queryset().select_related('project', 'work') return super(ProjectItemManageView, self).get_queryset().select_related('project', 'work')
class PlaylistAddView(ProjectMixin, UpdateView): class ProjectItemAddView(ProjectMixin, UpdateView):
form_class = forms.PlaylistAddForm form_class = forms.PlaylistAddForm
template_name = "interface/default_form.html" template_name = "interface/default_form.html"
def get_success_url(self): def get_success_url(self):
return resolve_url('playlist_manage', project=self.kwargs['project']) return resolve_url('item_list_manage', project=self.kwargs['project'])
def get_object(self): def get_object(self):
return self.get_project() return self.get_project()
@ -102,6 +106,12 @@ class WorkAddView(EnsembleMixin, FormView):
template_name = "interface/default_form.html" template_name = "interface/default_form.html"
form_class = forms.WorkCreateForm form_class = forms.WorkCreateForm
def get_form(self):
f = super(WorkAddView, self).get_form()
qs = f.fields['orchestration'].queryset
f.fields['orchestration'].queryset = qs.filter(ensemble_id__isnull=True) | qs.filter(ensemble_id=self.request.ensemble_id)
return f
def form_valid(self, form): def form_valid(self, form):
obj = form.save(commit=False) obj = form.save(commit=False)
obj.ensemble_id = self.request.ensemble_id obj.ensemble_id = self.request.ensemble_id
@ -127,6 +137,32 @@ class WorkDetailView(EnsembleMixin, DetailView):
def get_queryset(self): def get_queryset(self):
return Work.objects.filter(ensemble=self.request.ensemble_id) return Work.objects.filter(ensemble=self.request.ensemble_id)
class WorkPartSetView(EnsembleMixin, DetailView):
template_name = "library/work_partset.html"
def post(self, request, *args, **kwargs):
work = self.get_object()
parts = request.POST.getlist('parts')
copies = request.POST.getlist('copies')
sections = []
for i, tag in enumerate(parts):
part = work.parts.select_related('doc').get(tag=tag)
sections.append((part.doc.upload.path, part.instrument, part.start, part.end, int(copies[i])))
result = extract_and_concat(sections)
download_name = f'{work.name}.pdf'
response = FileResponse(result, content_type="application/pdf")
response['Content-Disposition'] = f'inline; filename="{download_name}"'
return response
def get_queryset(self):
return Work.objects.filter(ensemble_id=self.request.ensemble_id)
class DocumentDetailView(EnsembleMixin, DetailView): class DocumentDetailView(EnsembleMixin, DetailView):
@ -198,6 +234,7 @@ class DocumentAnnotateView(EnsembleMixin, DetailView):
def get_queryset(self): def get_queryset(self):
return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work') return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work')
class PartDownloadView(EnsembleMixin, SingleObjectMixin, View): class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
def get(self, request, **args): def get(self, request, **args):

View File

@ -38,6 +38,7 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_markdown2', 'django_markdown2',
'byostorage',
'interface', 'interface',
'library', 'library',
] ]

View File

@ -11,3 +11,4 @@ s3transfer==0.3.3
six==1.15.0 six==1.15.0
sqlparse==0.3.1 sqlparse==0.3.1
urllib3==1.25.10 urllib3==1.25.10
git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@165cca64c19176ca282147cdacc1f92c3a6ceb2f#egg=django_byostorage