Got library sort of working
This commit is contained in:
parent
7aedc6bd07
commit
598ee5ad7e
18
interface/migrations/0028_ensemble_details.py
Normal file
18
interface/migrations/0028_ensemble_details.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.1 on 2021-05-05 05:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('interface', '0027_auto_20210322_1154'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ensemble',
|
||||
name='details',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@ -44,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)
|
||||
details = models.TextField(blank=True)
|
||||
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL)
|
||||
|
||||
def active_projects(self):
|
||||
@ -53,10 +54,10 @@ class Ensemble(models.Model):
|
||||
code = str(self.code)
|
||||
return "{}-{}-{}".format(code[:3], code[3:6], code[6:])
|
||||
|
||||
def save(self):
|
||||
def save(self, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super(Ensemble, self).save()
|
||||
super(Ensemble, self).save(**kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -86,7 +87,7 @@ class Project(models.Model):
|
||||
|
||||
@property
|
||||
def has_happened(self):
|
||||
return self.event_date < timezone.now()
|
||||
return self.event_date < timezone.now().date()
|
||||
|
||||
def save(self):
|
||||
if not self.slug:
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
--border-color: #292929;
|
||||
--gray-blue: #667788;
|
||||
--light-blue: #c5eff7;
|
||||
--light-grey: #EEEEEE;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -36,7 +37,7 @@ BODY {
|
||||
}
|
||||
|
||||
.main {
|
||||
max-width: 1000px;
|
||||
max-width: 1280px;
|
||||
margin: 10px auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
@ -55,16 +56,30 @@ BODY {
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
.wide {
|
||||
width: 1200px;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1200px) {
|
||||
.wide {
|
||||
width: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 900px) {
|
||||
.mdhide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wide {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 700px) {
|
||||
@ -74,6 +89,9 @@ BODY {
|
||||
.collapse {
|
||||
flex-direction: column;
|
||||
}
|
||||
.wide {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -138,6 +156,16 @@ INPUT[type=checkbox] {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--gray-blue);
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
border-radius: 10px;
|
||||
padding: 4px 10px 2px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: var(--gray-blue);
|
||||
display: inline-block;
|
||||
@ -232,10 +260,28 @@ H1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
TD {
|
||||
padding: 5px;
|
||||
TABLE {
|
||||
border-spacing: 0px;
|
||||
}
|
||||
|
||||
TD, TH {
|
||||
padding: 3px 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
TABLE THEAD TR {
|
||||
background-color: var(--gray-blue);
|
||||
color: var(--light-blue);
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
TABLE THEAD TH {
|
||||
padding: 5px 6px;
|
||||
}
|
||||
|
||||
TABLE.zebra TR:nth-child(even) {
|
||||
background-color: var(--light-grey);
|
||||
}
|
||||
|
||||
TABLE.horizontal TH {
|
||||
text-align: right;
|
||||
@ -298,12 +344,12 @@ TABLE SELECT {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
TD.select-cell {
|
||||
padding: 0;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.select-cell SELECT {
|
||||
border: none;
|
||||
background-color: white;
|
||||
background-color: transparent;
|
||||
height: 30px;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
{% block page %}
|
||||
<div>
|
||||
<h3>{{ title }}</h3>
|
||||
<h3>{% firstof title view.title %}</h3>
|
||||
<form class="vertical" method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
|
||||
@ -1,22 +1,12 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% 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' project=project.id %}">
|
||||
<h3>{{ project.name }}</h3>
|
||||
<p><small>
|
||||
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
|
||||
{% if project.works.count %}{{ project.works.count }} works<br/>{% endif %}
|
||||
{% if project.submissions.count %}{{ project.submissions.count }} submissions<br/>{% endif %}
|
||||
</small></p>
|
||||
</a>
|
||||
{% block content %}
|
||||
<h2>{{ ensemble.name }}</h2>
|
||||
<p>{{ ensemble.details }}</p>
|
||||
<h4>Administrators</h4>
|
||||
<ul>
|
||||
{% for admin in ensemble.admins.all %}
|
||||
<li><a href="mailto:{{ admin.email }}">{% firstof admin.get_full_name admin.get_username %}</a></li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div style="float: right; margin-top: 10px; color: #999;">
|
||||
<small>{{ ensemble.ensemble_code }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
22
interface/templates/interface/ensemble_project_list.html
Normal file
22
interface/templates/interface/ensemble_project_list.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
|
||||
{% 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' project=project.id %}">
|
||||
<h3>{{ project.name }}</h3>
|
||||
<p><small>
|
||||
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
|
||||
{% if project.works.count %}{{ project.works.count }} works<br/>{% endif %}
|
||||
{% if project.submissions.count %}{{ project.submissions.count }} submissions<br/>{% endif %}
|
||||
</small></p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div style="float: right; margin-top: 10px; color: #999;">
|
||||
<small>{{ ensemble.ensemble_code }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -7,9 +7,9 @@
|
||||
{% if project.event_date %}
|
||||
<h3 class="text-center">
|
||||
{% if project.has_happened %}
|
||||
{{ project.event_date|timesince }}
|
||||
{{ project.event_date|timesince }} ago.
|
||||
{% else %}
|
||||
{{ project.event_date|}}
|
||||
In {{ project.event_date|timeuntil }}.
|
||||
{% endif %}
|
||||
</h3>
|
||||
{% endif %}
|
||||
|
||||
10
interface/templatetags/url_tools.py
Normal file
10
interface/templatetags/url_tools.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def url_update(context, **kwargs):
|
||||
params = context.request.GET.copy()
|
||||
for k in kwargs:
|
||||
params[k] = kwargs[k]
|
||||
return "?" + params.urlencode()
|
||||
6
interface/tests/test_integration.py
Normal file
6
interface/tests/test_integration.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.test import TestCase
|
||||
|
||||
class IntegrationTestCase(TestCase):
|
||||
|
||||
def test_runs(self):
|
||||
self.assertTrue(True)
|
||||
@ -10,7 +10,9 @@ urlpatterns = [
|
||||
path('register', views.register, name="register"),
|
||||
path('manage', views.ManageView.as_view(), name="manage"),
|
||||
|
||||
path('', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
|
||||
path('', views.EnsembleProjectListView.as_view(), name='ensemble_detail'),
|
||||
path('ensembles/<int:pk>', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
|
||||
|
||||
path('projects/<int:project>', views.ProjectDetailView.as_view(), name="project_detail"),
|
||||
path('projects/<int:project>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"),
|
||||
|
||||
|
||||
@ -60,6 +60,10 @@ class EnsembleMixin(object):
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def ensemble(self):
|
||||
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
|
||||
|
||||
class ProjectMixin(EnsembleMixin):
|
||||
|
||||
def get_project(self):
|
||||
@ -195,7 +199,8 @@ def logout(request):
|
||||
return redirect('/')
|
||||
|
||||
|
||||
class EnsembleDetailView(EnsembleMixin, DetailView):
|
||||
class EnsembleProjectListView(EnsembleMixin, DetailView):
|
||||
template_name = 'interface/ensemble_project_list.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# capture provided urls
|
||||
@ -207,6 +212,9 @@ class EnsembleDetailView(EnsembleMixin, DetailView):
|
||||
def get_object(self):
|
||||
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
|
||||
|
||||
class EnsembleDetailView(DetailView):
|
||||
model = models.Ensemble
|
||||
|
||||
class ProjectDetailView(ProjectMixin, DetailView):
|
||||
|
||||
def get_object(self):
|
||||
|
||||
13
library/README.md
Normal file
13
library/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
# Model overview
|
||||
|
||||
Collections contain Works
|
||||
Only the collection's administrators can add/remove/edit items
|
||||
Collections can be enabled for Ensembles which makes everything availble in the Library
|
||||
|
||||
|
||||
## How do loans work?
|
||||
|
||||
1. Project admins need to be able to search for available music - list with access type
|
||||
2. Item can be attached to Project
|
||||
|
||||
@ -2,17 +2,29 @@ from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
class OrchestrationAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'ensemble']
|
||||
list_filter = ['ensemble']
|
||||
#class OrchestrationAdmin(admin.ModelAdmin):
|
||||
# list_display = ['name', 'ensemble']
|
||||
# list_filter = ['ensemble']
|
||||
|
||||
#admin.site.register(models.Orchestration, OrchestrationAdmin)
|
||||
|
||||
class CollectionAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'location']
|
||||
|
||||
admin.site.register(models.Collection, CollectionAdmin)
|
||||
|
||||
class ItemInline(admin.TabularInline):
|
||||
model = models.Item
|
||||
|
||||
class DocInline(admin.TabularInline):
|
||||
model = models.Document
|
||||
|
||||
class WorkAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'composer', 'orchestration']
|
||||
list_filter = ['ensemble']
|
||||
inlines = [ItemInline]
|
||||
list_display = ['name', 'edition', 'composer', 'running_time']
|
||||
list_filter = ['collection']
|
||||
inlines = [DocInline, ItemInline]
|
||||
|
||||
admin.site.register(models.Work, WorkAdmin)
|
||||
|
||||
class PartInline(admin.TabularInline):
|
||||
model = models.Part
|
||||
@ -20,14 +32,19 @@ class PartInline(admin.TabularInline):
|
||||
|
||||
class DocumentAdmin(admin.ModelAdmin):
|
||||
list_display = ['work', '__str__']
|
||||
list_filter = ['work__ensemble']
|
||||
list_filter = ['work__collection']
|
||||
inlines = [PartInline]
|
||||
|
||||
admin.site.register(models.Document, DocumentAdmin)
|
||||
|
||||
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.Item, ItemAdmin)
|
||||
admin.site.register(models.Item, ItemAdmin)
|
||||
|
||||
class EnsembleAccessAdmin(admin.ModelAdmin):
|
||||
list_display = ['ensemble', 'collection', 'access_type']
|
||||
list_filter = ['ensemble']
|
||||
|
||||
admin.site.register(models.EnsembleAccess, EnsembleAccessAdmin)
|
||||
@ -1,12 +1,15 @@
|
||||
from django import forms
|
||||
from .models import Work
|
||||
from interface.models import Project
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
class WorkCreateForm(forms.ModelForm):
|
||||
uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True}))
|
||||
uploads = forms.FileField(label="PDFs to upload", widget=forms.ClearableFileInput(attrs={'multiple': True}), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Work
|
||||
fields = ['uploads', 'name', 'orchestration', 'running_time', 'notes']
|
||||
fields = ['uploads', 'name', 'composer', 'edition', 'collection', 'code', 'running_time', 'notes']
|
||||
|
||||
class PlaylistAddForm(forms.Form):
|
||||
work = forms.ModelChoiceField(queryset=Work.objects.all())
|
||||
@ -21,4 +24,7 @@ class PlaylistAddForm(forms.Form):
|
||||
self.instance = instance
|
||||
|
||||
def save(self):
|
||||
self.instance.works.add(self.cleaned_data['work'])
|
||||
self.instance.works.add(self.cleaned_data['work'])
|
||||
|
||||
class ProjectSelectForm(forms.Form):
|
||||
project = forms.ModelChoiceField(queryset=Project.objects.all())
|
||||
0
library/management/__init__.py
Normal file
0
library/management/__init__.py
Normal file
0
library/management/commands/__init__.py
Normal file
0
library/management/commands/__init__.py
Normal file
20
library/management/commands/import_works.py
Normal file
20
library/management/commands/import_works.py
Normal file
@ -0,0 +1,20 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
import argparse
|
||||
import csv
|
||||
|
||||
from library import models
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Imports works from a csv file'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('collection', type=int, help="Collection ID")
|
||||
parser.add_argument('source', type=argparse.FileType('r'), help="Source CSV")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
collection = models.Collection.objects.get(pk=options['collection'])
|
||||
|
||||
reader = csv.DictReader(options['source'])
|
||||
for row in reader:
|
||||
collection.works.create(name=row['Piece'], composer=row['Composer'], notes=row['Notes'])
|
||||
@ -1,9 +1,10 @@
|
||||
# Generated by Django 3.1.1 on 2021-03-11 07:07
|
||||
# Generated by Django 3.1.1 on 2021-04-28 03:53
|
||||
|
||||
import byostorage.cached
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import library.models
|
||||
import byostorage.cached
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -11,14 +12,29 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('interface', '0022_auto_20210303_2043'),
|
||||
('interface', '0027_auto_20210322_1154'),
|
||||
('byostorage', '0003_auto_20210323_1047'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Collection',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('location', models.CharField(help_text='Physical location', max_length=100)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('administrators', models.ManyToManyField(help_text='Administrators for this collection', related_name='collections', to=settings.AUTH_USER_MODEL)),
|
||||
('ensembles', models.ManyToManyField(related_name='collections', to='interface.Ensemble')),
|
||||
('storage', models.ForeignKey(blank=True, help_text='Storage for documents', null=True, on_delete=django.db.models.deletion.CASCADE, to='byostorage.userstorage')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Document',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('doctype', models.PositiveSmallIntegerField(choices=[(1, 'PDF'), (2, 'Audio'), (3, 'Video'), (4, 'Source')], default=1)),
|
||||
('upload', models.FileField(storage=byostorage.cached.CachedStorage(), upload_to=library.models.doc_upload_filename)),
|
||||
],
|
||||
),
|
||||
@ -48,15 +64,19 @@ class Migration(migrations.Migration):
|
||||
('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)),
|
||||
('running_time', models.IntegerField(blank=True, null=True)),
|
||||
('version', models.CharField(blank=True, help_text='Version or edition details', max_length=100)),
|
||||
('composer', models.CharField(blank=True, help_text='Use Composer / Arranger format', max_length=255)),
|
||||
('code', models.CharField(blank=True, help_text='Collection specific code or number', max_length=100)),
|
||||
('licence', models.PositiveSmallIntegerField(choices=[(2, 'Public Domain'), (4, 'Copyright Expired'), (6, 'Copyrighted'), (10, 'Internal use only')], default=6, help_text='Copyright status')),
|
||||
('max_loans', models.BooleanField(default=1, help_text='How many projects can this work be attached to')),
|
||||
('running_time', models.IntegerField(blank=True, help_text='Running time in seconds', null=True)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='interface.ensemble')),
|
||||
('tag_list', models.CharField(blank=True, help_text='Multiple tags for the work', max_length=255)),
|
||||
('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.collection')),
|
||||
('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')),
|
||||
('parent', models.ForeignKey(blank=True, help_text='Arrangement of another work or part of an anthology', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='related_works', to='library.work')),
|
||||
('projects', models.ManyToManyField(help_text='Current usage', related_name='works', through='library.Item', to='interface.Project')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('ensemble', 'slug')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Part',
|
||||
@ -75,7 +95,16 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='work',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.work'),
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_items', to='library.work'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EnsembleAccess',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('access_type', models.PositiveSmallIntegerField(choices=[(1, 'Unlimited'), (2, 'Approval required')], default=2)),
|
||||
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.collection')),
|
||||
('ensemble', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='interface.ensemble')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
|
||||
34
library/migrations/0002_auto_20210504_1830.py
Normal file
34
library/migrations/0002_auto_20210504_1830.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.1.1 on 2021-05-04 08:30
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('library', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='approved_by',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='auth.user'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='checkin',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='checkout',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
24
library/migrations/0003_auto_20210504_1845.py
Normal file
24
library/migrations/0003_auto_20210504_1845.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.1 on 2021-05-04 08:45
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0002_auto_20210504_1830'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='work',
|
||||
name='max_loans',
|
||||
field=models.IntegerField(default=1, help_text='How many projects can this work be attached to'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='work',
|
||||
name='orchestration',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='works', to='library.orchestration'),
|
||||
),
|
||||
]
|
||||
@ -1,27 +0,0 @@
|
||||
# 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),
|
||||
]
|
||||
44
library/migrations/0004_auto_20210505_0927.py
Normal file
44
library/migrations/0004_auto_20210505_0927.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Generated by Django 3.1.1 on 2021-05-04 23:27
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0003_auto_20210504_1845'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='item',
|
||||
old_name='checkin',
|
||||
new_name='due',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='returned',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='collection',
|
||||
name='location',
|
||||
field=models.CharField(help_text='Physical location (institution, town...)', max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='collection',
|
||||
name='name',
|
||||
field=models.CharField(help_text='Often just the name of the owning ensemble', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='collection',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, help_text='Publicly visible notes about collection and loans policy'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='work',
|
||||
name='collection',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='works', to='library.collection'),
|
||||
),
|
||||
]
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 3.1.1 on 2021-03-22 23:47
|
||||
# Generated by Django 3.1.1 on 2021-05-06 02:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0003_orchestrations'),
|
||||
('library', '0004_auto_20210505_0927'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='work',
|
||||
name='composer',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
name='parts',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
48
library/migrations/0006_auto_20210902_1355.py
Normal file
48
library/migrations/0006_auto_20210902_1355.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Generated by Django 3.2.7 on 2021-09-02 03:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('interface', '0028_ensemble_details'),
|
||||
('library', '0005_work_parts'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='ensembleaccess',
|
||||
options={'verbose_name_plural': 'Ensemble access'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='collection',
|
||||
name='ensembles',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='version',
|
||||
field=models.CharField(blank=True, max_length=30),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='version',
|
||||
field=models.CharField(blank=True, help_text='Limited to specific version tag', max_length=30),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='collection',
|
||||
name='name',
|
||||
field=models.CharField(help_text='Name of the collection', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ensembleaccess',
|
||||
name='collection',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allowed_ensembles', to='library.collection'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ensembleaccess',
|
||||
name='ensemble',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allowed_collections', to='interface.ensemble'),
|
||||
),
|
||||
]
|
||||
33
library/migrations/0007_auto_20210902_1407.py
Normal file
33
library/migrations/0007_auto_20210902_1407.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.2.7 on 2021-09-02 04:07
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0006_auto_20210902_1355'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='work',
|
||||
name='orchestration',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='work',
|
||||
name='version',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_created=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='work',
|
||||
name='edition',
|
||||
field=models.CharField(blank=True, help_text='Edition details', max_length=100),
|
||||
),
|
||||
]
|
||||
18
library/migrations/0008_work_orchestration.py
Normal file
18
library/migrations/0008_work_orchestration.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.7 on 2021-09-02 04:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('library', '0007_auto_20210902_1407'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='work',
|
||||
name='orchestration',
|
||||
field=models.CharField(blank=True, help_text='IMDB format instrumentation', max_length=255),
|
||||
),
|
||||
]
|
||||
@ -1,7 +1,11 @@
|
||||
from os import SCHED_OTHER
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
from django.utils.functional import cached_property
|
||||
from django.core.files.storage import get_storage_class
|
||||
from django.db.models import Q
|
||||
|
||||
import re
|
||||
|
||||
@ -14,42 +18,181 @@ except (ImportError, AttributeError):
|
||||
library_storage = get_storage_class()()
|
||||
logger.info("Library storage: %s", library_storage.__class__.__name__)
|
||||
|
||||
INSTRUMENTS = [
|
||||
('Score', 'Score'),
|
||||
('S', 'Soprano'),
|
||||
('A', 'Alto'),
|
||||
('T', 'Tenor'),
|
||||
('B', 'Bass'),
|
||||
('V', 'Vocals'),
|
||||
('Vln', 'Violin'),
|
||||
('Vla', 'Viola'),
|
||||
('Vc', 'Violoncello'),
|
||||
('Cb', 'Contrabass'),
|
||||
('Fl', 'Flute'),
|
||||
('Picc', 'Piccolo'),
|
||||
('Cl', 'Clarinet'),
|
||||
('Ob', 'Oboe'),
|
||||
('Hn', 'Horn'),
|
||||
('Tpt', 'Trumpet'),
|
||||
('Tbn', 'Trombone'),
|
||||
('Tuba', 'Tuba'),
|
||||
('Timp', 'Timpani'),
|
||||
('Drum', 'Drumset'),
|
||||
('Perc', 'Percussion'),
|
||||
('Pno', 'Piano'),
|
||||
('Hp', 'Harp'),
|
||||
]
|
||||
# taken from https://imslp.org/wiki/IMSLP:Abbreviations_for_Instruments
|
||||
ABBREVIATIONS = """
|
||||
score Score
|
||||
acc Accordion
|
||||
afl Alto flute
|
||||
alt Alto (voice) (contralto)
|
||||
arp Arpeggione
|
||||
bag Bagpipe
|
||||
bar Baritone (voice)
|
||||
bass Bass (voice)
|
||||
bbar Bass baritone (voice)
|
||||
bc Continuo (Basso continuo)
|
||||
bcl Bass clarinet
|
||||
bell Bell (Chimes)
|
||||
bfl Bass flute
|
||||
bgtr Bass guitar
|
||||
bjo Banjo
|
||||
bn Bassoon
|
||||
bob Bass oboe (Baritone oboe)
|
||||
br Brass instruments
|
||||
bryt Baryton
|
||||
bstcl Basset clarinet
|
||||
bsthn Basset horn
|
||||
bug Bugle
|
||||
cbcl Contrabass clarinet
|
||||
cbn Contrabassoon
|
||||
cch Children's chorus
|
||||
cel Celesta
|
||||
ch Mixed chorus
|
||||
cimb Cimbalom
|
||||
cit Cittern
|
||||
cl Clarinet
|
||||
clvd Clavichord
|
||||
cm Chalumeau
|
||||
conc Concertina
|
||||
crh Crumhorn
|
||||
crt Cornet
|
||||
crtt Cornett (Zink)
|
||||
cv Child's voice
|
||||
db Double Bass
|
||||
dlcn Dulcian
|
||||
dom Domra
|
||||
dulc Dulcimer
|
||||
egtr Electric guitar
|
||||
eh English horn (Cor anglais)
|
||||
elec Electronic Instruments
|
||||
epf Electric piano
|
||||
eq Equal voices
|
||||
erhu Erhu
|
||||
euph Euphonium
|
||||
fch Female chorus
|
||||
fda Flute d'amore (Tenor flute)
|
||||
fgh Flugelhorn
|
||||
fife Fife
|
||||
fl Flute
|
||||
flag Flageolet
|
||||
ghca Glass harmonica (Bowl organ)
|
||||
gl Glockenspiel
|
||||
gtr Guitar
|
||||
harm Harmonium
|
||||
hca Harmonica (Mouth Organ)
|
||||
heck Heckelphone
|
||||
hn Horn
|
||||
hp Harp
|
||||
hpd Harpsichord
|
||||
kbd Keyboard instrument
|
||||
lute Lute
|
||||
lyre Lyre
|
||||
mand Mandolin
|
||||
mar Marimba
|
||||
mch Male chorus
|
||||
mez Mezzo-soprano
|
||||
mus Musette
|
||||
nar Narrator (Reciter)
|
||||
ob Oboe
|
||||
oca Ocarina
|
||||
oda Oboe d'amore
|
||||
om Ondes Martenot
|
||||
oph Ophicleide
|
||||
orch Orchestra
|
||||
org Organ
|
||||
oud Oud
|
||||
pan Pan flute (Pan-pipes)
|
||||
perc Percussion
|
||||
pf Piano
|
||||
pf3h Piano 3 hands
|
||||
pf4h Piano 4 hands
|
||||
pf5h Piano 5 hands
|
||||
pf6h Piano 6 hands
|
||||
pflh Piano left hand
|
||||
pfped Pedal piano
|
||||
pfrh Piano right hand
|
||||
picc Piccolo
|
||||
pipa Pipa
|
||||
pk Timpani
|
||||
ptpt Piccolo trumpet
|
||||
reb Rebec
|
||||
rec Recorder
|
||||
sar Sarrusophone
|
||||
sax Saxophone
|
||||
sheng Sheng
|
||||
shw Shawm
|
||||
sit Sitar
|
||||
skbt Sackbut
|
||||
sop Soprano (voice)
|
||||
srp Serpent
|
||||
stpt Slide trumpet
|
||||
str String instruments
|
||||
sxh Saxhorn
|
||||
syn Synthesizer
|
||||
tba Tuba
|
||||
tbn Trombone
|
||||
ten Tenor
|
||||
thrm Theremin
|
||||
timp Timpani
|
||||
tpt Trumpet
|
||||
uch Unison chorus
|
||||
uke Ukelele (Ukulele)
|
||||
v Voice (solo)
|
||||
va Viola
|
||||
vap Viola pomposa
|
||||
vc Cello
|
||||
vda Viola d'amore
|
||||
vib Vibraphone
|
||||
vie Vielle (Hurdy-Gurdy)
|
||||
viol Viol (Viola da gamba)
|
||||
vlne Violone
|
||||
vn Violin
|
||||
vuv Vuvuzela
|
||||
vv Voices (multiple soloists)
|
||||
wag Wagner tuba
|
||||
ww Woodwind instruments
|
||||
xiao Xiao
|
||||
xyl Xylophone
|
||||
zith Zither
|
||||
"""
|
||||
|
||||
INSTRUMENTS = []
|
||||
for line in ABBREVIATIONS.split('\n'):
|
||||
parts = line.strip().split(maxsplit=1)
|
||||
if len(parts) < 2: continue
|
||||
name, _, _ = parts[1].partition('(')
|
||||
INSTRUMENTS.append((parts[0], name))
|
||||
|
||||
'''
|
||||
ORCHESTRATIONS = {
|
||||
'SATB': ('S', 'A', 'T', 'B'),
|
||||
'String Quartet': ('Vln1', 'Vln2', 'Vla', 'Vc'),
|
||||
'String Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb'),
|
||||
'Chamber Orchestra': ('Vln1', 'Vln2', 'Vla', 'Vc', 'Cb',
|
||||
'Fl1', 'Fl2', 'Cl1', 'Cl2', 'Hn1', 'Hn2',
|
||||
'Tpt1', 'Tpt2', 'Tbn1', 'Tbn2', 'Tuba',
|
||||
'Timp', 'Drum', 'Perc'),
|
||||
'RWE': ('Fl1', 'Fl2', 'Cl', 'Tbn', 'Vln1', 'Vln2', 'Vla', 'Vc'),
|
||||
'Custom': ()
|
||||
'Custom': (),
|
||||
}
|
||||
'''
|
||||
|
||||
DOCTYPES = [
|
||||
(1, 'PDF'),
|
||||
(2, 'Audio'),
|
||||
(3, 'Video'),
|
||||
(4, 'Source'),
|
||||
]
|
||||
|
||||
LICENCE_TYPES = [
|
||||
(2, 'Public Domain'),
|
||||
(4, 'Copyright Expired'),
|
||||
(6, 'Copyrighted'),
|
||||
(10, 'Internal use only'),
|
||||
]
|
||||
|
||||
ACCESS_TYPES = [
|
||||
(1, 'Unlimited'),
|
||||
(2, 'Approval required'),
|
||||
]
|
||||
|
||||
def tag_to_instrument(tag):
|
||||
m = re.match(r'([A-Za-z]+)(\d*)', tag)
|
||||
@ -58,13 +201,18 @@ 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):
|
||||
"""
|
||||
Stores a list of instrument codes as a single entry (space delimited).
|
||||
Can be global or ensemble specific
|
||||
"""
|
||||
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(',') ]
|
||||
tags = [ t.strip() for t in self.instruments.split(' ') ]
|
||||
return [ (t, tag_to_instrument(t)) for t in tags if t ]
|
||||
|
||||
def save(self):
|
||||
@ -73,44 +221,21 @@ class Orchestration(models.Model):
|
||||
|
||||
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)
|
||||
composer = models.CharField(max_length=255, blank=True)
|
||||
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='Item', related_name="works")
|
||||
|
||||
class Meta:
|
||||
unique_together = ['ensemble', 'slug']
|
||||
|
||||
@property
|
||||
def parts(self):
|
||||
return Part.objects.filter(doc__work=self.pk)
|
||||
|
||||
@property
|
||||
def instruments(self):
|
||||
return self.orchestration.as_list()
|
||||
|
||||
def save(self):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super(Work, self).save()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
'''
|
||||
|
||||
class Item(models.Model):
|
||||
"""
|
||||
Item represents a Work in a Project e.g. item in set list or programme
|
||||
Item represents a specic version of 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)
|
||||
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name='project_items')
|
||||
checkout = models.DateTimeField()
|
||||
due = models.DateTimeField(null=True, blank=True)
|
||||
returned = models.DateTimeField(null=True, blank=True)
|
||||
approved_by = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
||||
order = models.SmallIntegerField(default=0)
|
||||
version = models.CharField(max_length=30, blank=True, help_text="Limited to specific version tag")
|
||||
|
||||
class Meta:
|
||||
ordering = ['order', 'work']
|
||||
@ -118,20 +243,151 @@ class Item(models.Model):
|
||||
def __str__(self):
|
||||
return f"<{self.project.slug}:{self.work.slug}>"
|
||||
|
||||
class Collection(models.Model):
|
||||
"""
|
||||
Storage location for works (physical or virtual)
|
||||
"""
|
||||
name = models.CharField(max_length=255, help_text="Name of the collection")
|
||||
administrators = models.ManyToManyField('auth.User', related_name="collections", help_text="Administrators for this collection")
|
||||
location = models.CharField(max_length=100, help_text="Physical location (institution, town...)")
|
||||
storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True, help_text="Storage for documents")
|
||||
notes = models.TextField(blank=True, help_text="Publicly visible notes about collection and loans policy")
|
||||
#ensembles = models.ManyToManyField('interface.Ensemble', related_name="collections", through='EnsembleAccess')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class EnsembleAccess(models.Model):
|
||||
"""
|
||||
Can have different access levels to a collection
|
||||
"""
|
||||
ensemble = models.ForeignKey('interface.Ensemble', on_delete=models.CASCADE, related_name="allowed_collections")
|
||||
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="allowed_ensembles")
|
||||
access_type = models.PositiveSmallIntegerField(choices=ACCESS_TYPES, default=2)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Ensemble access"
|
||||
|
||||
class Work(models.Model):
|
||||
"""
|
||||
A musical work 'owned' by a collection from a licencing perspective.
|
||||
"""
|
||||
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works", help_text="Owner")
|
||||
slug = models.SlugField(max_length=100, editable=False)
|
||||
name = models.CharField(max_length=255)
|
||||
edition = models.CharField(max_length=100, blank=True, help_text="Edition details")
|
||||
parent = models.ForeignKey('Work', null=True, blank=True, on_delete=models.SET_NULL, related_name="related_works",
|
||||
help_text="Arrangement of another work or part of an anthology")
|
||||
composer = models.CharField(max_length=255, blank=True, help_text="Use Composer / Arranger format")
|
||||
#orchestration = models.ForeignKey(Orchestration, null=True, on_delete=models.SET_NULL, related_name='works', blank=True)
|
||||
orchestration = models.CharField(max_length=255, blank=True, help_text="IMDB format instrumentation")
|
||||
parts = models.JSONField(null=True, blank=True)
|
||||
|
||||
# Collection details
|
||||
collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name="works")
|
||||
code = models.CharField(max_length=100, blank=True, help_text="Collection specific code or number")
|
||||
licence = models.PositiveSmallIntegerField(choices=LICENCE_TYPES, default=6, help_text="Copyright status")
|
||||
max_loans = models.IntegerField(default=1, help_text="How many projects can this work be attached to")
|
||||
|
||||
# Extra info
|
||||
running_time = models.IntegerField(null=True, blank=True, help_text="Running time in seconds")
|
||||
notes = models.TextField(blank=True)
|
||||
tag_list = models.CharField(max_length=255, blank=True, help_text="Multiple tags for the work")
|
||||
|
||||
projects = models.ManyToManyField('interface.Project', through='Item', related_name="works", help_text="Current usage")
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
if self.running_time is None:
|
||||
return "-:--"
|
||||
return "{0:d}:{1:02d}".format(int(self.running_time / 60), self.running_time % 60)
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return self.tag_list.split(';') if self.tag_list else []
|
||||
|
||||
@tags.setter
|
||||
def set_tags(self, tags):
|
||||
self.tag_list = ";".join(tags)
|
||||
|
||||
@property
|
||||
def digital_parts(self):
|
||||
return Part.objects.filter(doc__work=self.pk)
|
||||
|
||||
@property
|
||||
def physical_parts(self):
|
||||
if not self.parts:
|
||||
return []
|
||||
return [ (tag_to_instrument(k), v) for (k, v) in self.parts.items() ]
|
||||
|
||||
#@property
|
||||
#def instruments(self):
|
||||
# return self.orchestration.as_list()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super(Work, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def active_projects(self):
|
||||
return self.projects.filter(active=True)
|
||||
|
||||
@property
|
||||
def current_loans(self):
|
||||
return self.project_items.filter(checkout__lte=now(), returned=None).select_related('project')
|
||||
|
||||
@cached_property
|
||||
def loans(self):
|
||||
try:
|
||||
return self.loan_count
|
||||
except AttributeError:
|
||||
return self.project_items.filter(checkout__lte=now(), returned=None).count()
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
if self.max_loans < 0:
|
||||
return True
|
||||
return self.max_loans > self.loans
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
if self.max_loans < 0:
|
||||
return 'Unlimited'
|
||||
a = self.max_loans - self.loans
|
||||
return '{0} of {1}'.format(max(a, 0), self.max_loans)
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return f"{self.collection.pk:03d}-{self.pk:03d}"
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.composer})"
|
||||
|
||||
def doc_upload_filename(doc, filename):
|
||||
storage = doc.work.ensemble.storage
|
||||
storage = doc.work.collection.storage
|
||||
if not storage:
|
||||
raise RuntimeError("Storage not set")
|
||||
return f'{storage}:{doc.work.ensemble.slug}/works/{doc.work.slug}/{filename}'
|
||||
raise RuntimeError("Collection has no storage attached")
|
||||
return f'{storage}:works/{doc.work.slug}-{doc.work.pk}/{filename}'
|
||||
|
||||
class Document(models.Model):
|
||||
"""
|
||||
Document represents a single file stored in the storage backend.
|
||||
"""
|
||||
work = models.ForeignKey('Work', on_delete=models.CASCADE, related_name="docs")
|
||||
doctype = models.PositiveSmallIntegerField(choices=DOCTYPES, default=1)
|
||||
upload = models.FileField(upload_to=doc_upload_filename, storage=library_storage)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
version = models.CharField(max_length=30, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.upload.name
|
||||
|
||||
class Part(models.Model):
|
||||
"""
|
||||
Part is a tagged portion of a Document
|
||||
"""
|
||||
doc = models.ForeignKey(Document, on_delete=models.CASCADE, related_name="parts")
|
||||
tag = models.SlugField(max_length=20)
|
||||
start = models.SmallIntegerField(null=True, blank=True)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
|
||||
{% block admin %}
|
||||
<a href="{% url 'item_list_manage' project.pk %}"><i class="fas fa-list"></i><span class="smhide admin">Change items</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 %}
|
||||
@ -23,15 +23,22 @@
|
||||
<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="">
|
||||
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th/>
|
||||
<th>Piece</th>
|
||||
<th>Part</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }})</td>
|
||||
<td>{{ forloop.counter }}.</td>
|
||||
<td>
|
||||
<a href="{% url 'work_detail' item.work.pk %}">{{ item.work.name }}</a>
|
||||
</td>
|
||||
<td class="">
|
||||
<td class="select-cell">
|
||||
<input type="hidden" name="works" value="{{ item.work.pk }}"/>
|
||||
<select name="instruments">
|
||||
<option value='-'>None</option>
|
||||
|
||||
@ -6,18 +6,19 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<table style="max-width: 600px; margin: 10pt auto;" class="item-table">
|
||||
<table style="max-width: 600px; margin: 10pt auto;" class="zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Time</th>
|
||||
<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>{{ item.work.duration }}</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>
|
||||
|
||||
@ -2,5 +2,5 @@
|
||||
<a href="{% url 'item_list' project=project.pk %}">My Music</a>
|
||||
{% endif %}
|
||||
{% if request.is_admin %}
|
||||
<a href="{% url 'work_list' %}">Works</a>
|
||||
<a href="{% url 'work_list' %}">Library</a>
|
||||
{% endif %}
|
||||
@ -6,23 +6,84 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<h2><a href="{% url 'work_list' %}">Works</a> / {{ work.name }}</h2>
|
||||
<h2>{{ work.name }} {% if work.running_time %}({{ work.duration }}){% endif %} <small>[{{ work.identifier }}]</small></h2>
|
||||
<h4>{{ work.composer }}{% if work.version %} - {{ work.version }}{% endif %}</h4>
|
||||
<p>{{ work.notes }}</p>
|
||||
<h3>Parts</h3>
|
||||
<ul>
|
||||
{% for part in work.parts %}
|
||||
<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>
|
||||
{% if work.collection %}
|
||||
<p>Location: {{ work.collection }} [{{ work.collection_index }}]</p>
|
||||
{% endif %}
|
||||
|
||||
<h3>Original Documents</h3>
|
||||
{% if work.parent %}
|
||||
<p>From <a href="{% url 'work_detail' work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if work.related_works.count %}
|
||||
<h3>Related</h3>
|
||||
<ul>
|
||||
{% for related in work.related_works.all %}
|
||||
<li><a href="{% url 'work_detail' related.pk %}">{{ related.name }} - {{ related.composer }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h4>Loans
|
||||
{% if request.is_admin %}
|
||||
<a href="{% url 'work_add_to_project' work.pk %}"><i class="fas fa-plus-circle"></i></a>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<table style="margin: 10px auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ensemble</th>
|
||||
<th>Project</th>
|
||||
<th>Checked Out</th>
|
||||
<th>Due Back</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in work.current_loans %}
|
||||
<tr>
|
||||
<td><a href="{% url 'ensemble_detail' item.project.ensemble_id %}">{{ item.project.ensemble.name }}</a></td>
|
||||
<td><a href="{% url 'project_detail' item.project.pk %}">{{ item.project.name }}</a></td>
|
||||
<td>{{ item.checkout.date|date:"d/m/Y" }}</td>
|
||||
<td>{{ item.due.date|date:"d/m/Y" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td>No current loans</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Printed Parts</h4>
|
||||
<p>
|
||||
{% for inst, c in work.physical_parts %}
|
||||
<span class="badge">{{ inst }} ({{ c }})</span>
|
||||
{% empty %}
|
||||
No physical parts available
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
||||
<h4>Digital Parts
|
||||
<a href="{% url 'work_partset' pk=work.pk %}" title="Print part set"><i class="fas fa-print"></i></a>
|
||||
</h4>
|
||||
<p>
|
||||
{% for part in work.digital_parts %}
|
||||
<a class="badge" href="{% url 'part_download' pk=part.pk filename=part.filename %}"
|
||||
target="part_{{ part.pk }}" rel="">{{ part.instrument }}</a>
|
||||
{% empty %}
|
||||
No digital parts available
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
||||
<h4>Documents</h4>
|
||||
<ul>
|
||||
{% for doc in work.docs.all %}
|
||||
<li>
|
||||
<a href="{% url 'document_download' pk=doc.pk %}" target="_blank">{{ doc.upload.name|basename }}</a>
|
||||
|
||||
[{{ doc.parts.count }} parts]
|
||||
{% with parts=doc.parts.count %}
|
||||
{% if parts %}[{{ parts }} parts]{% endif %}
|
||||
{% endwith %}
|
||||
{% if request.is_admin %}
|
||||
<a href="{% url 'document_annotate' pk=doc.pk %}"><i class="fas fa-tags"></i></a>
|
||||
{% endif %}
|
||||
|
||||
@ -1,25 +1,61 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
{% load url_tools %}
|
||||
|
||||
{% block admin %}
|
||||
<a href="{% url 'work_add' %}"><i class="fas fa-plus-circle"></i> Add new</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<h2>Works</h2>
|
||||
<table style="max-width: 800px; margin: 10pt auto;">
|
||||
<h2>Library for {{ view.ensemble }}</h2>
|
||||
<div style="margin-bottom: 1em;">
|
||||
<form method="GET">
|
||||
<input name="filter" type="text" placeholder="Filter" value="{{ request.GET.filter }}"/>
|
||||
<a href="?">Clear</a>
|
||||
</form>
|
||||
</div>
|
||||
<table class="zebra wide">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Work</th>
|
||||
<th>Composer</th>
|
||||
<th class="smhide">Edition</th>
|
||||
<th class="smhide">Orchestration</th>
|
||||
<th class="smhide">Collection</th>
|
||||
<th>Copies</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for work in object_list %}
|
||||
{% with work.docs.count as doc_count %}
|
||||
{% with work.parts.count as part_count %}
|
||||
<tr>
|
||||
<td><a href="{% url 'work_detail' pk=work.pk %}">{{ work.name }}</a></td>
|
||||
<td>{{ doc_count }} file{{ doc_count|pluralize }} with {{ part_count }} part{{ part_count|pluralize }}</td>
|
||||
<td title="{{ work.composer }}">{{ work.composer|truncatewords:3 }}</td>
|
||||
<td class="smhide" title="{{ work.edition }}">{{ work.edition|truncatewords:2 }}</td>
|
||||
<td class="smhid" title="{{ work.orchestration }}">{{ work.orchestration|truncatewords:2}}</td>
|
||||
<td class="smhide">{{ work.collection.name }}</td>
|
||||
<td style="color: {{ work.is_available|yesno:'green,red' }};">{{ work.available }}</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<tr><td colspan="4">No works found</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination" style="text-align: right;">
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="{% url_update page=1 %}" title="First">«</a>
|
||||
<a href="{% url_update page=page_obj.previous_page_number %}" title="Previous">‹</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="{% url_update page=page_obj.next_page_number %}" title="Next">›</a>
|
||||
<a href="{% url_update page=page_obj.paginator.num_pages %}" title="Last">»</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@ -1,3 +1,22 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from django.contrib.auth.models import User
|
||||
from interface.models import Ensemble, Project
|
||||
from . import models
|
||||
|
||||
class IntegrationTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.homer = User.objects.create(username='homer')
|
||||
self.ned = User.objects.create(username="ned")
|
||||
self.lisa = User.objects.create(username="lisa")
|
||||
self.dewey = User.objects.create(username="dewey")
|
||||
|
||||
self.be_sharps = self.homer.ensembles.create(name='Be Sharps', code="barbershop")
|
||||
self.sesd = self.dewey.ensembles.create(name="Springfield Elementary School Band", code="sax")
|
||||
|
||||
self.sel = self.lisa.collections.create(name="Springfield Elementary Library")
|
||||
self.flanders = self.ned.collections.create(name="Neds Shed")
|
||||
|
||||
def test_integration(self):
|
||||
pass
|
||||
|
||||
@ -13,8 +13,12 @@ urlpatterns = [
|
||||
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/works/<int:pk>/add_to_project', views.WorkAddToProject.as_view(), name="work_add_to_project"),
|
||||
path('library/documents/<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"),
|
||||
path('library/parts/<int:pk>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
|
||||
]
|
||||
]
|
||||
|
||||
from django.views.static import serve
|
||||
urlpatterns.append(path('localstorage/<path:path>', serve, {'document_root': 'local_storage'}))
|
||||
@ -4,6 +4,8 @@ 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
|
||||
from django.db.models import Q, Count
|
||||
from django.utils.timezone import now
|
||||
|
||||
import json
|
||||
|
||||
@ -98,9 +100,20 @@ class ProjectItemAddView(ProjectMixin, UpdateView):
|
||||
return self.get_project()
|
||||
|
||||
class WorkListView(EnsembleMixin, ListView):
|
||||
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
return Work.objects.filter(ensemble=self.request.ensemble_id).order_by('name')
|
||||
#works = Work.objects.filter(collection__ensembles=self.request.ensemble_id).order_by('name').select_related('collection')
|
||||
works = Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id).order_by('name').select_related('collection')
|
||||
|
||||
loan_count = Count('project_items', Q(project_items__checkout__lte=now(), project_items__returned=None))
|
||||
works = works.annotate(loan_count=loan_count)
|
||||
|
||||
q = self.request.GET.get('filter')
|
||||
if q:
|
||||
works = works.filter(Q(name__contains=q) | Q(composer__contains=q) | Q(tag_list__contains=q))
|
||||
|
||||
return works
|
||||
|
||||
class WorkAddView(EnsembleMixin, FormView):
|
||||
template_name = "interface/default_form.html"
|
||||
@ -108,34 +121,59 @@ class WorkAddView(EnsembleMixin, FormView):
|
||||
|
||||
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)
|
||||
#qs = f.fields['orchestration'].queryset
|
||||
#f.fields['orchestration'].queryset = qs.filter(ensemble_id__isnull=True) | qs.filter(ensemble_id=self.request.ensemble_id)
|
||||
qs = f.fields['collection'].queryset
|
||||
qs = qs.filter(administrators=self.request.user)
|
||||
f.fields['collection'].queryset = qs
|
||||
return f
|
||||
|
||||
def form_valid(self, form):
|
||||
obj = form.save(commit=False)
|
||||
obj.ensemble_id = self.request.ensemble_id
|
||||
try:
|
||||
obj.save()
|
||||
except IntegrityError:
|
||||
form.add_error('name', 'Name must be unique')
|
||||
return self.form_invalid(form)
|
||||
work = form.save(commit=False)
|
||||
work.ensemble_id = self.request.ensemble_id
|
||||
work.save()
|
||||
|
||||
# handle the files
|
||||
uploads = self.request.FILES.getlist('uploads')
|
||||
docs = []
|
||||
for f in uploads:
|
||||
docs.append(obj.docs.create(upload=f).pk)
|
||||
docs.append(work.docs.create(upload=f).pk)
|
||||
|
||||
if len(docs) == 1:
|
||||
return redirect('document_annotate', docs[0])
|
||||
else:
|
||||
return redirect('work_detail', pk=obj.pk)
|
||||
return redirect('work_detail', pk=work.pk)
|
||||
|
||||
class WorkDetailView(EnsembleMixin, DetailView):
|
||||
|
||||
def get_queryset(self):
|
||||
return Work.objects.filter(ensemble=self.request.ensemble_id)
|
||||
return Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id)
|
||||
|
||||
class WorkAddToProject(EnsembleMixin, FormView):
|
||||
form_class = forms.ProjectSelectForm
|
||||
template_name = "interface/default_form.html"
|
||||
title = "Select project to add work to"
|
||||
|
||||
def get_object(self):
|
||||
return Work.objects.get(pk=self.kwargs['pk'])
|
||||
|
||||
def get_form(self):
|
||||
f = super(WorkAddToProject, self).get_form()
|
||||
qs = f.fields['project'].queryset
|
||||
|
||||
work = self.get_object()
|
||||
qs = qs.filter(ensemble_id=self.request.ensemble_id).exclude(pk__in=work.projects.all())
|
||||
|
||||
f.fields['project'].queryset = qs
|
||||
return f
|
||||
|
||||
def form_valid(self, form):
|
||||
work = self.get_object()
|
||||
project = form.cleaned_data['project']
|
||||
work.project_items.create(project=project, approved_by=self.request.user, checkout=now())
|
||||
return redirect('item_list', project=project.pk)
|
||||
|
||||
|
||||
|
||||
class WorkPartSetView(EnsembleMixin, DetailView):
|
||||
template_name = "library/work_partset.html"
|
||||
@ -162,7 +200,7 @@ class WorkPartSetView(EnsembleMixin, DetailView):
|
||||
return response
|
||||
|
||||
def get_queryset(self):
|
||||
return Work.objects.filter(ensemble_id=self.request.ensemble_id)
|
||||
return Version.objects.filter(work__ensemble_id=self.request.ensemble_id)
|
||||
|
||||
class DocumentDetailView(EnsembleMixin, DetailView):
|
||||
|
||||
@ -192,7 +230,7 @@ class DocumentDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
||||
return redirect(self.object.upload.url)
|
||||
|
||||
def get_queryset(self):
|
||||
return Document.objects.filter(work__ensemble=self.request.ensemble_id)
|
||||
return Document.objects.filter(work__collection__allowed_ensembles__ensemble=self.request.ensemble_id)
|
||||
|
||||
class DocumentAnnotateView(EnsembleMixin, DetailView):
|
||||
template_name = 'library/document_annotate.html'
|
||||
@ -228,11 +266,11 @@ class DocumentAnnotateView(EnsembleMixin, DetailView):
|
||||
for i in range(part.start, (part.end or part.start)+1):
|
||||
pages[i] = part.tag
|
||||
|
||||
data['json_data'] = {'pageTags': pages, 'instruments': data['document'].work.instruments}
|
||||
data['json_data'] = {'pageTags': pages, 'instruments': data['document'].work.orchestration}
|
||||
return data
|
||||
|
||||
def get_queryset(self):
|
||||
return Document.objects.filter(work__ensemble=self.request.ensemble_id).select_related('work')
|
||||
return Document.objects.filter(work__collection__allowed_ensembles__ensemble=self.request.ensemble_id).select_related('work')
|
||||
|
||||
|
||||
class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
||||
@ -251,4 +289,4 @@ class PartDownloadView(EnsembleMixin, SingleObjectMixin, View):
|
||||
return response
|
||||
|
||||
def get_queryset(self):
|
||||
return Part.objects.filter(doc__work__ensemble=self.request.ensemble_id).select_related('doc', 'doc__work')
|
||||
return Part.objects.filter(doc__work__collection__allowed_ensembles__ensemble=self.request.ensemble_id).select_related('doc', 'doc__work')
|
||||
@ -84,6 +84,7 @@ DATABASES = {
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
|
||||
@ -126,4 +127,4 @@ STATIC_URL = '/static/'
|
||||
# Need to set this
|
||||
AWS_BUCKET = ''
|
||||
|
||||
MEDIA_ROOT = 'media'
|
||||
MEDIA_ROOT = 'media'
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
asgiref==3.2.10
|
||||
boto3==1.15.11
|
||||
botocore==1.18.11
|
||||
Django==3.1.1
|
||||
asgiref==3.4.1
|
||||
boto3==1.18.34
|
||||
botocore==1.21.34
|
||||
Django==3.2.7
|
||||
django-byostorage @ git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@c67d636d2457faa57644cd812ca1b5a916e23766
|
||||
django-markdown2==0.3.1
|
||||
jmespath==0.10.0
|
||||
markdown2==2.3.9
|
||||
python-dateutil==2.8.1
|
||||
pytz==2020.1
|
||||
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@master#egg=django_byostorage
|
||||
markdown2==2.4.1
|
||||
python-dateutil==2.8.2
|
||||
pytz==2021.1
|
||||
s3transfer==0.5.0
|
||||
six==1.16.0
|
||||
sqlparse==0.4.1
|
||||
urllib3==1.26.6
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user