Library working:
This commit is contained in:
parent
35555c3321
commit
1581f56b74
14
README.md
14
README.md
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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"),
|
('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)
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
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('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"),
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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)
|
||||||
@ -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',
|
||||||
|
|||||||
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'),
|
('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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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" %}
|
{% 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 %}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
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' %}
|
{% 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>
|
||||||
|
|
||||||
|
[{{ 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 %}
|
||||||
|
|||||||
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 = [
|
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"),
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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',
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user