Library working:
This commit is contained in:
parent
35555c3321
commit
1581f56b74
14
README.md
14
README.md
@ -1,6 +1,18 @@
|
||||
## 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
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ class EnsembleAdmin(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']
|
||||
|
||||
class SubmissionAdmin(admin.ModelAdmin):
|
||||
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
19
interface/migrations/0023_ensemble_storage.py
Normal file
19
interface/migrations/0023_ensemble_storage.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
23
interface/migrations/0024_auto_20210312_1712.py
Normal file
23
interface/migrations/0024_auto_20210312_1712.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
23
interface/migrations/0025_auto_20210312_1713.py
Normal file
23
interface/migrations/0025_auto_20210312_1713.py
Normal 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',
|
||||
),
|
||||
]
|
||||
18
interface/migrations/0026_auto_20210313_0926.py
Normal file
18
interface/migrations/0026_auto_20210313_0926.py
Normal 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',
|
||||
),
|
||||
]
|
||||
@ -23,6 +23,18 @@ MEDIA_TYPES = [
|
||||
('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):
|
||||
return "".join([ random.choice('0123456789') for _ in range(length) ])
|
||||
|
||||
@ -32,6 +44,7 @@ class Ensemble(models.Model):
|
||||
passphrase = models.CharField(max_length=100)
|
||||
admins = models.ManyToManyField('auth.User', related_name='ensembles')
|
||||
slug = models.SlugField(max_length=100, editable=False)
|
||||
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL)
|
||||
|
||||
def active_projects(self):
|
||||
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)
|
||||
description = models.TextField(blank=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)
|
||||
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
|
||||
def submissions(self):
|
||||
@ -65,6 +80,14 @@ class Project(models.Model):
|
||||
key = os.path.join(self.slug, object_name)
|
||||
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):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
|
||||
@ -313,7 +313,8 @@ TD.select-cell {
|
||||
}
|
||||
|
||||
DIV.selected {
|
||||
background-color: #EEE;
|
||||
background-color: var(--gray-blue);
|
||||
color: white;
|
||||
border: 1px solid #999;
|
||||
border-radius: 5px;
|
||||
}
|
||||
@ -5,6 +5,7 @@
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<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>
|
||||
<title>{% block title %}Polyphonic{% endblock %}</title>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends "interface/project_base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% block page %}
|
||||
<div style="flex-grow: 1">
|
||||
<h1>Projects for {{ ensemble.name }}</h1>
|
||||
<div class="list-group narrow">
|
||||
{% 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>
|
||||
<p><small>
|
||||
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
|
||||
@ -15,7 +15,7 @@
|
||||
</a>
|
||||
{% endfor %}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,25 +10,32 @@
|
||||
{% endif %}
|
||||
|
||||
<h1>{{ project.name }}</h1>
|
||||
|
||||
{% block page %}
|
||||
No content
|
||||
{% endblock %}
|
||||
{% if project %}
|
||||
|
||||
<div class="project-links">
|
||||
<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 %}
|
||||
<a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
|
||||
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if project and project.enable_submissions%}
|
||||
<a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a>
|
||||
<!--a role="tab" href="">Record a submission</a-->
|
||||
{% if request.is_admin %}
|
||||
<a role="tab" href="{% url 'submission_list' project=project.id %}">Submissions</a>
|
||||
{% endif %}
|
||||
<a role="tab" href="{% url 'submission_create' project=project.id %}">Send a file</a>
|
||||
{% endif %}
|
||||
|
||||
{% include "library/project_menu.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -2,30 +2,25 @@
|
||||
{% load md2 %}
|
||||
|
||||
|
||||
{% block page %}
|
||||
{% block page %}
|
||||
<div class="narrow">
|
||||
{% if project.deadline %}
|
||||
<h3 class="text-center">In {{ project.deadline|timeuntil }}</h3>
|
||||
{% if project.event_date %}
|
||||
<h3 class="text-center">{{ project.when }}</h3>
|
||||
{% endif %}
|
||||
<p>{{ project.description|markdown }}</p>
|
||||
|
||||
<p>{{ project.description|markdown }}</p>
|
||||
|
||||
{% 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 %}
|
||||
{% with sub_count=project.submissions.count %}
|
||||
<p>There have been {{ sub_count }} submission{{ sub_count|pluralize }} so far...</p>
|
||||
{% if sub_count %}
|
||||
<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>
|
||||
|
||||
{% if project.enable_submissions %}
|
||||
{% include 'submissions/project_detail.html' %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% if project.enable_library %}
|
||||
{% include 'library/project_detail.html' %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
9
interface/templatetags/path_filters.py
Normal file
9
interface/templatetags/path_filters.py
Normal 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)
|
||||
@ -11,8 +11,8 @@ urlpatterns = [
|
||||
path('manage', views.ManageView.as_view(), name="manage"),
|
||||
|
||||
path('', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
|
||||
path('projects/<int:pk>', views.ProjectDetailView.as_view(), name="project_detail"),
|
||||
path('projects/<int:pk>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"),
|
||||
path('projects/<int:project>', views.ProjectDetailView.as_view(), name="project_detail"),
|
||||
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>/edit', views.WikiEditView.as_view(), name="wiki_edit"),
|
||||
|
||||
@ -207,10 +207,10 @@ class EnsembleDetailView(EnsembleMixin, DetailView):
|
||||
def get_object(self):
|
||||
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
|
||||
|
||||
class ProjectDetailView(EnsembleMixin, DetailView):
|
||||
class ProjectDetailView(ProjectMixin, DetailView):
|
||||
|
||||
def get_queryset(self):
|
||||
return models.Project.objects.filter(ensemble=self.request.ensemble_id)
|
||||
def get_object(self):
|
||||
return self.get_project()
|
||||
|
||||
class ProjectMakefileView(EnsembleMixin, DetailView):
|
||||
template_name = 'interface/project_submissions.mk'
|
||||
|
||||
@ -2,12 +2,17 @@ from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
class PlaylistInline(admin.TabularInline):
|
||||
model = models.Playlist
|
||||
class OrchestrationAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'ensemble']
|
||||
list_filter = ['ensemble']
|
||||
|
||||
class ItemInline(admin.TabularInline):
|
||||
model = models.Item
|
||||
|
||||
class WorkAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'orchestration']
|
||||
inlines = [PlaylistInline]
|
||||
list_filter = ['ensemble']
|
||||
inlines = [ItemInline]
|
||||
|
||||
class PartInline(admin.TabularInline):
|
||||
model = models.Part
|
||||
@ -15,12 +20,14 @@ class PartInline(admin.TabularInline):
|
||||
|
||||
class DocumentAdmin(admin.ModelAdmin):
|
||||
list_display = ['work', '__str__']
|
||||
list_filter = ['work__ensemble']
|
||||
inlines = [PartInline]
|
||||
|
||||
class PlaylistAdmin(admin.ModelAdmin):
|
||||
class ItemAdmin(admin.ModelAdmin):
|
||||
list_display = ['project', 'work', 'order']
|
||||
list_filter = ['project']
|
||||
|
||||
admin.site.register(models.Orchestration, OrchestrationAdmin)
|
||||
admin.site.register(models.Work, WorkAdmin)
|
||||
admin.site.register(models.Document, DocumentAdmin)
|
||||
admin.site.register(models.Playlist, PlaylistAdmin)
|
||||
admin.site.register(models.Item, ItemAdmin)
|
||||
@ -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
|
||||
import django.db.models.deletion
|
||||
import library.models
|
||||
import library.storage
|
||||
import byostorage.cached
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -19,11 +19,11 @@ class Migration(migrations.Migration):
|
||||
name='Document',
|
||||
fields=[
|
||||
('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(
|
||||
name='Playlist',
|
||||
name='Item',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('order', models.SmallIntegerField(default=0)),
|
||||
@ -33,27 +33,31 @@ class Migration(migrations.Migration):
|
||||
'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(
|
||||
name='Work',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(editable=False, max_length=100)),
|
||||
('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)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('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={
|
||||
'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(
|
||||
name='Part',
|
||||
fields=[
|
||||
@ -68,6 +72,11 @@ class Migration(migrations.Migration):
|
||||
'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(
|
||||
model_name='document',
|
||||
name='work',
|
||||
|
||||
27
library/migrations/0003_orchestrations.py
Normal file
27
library/migrations/0003_orchestrations.py
Normal 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),
|
||||
]
|
||||
@ -34,7 +34,7 @@ INSTRUMENTS = [
|
||||
('Tbn', 'Trombone'),
|
||||
('Tuba', 'Tuba'),
|
||||
('Timp', 'Timpani'),
|
||||
('Drum', 'Drupset'),
|
||||
('Drum', 'Drumset'),
|
||||
('Perc', 'Percussion'),
|
||||
('Pno', 'Piano'),
|
||||
('Hp', 'Harp'),
|
||||
@ -58,14 +58,30 @@ def tag_to_instrument(tag):
|
||||
l = m.groups()
|
||||
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):
|
||||
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="works")
|
||||
slug = models.SlugField(max_length=100, editable=False)
|
||||
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)
|
||||
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:
|
||||
unique_together = ['ensemble', 'slug']
|
||||
@ -76,8 +92,7 @@ class Work(models.Model):
|
||||
|
||||
@property
|
||||
def instruments(self):
|
||||
tags = ORCHESTRATIONS.get(self.orchestration, 'Custom')
|
||||
return [ (tag, tag_to_instrument(tag)) for tag in tags ]
|
||||
return self.orchestration.as_list()
|
||||
|
||||
def save(self):
|
||||
if not self.slug:
|
||||
@ -87,7 +102,11 @@ class Work(models.Model):
|
||||
def __str__(self):
|
||||
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)
|
||||
work = models.ForeignKey('Work', on_delete=models.CASCADE)
|
||||
order = models.SmallIntegerField(default=0)
|
||||
@ -98,12 +117,15 @@ class Playlist(models.Model):
|
||||
def __str__(self):
|
||||
return f"<{self.project.slug}:{self.work.slug}>"
|
||||
|
||||
def upload_filename(instance, filename):
|
||||
return f'{instance.work.ensemble.slug}/works/{instance.work.slug}/{filename}'
|
||||
def doc_upload_filename(doc, 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):
|
||||
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):
|
||||
return self.upload.name
|
||||
|
||||
@ -1,19 +1,28 @@
|
||||
import tempfile
|
||||
import subprocess
|
||||
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):
|
||||
|
||||
# create a temporary directory for our sections
|
||||
d = tempfile.TemporaryDirectory(prefix="polyphonic_")
|
||||
pdfmarks = os.path.join(d.name, 'pdfmarks.txt')
|
||||
marks = open(pdfmarks, 'w')
|
||||
|
||||
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:
|
||||
sections.append(source)
|
||||
@ -25,13 +34,22 @@ def extract_and_concat(items):
|
||||
|
||||
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'-sOutputFile={dest}',
|
||||
source]
|
||||
|
||||
subprocess.run(cmd, check=True)
|
||||
sections.append(dest)
|
||||
bookmark = "".join(filter(lambda c: c in SAFECHARS, bookmark))
|
||||
|
||||
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
|
||||
output = tempfile.NamedTemporaryFile(prefix="polyphonic_", suffix='.pdf')
|
||||
@ -39,6 +57,7 @@ def extract_and_concat(items):
|
||||
cmd = ['gs', '-sDEVICE=pdfwrite', '-q', '-dBATCH', '-dNOPAUSE',
|
||||
'-sOutputFile=-']
|
||||
cmd.extend(sections)
|
||||
cmd.append(pdfmarks)
|
||||
|
||||
subprocess.run(cmd, stdout=output)
|
||||
|
||||
|
||||
@ -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
|
||||
@ -1,27 +1,37 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block page %}
|
||||
<p>
|
||||
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/>
|
||||
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>
|
||||
<form action="" method="post" target="_blank" style="display: flex;">
|
||||
{% csrf_token %}
|
||||
<table style="max-width: 600px; margin: 10pt auto;" class="item-table">
|
||||
<thead>
|
||||
</thead>
|
||||
<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>
|
||||
<table style="max-width: 600px; margin: 10pt auto;" class="">
|
||||
<tbody>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }})</td>
|
||||
<td>
|
||||
<a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a>
|
||||
</td>
|
||||
<td class="select-cell">
|
||||
<td class="">
|
||||
<input type="hidden" name="works" value="{{ item.work.pk }}"/>
|
||||
<select name="instruments">
|
||||
<option value='-'>None</option>
|
||||
@ -34,17 +44,6 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</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>
|
||||
|
||||
{% endblock %}
|
||||
@ -2,28 +2,31 @@
|
||||
|
||||
{% block admin %}
|
||||
<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 %}
|
||||
|
||||
{% block page %}
|
||||
<table style="max-width: 600px; margin: 10pt auto;" class="item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Piece</th>
|
||||
<th>Part</th>
|
||||
<th>Item</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="work-list">
|
||||
{% for item in object_list %}
|
||||
<tr data-pk="{{ item.pk }}" data-order="{{ forloop.counter }}">
|
||||
<td>{{ item.work.name }}</td>
|
||||
<td>{% firstof item.work.running_time '?' %}</td>
|
||||
<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-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>
|
||||
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="2">No items</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -47,8 +50,13 @@ function moveItem(pk, dir) {
|
||||
// check the direction is sensible
|
||||
if (i + dir < 0 || i + dir >= items.length) return;
|
||||
|
||||
items[i].dataset.order = parseInt(items[i].dataset.order) + dir;
|
||||
items[i+dir].dataset.order = parseInt(items[i+dir].dataset.order) - dir;
|
||||
if (dir === 0) {
|
||||
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))
|
||||
|
||||
@ -76,7 +84,7 @@ function save() {
|
||||
}).then((response) => {
|
||||
if (response.ok) {
|
||||
dirty=false;
|
||||
window.location = "{% url 'project_playlist' project=project.pk %}";
|
||||
window.location = "{% url 'item_list' project=project.pk %}";
|
||||
} else {
|
||||
alert("Failed: " + response.statusText)
|
||||
}
|
||||
1
library/templates/library/project_detail.html
Normal file
1
library/templates/library/project_detail.html
Normal 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>
|
||||
@ -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 %}
|
||||
@ -1,4 +1,5 @@
|
||||
{% extends 'interface/project_base.html' %}
|
||||
{% load path_filters %}
|
||||
|
||||
{% block admin %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url 'work_partset' pk=work.pk %}"><i class="fas fa-print"></i> Print part set</a>
|
||||
|
||||
<h3>Original Documents</h3>
|
||||
<ul>
|
||||
{% for doc in work.docs.all %}
|
||||
<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>
|
||||
|
||||
[{{ doc.parts.count }} parts]
|
||||
{% 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 %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
31
library/templates/library/work_partset.html
Normal file
31
library/templates/library/work_partset.html
Normal 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 %}
|
||||
@ -5,13 +5,14 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path('projects/<int:project>/works', views.PlaylistView.as_view(), name="project_playlist"),
|
||||
path('projects/<int:project>/works/manage', views.PlaylistManageView.as_view(), name="playlist_manage"),
|
||||
path('projects/<int:project>/works/append', views.PlaylistAddView.as_view(), name="playlist_append"),
|
||||
path('projects/<int:project>/items', views.ProjectItemListView.as_view(), name="item_list"),
|
||||
path('projects/<int:project>/items/manage', views.ProjectItemManageView.as_view(), name="item_list_manage"),
|
||||
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/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>/partset', views.WorkPartSetView.as_view(), name="work_partset"),
|
||||
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>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from django.shortcuts import render, redirect, resolve_url
|
||||
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.http import FileResponse, HttpResponse
|
||||
from django.db import IntegrityError
|
||||
@ -9,13 +9,13 @@ import json
|
||||
|
||||
from interface.views import EnsembleMixin, ProjectMixin
|
||||
from interface.models import Project
|
||||
from .models import Work, Document, Part, Playlist, INSTRUMENTS
|
||||
from . import forms
|
||||
from .models import Work, Document, Part, INSTRUMENTS
|
||||
from . import forms, models
|
||||
from .pdf_utils import extract_pages, extract_and_concat
|
||||
|
||||
class PlaylistView(ProjectMixin, ListView):
|
||||
template_name = "library/playlist_view.html"
|
||||
model = Playlist
|
||||
class ProjectItemListView(ProjectMixin, ListView):
|
||||
template_name = "library/item_list.html"
|
||||
model = models.Item
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
|
||||
@ -43,7 +43,7 @@ class PlaylistView(ProjectMixin, ListView):
|
||||
continue
|
||||
|
||||
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)
|
||||
|
||||
@ -55,18 +55,18 @@ class PlaylistView(ProjectMixin, ListView):
|
||||
|
||||
|
||||
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):
|
||||
data = super(PlaylistView, self).get_context_data(**kwargs)
|
||||
data = super(ProjectItemListView, self).get_context_data(**kwargs)
|
||||
data['instruments'] = INSTRUMENTS
|
||||
data['instrument'] = self.request.session.get('instrument', 'Score')
|
||||
data['part'] = self.request.session.get('part', '0')
|
||||
return data
|
||||
|
||||
class PlaylistManageView(ProjectMixin, ListView):
|
||||
template_name = "library/playlist_manage.html"
|
||||
model = Playlist
|
||||
class ProjectItemManageView(ProjectMixin, ListView):
|
||||
template_name = "library/item_list_manage.html"
|
||||
model = models.Item
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
self.request = request
|
||||
@ -76,19 +76,23 @@ class PlaylistManageView(ProjectMixin, ListView):
|
||||
|
||||
q = self.get_queryset()
|
||||
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)
|
||||
|
||||
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
|
||||
template_name = "interface/default_form.html"
|
||||
|
||||
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):
|
||||
return self.get_project()
|
||||
@ -102,6 +106,12 @@ class WorkAddView(EnsembleMixin, FormView):
|
||||
template_name = "interface/default_form.html"
|
||||
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):
|
||||
obj = form.save(commit=False)
|
||||
obj.ensemble_id = self.request.ensemble_id
|
||||
@ -127,6 +137,32 @@ class WorkDetailView(EnsembleMixin, DetailView):
|
||||
def get_queryset(self):
|
||||
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):
|
||||
|
||||
@ -198,6 +234,7 @@ class DocumentAnnotateView(EnsembleMixin, DetailView):
|
||||
def get_queryset(self):
|
||||
return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work')
|
||||
|
||||
|
||||
class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
||||
|
||||
def get(self, request, **args):
|
||||
|
||||
@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django_markdown2',
|
||||
'byostorage',
|
||||
'interface',
|
||||
'library',
|
||||
]
|
||||
|
||||
@ -11,3 +11,4 @@ s3transfer==0.3.3
|
||||
six==1.15.0
|
||||
sqlparse==0.3.1
|
||||
urllib3==1.25.10
|
||||
git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@165cca64c19176ca282147cdacc1f92c3a6ceb2f#egg=django_byostorage
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user