Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1ef2b9dac | ||
|
|
5c56d40bf8 | ||
|
|
a1341d1edc | ||
|
|
4d964291b2 | ||
|
|
ee5305ba6c | ||
|
|
147c84550c | ||
|
|
596445061f | ||
|
|
02858a76c0 | ||
|
|
1bcec919cf | ||
|
|
78789c02ed |
@@ -1,10 +1,13 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
*.swp
|
||||||
credentials.json
|
credentials.json
|
||||||
credentials
|
credentials
|
||||||
local_settings.py
|
local_settings.py
|
||||||
.coverage
|
.coverage
|
||||||
|
Session.vim
|
||||||
|
poetry.lock
|
||||||
/env
|
/env
|
||||||
/data
|
/data
|
||||||
/old
|
/old
|
||||||
|
|||||||
@@ -6,20 +6,23 @@ No registration required for ensemble participants - just a one time code and pa
|
|||||||
|
|
||||||
### Library App
|
### Library App
|
||||||
|
|
||||||
Store all your scores on your own cloud account (Amazon S3, Google Files etc). Tag up the scores so you can generate
|
Store all your scores on your own cloud account (Amazon S3, Google Files etc).
|
||||||
custom part sets and assign them to projects so people can easily print just their parts.
|
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
|
### Submissions App
|
||||||
|
|
||||||
Accept video/audio submissions direct to your cloud storage. Was developed and used during 2020 lockdown period for
|
Accept video/audio submissions direct to your cloud storage. Was developed and
|
||||||
virtual choirs/orchestras but could have more uses.
|
used during 2020 lockdown period for virtual choirs/orchestras but could have more uses.
|
||||||
|
|
||||||
### S3 Setup
|
### S3 Setup
|
||||||
|
|
||||||
#### Bucket setup [virtual-orchestra]
|
#### Bucket setup [virtual-orchestra]
|
||||||
|
|
||||||
Default block public access
|
Default block public access
|
||||||
|
|
||||||
Permissions -> CORS
|
Permissions -> CORS
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||||
@@ -40,6 +43,7 @@ Permissions -> CORS
|
|||||||
|
|
||||||
User
|
User
|
||||||
Create with programatic access (copy keys) and an inline policy for the bucket.
|
Create with programatic access (copy keys) and an inline policy for the bucket.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Version": "2012-10-17",
|
"Version": "2012-10-17",
|
||||||
@@ -63,4 +67,5 @@ Create with programatic access (copy keys) and an inline policy for the bucket.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
3.
|
3.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## Polyphonic TODO
|
||||||
|
|
||||||
|
## Core interface
|
||||||
|
|
||||||
|
* Shift from crispy forms to native component templates
|
||||||
|
* Make long running calls async (Django 5)
|
||||||
|
* Deprecate Django 4 portions
|
||||||
|
|
||||||
|
### Library App
|
||||||
|
|
||||||
|
* Remove music tags and replace with strings vn1 -> 'Violin 1'
|
||||||
|
* GDrive selector
|
||||||
|
* Move upload to modal from 'Upload' button
|
||||||
|
* Tagging app - migrate to AlpineJS
|
||||||
|
* Allow other tags (movements, sections, pieces)
|
||||||
|
|
||||||
|
### Submissions App
|
||||||
|
|
||||||
|
* None currently pending
|
||||||
@@ -55,4 +55,4 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+145
-129
@@ -1,17 +1,15 @@
|
|||||||
"""
|
""" """
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
# pyright: basic
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
from django.views.generic.edit import CreateView, UpdateView, FormMixin
|
from django.views.generic.edit import CreateView, UpdateView
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.db.models import Q
|
from django.http.request import HttpRequest
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from markdown2 import markdown
|
from markdown2 import markdown
|
||||||
|
|
||||||
@@ -19,8 +17,10 @@ from . import models, forms
|
|||||||
from interface.utils import check_signed_url
|
from interface.utils import check_signed_url
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AuthorizedResourceMixin(object):
|
class AuthorizedResourceMixin(object):
|
||||||
"""
|
"""
|
||||||
Handles these parts of the permission system:
|
Handles these parts of the permission system:
|
||||||
@@ -29,47 +29,51 @@ class AuthorizedResourceMixin(object):
|
|||||||
* Admin enforcing
|
* Admin enforcing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SESSION_KEY = 'authorized'
|
request: HttpRequest
|
||||||
|
kwargs: dict
|
||||||
|
_authorized: dict
|
||||||
|
|
||||||
|
SESSION_KEY = "authorized"
|
||||||
admin_required = False
|
admin_required = False
|
||||||
|
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
"By default check if superuser or a signed request"
|
"By default check if superuser or a signed request"
|
||||||
if self.request.is_admin:
|
if self.request.is_admin: # type: ignore
|
||||||
#logger.debug("is_authorized: superuser")
|
# logger.debug("is_authorized: superuser")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if 'sig' in self.request.GET:
|
if "sig" in self.request.GET:
|
||||||
check_signed_url(self.request.get_full_path())
|
check_signed_url(self.request.get_full_path())
|
||||||
self.on_signed_request()
|
self.on_signed_request()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_signed_request(self):
|
def on_signed_request(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def is_authorized_key(self, resource, key, auth):
|
def is_authorized_key(self, resource, key, auth):
|
||||||
current = self.get_authorized_keys(resource).get(str(key), None)
|
current = self.get_authorized_keys(resource).get(str(key), None)
|
||||||
if current is None:
|
if current is None:
|
||||||
#logger.debug("is_authorized_key: %s %s not in session", resource, key)
|
# logger.debug("is_authorized_key: %s %s not in session", resource, key)
|
||||||
return False
|
return False
|
||||||
if auth == current:
|
if auth == current:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
#logger.info("Authorisation revoked")
|
# logger.info("Authorisation revoked")
|
||||||
self.del_authorized_key(resource, key)
|
self.del_authorized_key(resource, key)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_authorized_keys(self, resource):
|
def get_authorized_keys(self, resource):
|
||||||
'Returns a set of authorized keys for this resource'
|
"Returns a set of authorized keys for this resource"
|
||||||
return self._authorized.get(resource, {})
|
return self._authorized.get(resource, {})
|
||||||
|
|
||||||
def add_authorized_key(self, resource, key, auth):
|
def add_authorized_key(self, resource, key, auth):
|
||||||
'Adds a key to the authorized list for this resource'
|
"Adds a key to the authorized list for this resource"
|
||||||
current = self.get_authorized_keys(resource)
|
current = self.get_authorized_keys(resource)
|
||||||
current[str(key)] = auth
|
current[str(key)] = auth
|
||||||
self._authorized[resource] = current
|
self._authorized[resource] = current
|
||||||
self.request.session[self.SESSION_KEY] = self._authorized
|
self.request.session[self.SESSION_KEY] = self._authorized # type: ignore
|
||||||
|
|
||||||
def del_authorized_key(self, resource, key):
|
def del_authorized_key(self, resource, key):
|
||||||
logger.info("Revoking authorization for %s %s", resource, key)
|
logger.info("Revoking authorization for %s %s", resource, key)
|
||||||
@@ -80,39 +84,41 @@ class AuthorizedResourceMixin(object):
|
|||||||
self._authorized[resource] = current
|
self._authorized[resource] = current
|
||||||
else:
|
else:
|
||||||
self._authorized.pop(resource)
|
self._authorized.pop(resource)
|
||||||
self.request.session[self.SESSION_KEY] = self._authorized
|
self.request.session[self.SESSION_KEY] = self._authorized # type: ignore
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def request_denied(self):
|
def request_denied(self):
|
||||||
raise Http404("Either the given resource doesn't exist or you dont have access to it.")
|
raise Http404(
|
||||||
|
"Either the given resource doesn't exist or you dont have access to it."
|
||||||
|
)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self._authorized = request.session.get("authorized", {})
|
||||||
self._authorized = request.session.get('authorized', {})
|
|
||||||
request.is_admin = request.user.is_superuser
|
request.is_admin = request.user.is_superuser
|
||||||
|
|
||||||
if not self.is_authorized():
|
if not self.is_authorized():
|
||||||
return self.request_denied()
|
return self.request_denied()
|
||||||
|
|
||||||
if self.admin_required and not request.is_admin:
|
if self.admin_required and not request.is_admin:
|
||||||
return self.request_denied()
|
return self.request_denied()
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
# TODO: RevokeResourceView - increment nonce
|
# TODO: RevokeResourceView - increment nonce
|
||||||
|
|
||||||
|
|
||||||
class ForgetResourceView(AuthorizedResourceMixin, RedirectView):
|
class ForgetResourceView(AuthorizedResourceMixin, RedirectView):
|
||||||
|
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_redirect_url(self, resource, key):
|
def get_redirect_url(self, resource, key):
|
||||||
self.del_authorized_key(resource, key)
|
self.del_authorized_key(resource, key)
|
||||||
return "/"
|
return "/"
|
||||||
|
|
||||||
|
|
||||||
class EnsembleMixin(AuthorizedResourceMixin):
|
class EnsembleMixin(AuthorizedResourceMixin):
|
||||||
|
ensemble_slug_kwarg = "ensemble"
|
||||||
ensemble_slug_kwarg = 'ensemble'
|
|
||||||
limited_project_access = False
|
limited_project_access = False
|
||||||
|
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
@@ -121,16 +127,16 @@ class EnsembleMixin(AuthorizedResourceMixin):
|
|||||||
|
|
||||||
if super().is_authorized():
|
if super().is_authorized():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.ensemble.has_admin(self.request.user):
|
if self.ensemble.has_admin(self.request.user): # type: ignore
|
||||||
self.request.is_admin = True
|
self.request.is_admin = True # type: ignore
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.is_authorized_key('ensemble', ensemble_slug, self.ensemble.nonce):
|
if self.is_authorized_key("ensemble", ensemble_slug, self.ensemble.nonce):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
authorized = set([ int(x) for x in self.get_authorized_keys('project').keys() ])
|
authorized = set([int(x) for x in self.get_authorized_keys("project").keys()])
|
||||||
projects = set(self.ensemble.projects.values_list('pk', flat=True))
|
projects = set(self.ensemble.projects.values_list("pk", flat=True))
|
||||||
logger.debug("is_authorized: %r & %r", authorized, projects)
|
logger.debug("is_authorized: %r & %r", authorized, projects)
|
||||||
if authorized & projects:
|
if authorized & projects:
|
||||||
self.limited_project_access = True
|
self.limited_project_access = True
|
||||||
@@ -138,131 +144,131 @@ class EnsembleMixin(AuthorizedResourceMixin):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self, queryset=None):
|
||||||
return self.ensemble
|
return self.ensemble
|
||||||
|
|
||||||
|
|
||||||
class ProjectMixin(AuthorizedResourceMixin):
|
class ProjectMixin(AuthorizedResourceMixin):
|
||||||
|
project_kwarg = "project"
|
||||||
project_kwarg = 'project'
|
|
||||||
|
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
project_id = self.kwargs[self.project_kwarg]
|
project_id = self.kwargs[self.project_kwarg]
|
||||||
self.project = get_object_or_404(models.Project.objects.select_related('ensemble'), pk=project_id)
|
self.project = get_object_or_404(
|
||||||
|
models.Project.objects.select_related("ensemble"), pk=project_id
|
||||||
|
)
|
||||||
|
|
||||||
if super().is_authorized():
|
if super().is_authorized():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# check if the current user is an admin on the ensemble
|
# check if the current user is an admin on the ensemble
|
||||||
if self.project.ensemble.has_admin(self.request.user):
|
if self.project.ensemble.has_admin(self.request.user): # type: ignore
|
||||||
logger.debug("is_authorized: ensemble admin for project")
|
logger.debug("is_authorized: ensemble admin for project")
|
||||||
self.request.is_admin = True
|
self.request.is_admin = True # type: ignore
|
||||||
return True
|
|
||||||
|
|
||||||
if self.is_authorized_key('ensemble', self.project.ensemble.pk, self.project.ensemble.nonce):
|
|
||||||
logger.debug('is_authorized: has ensemble link for project')
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.is_authorized_key('project', project_id, self.project.nonce):
|
if self.is_authorized_key(
|
||||||
logger.debug('is_authorized: has project link')
|
"ensemble", self.project.ensemble.pk, self.project.ensemble.nonce
|
||||||
|
):
|
||||||
|
logger.debug("is_authorized: has ensemble link for project")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.is_authorized_key("project", project_id, self.project.nonce):
|
||||||
|
logger.debug("is_authorized: has project link")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# filter any generated querysets
|
# filter any generated querysets
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(project=self.project)
|
return super().get_queryset().filter(project=self.project) # type: ignore
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs) # type: ignore
|
||||||
if 'project' in self.kwargs:
|
if "project" in self.kwargs:
|
||||||
context['project'] = self.project
|
context["project"] = self.project
|
||||||
context['modules'] = self.project.active_modules
|
context["modules"] = self.project.active_modules
|
||||||
return context
|
return context
|
||||||
|
|
||||||
class CrispyFormMixin(object):
|
|
||||||
|
|
||||||
|
class CrispyFormMixin(object):
|
||||||
cancel_url = None
|
cancel_url = None
|
||||||
|
|
||||||
def get_cancel_url(self):
|
def get_cancel_url(self):
|
||||||
return self.cancel_url
|
return self.cancel_url
|
||||||
|
|
||||||
|
|
||||||
""" ENSEMBLE VIEWS """
|
""" ENSEMBLE VIEWS """
|
||||||
|
|
||||||
class EnsembleListView(AuthorizedResourceMixin, ListView):
|
|
||||||
|
|
||||||
|
class EnsembleListView(AuthorizedResourceMixin, ListView):
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return models.Ensemble.objects.for_user(self.request.user,
|
return models.Ensemble.objects.for_user(
|
||||||
self.get_authorized_keys('ensemble').keys(),
|
self.request.user, # type: ignore
|
||||||
self.get_authorized_keys('project').keys())
|
self.get_authorized_keys("ensemble").keys(),
|
||||||
#ensembles = models.Ensemble.objects.all()
|
self.get_authorized_keys("project").keys(),
|
||||||
|
)
|
||||||
|
|
||||||
#if self.request.is_admin:
|
|
||||||
# return ensembles
|
|
||||||
|
|
||||||
# limit to registered ensembles
|
|
||||||
#f = Q(slug__in=self.get_authorized_keys('ensemble').keys()) | Q(projects__in=self.get_authorized_keys('project').keys())
|
|
||||||
|
|
||||||
# or ensembles where the user is admin
|
|
||||||
#if self.request.user.is_authenticated:
|
|
||||||
# f |= Q(admins=self.request.user.pk)
|
|
||||||
|
|
||||||
#return ensembles.filter(f).distinct()
|
|
||||||
|
|
||||||
class EnsembleDetailView(EnsembleMixin, DetailView):
|
class EnsembleDetailView(EnsembleMixin, DetailView):
|
||||||
|
|
||||||
def request_denied(self):
|
def request_denied(self):
|
||||||
if 'auth' in self.request.GET:
|
if "auth" in self.request.GET:
|
||||||
if self.request.GET['auth'] != self.ensemble.auth():
|
if self.request.GET["auth"] != self.ensemble.auth():
|
||||||
raise SuspiciousOperation("Bad ensemble link")
|
raise SuspiciousOperation("Bad ensemble link")
|
||||||
self.add_authorized_key('ensemble', self.ensemble.slug, self.ensemble.nonce)
|
self.add_authorized_key("ensemble", self.ensemble.slug, self.ensemble.nonce)
|
||||||
return HttpResponseRedirect(resolve_url('ensemble_detail', self.ensemble.slug))
|
return HttpResponseRedirect(
|
||||||
|
resolve_url("ensemble_detail", self.ensemble.slug)
|
||||||
|
)
|
||||||
|
|
||||||
return super().request_denied()
|
return super().request_denied()
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
data = super().get_context_data(**kwargs)
|
data = super().get_context_data(**kwargs)
|
||||||
inactive = 'inactive' in self.request.GET and self.request.is_admin
|
inactive = "inactive" in self.request.GET and self.request.is_admin # type: ignore
|
||||||
|
|
||||||
projects = self.ensemble.projects.all()
|
projects = self.ensemble.projects.all()
|
||||||
if self.limited_project_access:
|
if self.limited_project_access:
|
||||||
projects = projects.filter(pk__in=self.get_authorized_keys('project').keys())
|
projects = projects.filter(
|
||||||
|
pk__in=self.get_authorized_keys("project").keys()
|
||||||
|
)
|
||||||
|
|
||||||
if inactive:
|
if inactive:
|
||||||
projects = projects.order_by('-pk')
|
projects = projects.order_by("-pk")
|
||||||
else:
|
else:
|
||||||
projects = projects.active().current()
|
projects = projects.active().current()
|
||||||
|
|
||||||
data['inactive'] = inactive
|
data["inactive"] = inactive
|
||||||
data['object_list'] = projects
|
data["object_list"] = projects
|
||||||
if self.request.is_admin:
|
if self.request.is_admin: # type: ignore
|
||||||
data['ensemble_link'] = self.request.path + "?auth=" + self.ensemble.auth()
|
data["ensemble_link"] = self.request.path + "?auth=" + self.ensemble.auth()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class EnsembleRevokeView(SingleObjectMixin, RedirectView):
|
|
||||||
|
|
||||||
|
class EnsembleRevokeView(SingleObjectMixin, RedirectView):
|
||||||
def get_redirect_url(self):
|
def get_redirect_url(self):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
""" PROJECT VIEWS """
|
""" PROJECT VIEWS """
|
||||||
|
|
||||||
class ProjectListView(ProjectMixin, ListView):
|
|
||||||
|
|
||||||
|
class ProjectListView(ProjectMixin, ListView):
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_project_queryset(self):
|
def get_project_queryset(self):
|
||||||
return models.Project.objects.for_user(self.request.user,
|
return models.Project.objects.for_user(
|
||||||
self.get_authorized_keys('project'),
|
self.request.user, # type: ignore
|
||||||
self.get_authorized_keys('ensemble'))
|
self.get_authorized_keys("project"),
|
||||||
|
self.get_authorized_keys("ensemble"),
|
||||||
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.get_project_queryset().current().active()
|
return self.get_project_queryset().current().active()
|
||||||
|
|
||||||
|
|
||||||
class ProjectCreateView(EnsembleMixin, CreateView):
|
class ProjectCreateView(EnsembleMixin, CreateView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
model = models.Project
|
model = models.Project
|
||||||
@@ -270,26 +276,25 @@ class ProjectCreateView(EnsembleMixin, CreateView):
|
|||||||
title = "Add a new project"
|
title = "Add a new project"
|
||||||
form_class = forms.ProjectForm
|
form_class = forms.ProjectForm
|
||||||
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save(commit=False)
|
self.object = form.save(commit=False)
|
||||||
self.object.ensemble = self.ensemble
|
self.object.ensemble = self.ensemble
|
||||||
self.object.owner = self.request.user
|
self.object.owner = self.request.user # type: ignore
|
||||||
self.object.save()
|
self.object.save()
|
||||||
|
|
||||||
for module in form.cleaned_data['modules']:
|
for module in form.cleaned_data["modules"]:
|
||||||
self.object.modules.create(name=module)
|
self.object.modules.create(name=module)
|
||||||
|
|
||||||
return redirect('project_detail', project=self.object.pk)
|
return redirect("project_detail", project=self.object.pk)
|
||||||
|
|
||||||
|
|
||||||
class ProjectDetailView(ProjectMixin, DetailView):
|
class ProjectDetailView(ProjectMixin, DetailView):
|
||||||
|
|
||||||
def request_denied(self):
|
def request_denied(self):
|
||||||
if 'auth' in self.request.GET:
|
if "auth" in self.request.GET:
|
||||||
if self.request.GET['auth'] != self.project.auth():
|
if self.request.GET["auth"] != self.project.auth():
|
||||||
raise SuspiciousOperation("Bad project link")
|
raise SuspiciousOperation("Bad project link")
|
||||||
self.add_authorized_key('project', self.project.pk, self.project.nonce)
|
self.add_authorized_key("project", self.project.pk, self.project.nonce)
|
||||||
return HttpResponseRedirect(resolve_url('project_detail', self.project.pk))
|
return HttpResponseRedirect(resolve_url("project_detail", self.project.pk))
|
||||||
return super().request_denied()
|
return super().request_denied()
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
@@ -297,15 +302,16 @@ class ProjectDetailView(ProjectMixin, DetailView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
data = super().get_context_data(**kwargs)
|
data = super().get_context_data(**kwargs)
|
||||||
if self.request.is_admin:
|
if self.request.is_admin: # type: ignore
|
||||||
#data['project_link'] = signed_url(f'{self.request.path}?nonce={self.project.nonce}')
|
# data['project_link'] = signed_url(f'{self.request.path}?nonce={self.project.nonce}')
|
||||||
data['project_link'] = self.request.path + "?auth=" + self.project.auth()
|
data["project_link"] = self.request.path + "?auth=" + self.project.auth()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class ProjectUpdateView(ProjectMixin, UpdateView):
|
class ProjectUpdateView(ProjectMixin, UpdateView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
template_name = "interface/default_form.html"
|
template_name = "interface/default_form.html"
|
||||||
pk_url_kwarg = 'project'
|
pk_url_kwarg = "project"
|
||||||
form_class = forms.ProjectForm
|
form_class = forms.ProjectForm
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
@@ -313,26 +319,26 @@ class ProjectUpdateView(ProjectMixin, UpdateView):
|
|||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
data = super().get_initial()
|
data = super().get_initial()
|
||||||
data['modules'] = self.object.active_modules
|
data["modules"] = self.object.active_modules
|
||||||
print(data)
|
print(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save()
|
self.object = form.save()
|
||||||
current = set(self.object.active_modules)
|
current = set(self.object.active_modules)
|
||||||
desired = set(form.cleaned_data['modules'])
|
desired = set(form.cleaned_data["modules"])
|
||||||
self.object.modules.exclude(name__in=desired).delete()
|
self.object.modules.exclude(name__in=desired).delete()
|
||||||
for module in desired-current:
|
for module in desired - current:
|
||||||
self.object.modules.create(name=module)
|
self.object.modules.create(name=module)
|
||||||
return redirect('project_detail', self.kwargs['project'])
|
return redirect("project_detail", self.kwargs["project"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cancel_url(self):
|
def cancel_url(self):
|
||||||
return resolve_url('project_detail', self.kwargs['project'])
|
return resolve_url("project_detail", self.kwargs["project"])
|
||||||
|
|
||||||
|
|
||||||
# Old Makefile from submission module
|
# Old Makefile from submission module
|
||||||
#class ProjectMakefileView(EnsembleMixin, DetailView):
|
# class ProjectMakefileView(EnsembleMixin, DetailView):
|
||||||
# template_name = 'interface/project_submissions.mk'
|
# template_name = 'interface/project_submissions.mk'
|
||||||
# content_type = 'text/plain'
|
# content_type = 'text/plain'
|
||||||
#
|
#
|
||||||
@@ -359,29 +365,32 @@ class ProjectUpdateView(ProjectMixin, UpdateView):
|
|||||||
|
|
||||||
""" WIKI VIEWS """
|
""" WIKI VIEWS """
|
||||||
|
|
||||||
|
|
||||||
class WikiView(ProjectMixin, DetailView):
|
class WikiView(ProjectMixin, DetailView):
|
||||||
template_name = 'interface/wiki.html'
|
template_name = "interface/wiki.html"
|
||||||
model = models.WikiPage
|
model = models.WikiPage
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
data = super().get_context_data(**kwargs)
|
data = super().get_context_data(**kwargs)
|
||||||
data['wiki_html'] = markdown(self.object.markdown)
|
data["wiki_html"] = markdown(self.object.markdown)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class WikiCreateView(ProjectMixin, CreateView):
|
class WikiCreateView(ProjectMixin, CreateView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
model = models.WikiPage
|
model = models.WikiPage
|
||||||
template_name = 'interface/default_form.html'
|
template_name = "interface/default_form.html"
|
||||||
form_class = forms.WikiForm
|
form_class = forms.WikiForm
|
||||||
|
|
||||||
def cancel_url(self):
|
def cancel_url(self):
|
||||||
return resolve_url('project_detail', self.kwargs['project'])
|
return resolve_url("project_detail", self.kwargs["project"])
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save(commit=False)
|
self.object = form.save(commit=False)
|
||||||
self.object.project = self.project
|
self.object.project = self.project
|
||||||
self.object.save()
|
self.object.save()
|
||||||
return redirect('wiki', project=self.object.project_id, pk=self.object.pk)
|
return redirect("wiki", project=self.object.project_id, pk=self.object.pk)
|
||||||
|
|
||||||
|
|
||||||
class WikiEditView(ProjectMixin, UpdateView):
|
class WikiEditView(ProjectMixin, UpdateView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
@@ -389,46 +398,53 @@ class WikiEditView(ProjectMixin, UpdateView):
|
|||||||
form_class = forms.WikiForm
|
form_class = forms.WikiForm
|
||||||
|
|
||||||
def cancel_url(self):
|
def cancel_url(self):
|
||||||
return resolve_url('wiki', self.kwargs['project'], self.kwargs['pk'])
|
return resolve_url("wiki", self.kwargs["project"], self.kwargs["pk"])
|
||||||
|
|
||||||
|
|
||||||
""" RESOURCE VIEWS """
|
""" RESOURCE VIEWS """
|
||||||
|
|
||||||
|
|
||||||
class ResourceCreateView(ProjectMixin, CreateView):
|
class ResourceCreateView(ProjectMixin, CreateView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
model = models.Resource
|
model = models.Resource
|
||||||
form_class = forms.ResourceForm
|
form_class = forms.ResourceForm
|
||||||
template_name = 'interface/default_form.html'
|
template_name = "interface/default_form.html"
|
||||||
title = "Add a new resource"
|
title = "Add a new resource"
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save(commit=False)
|
self.object = form.save(commit=False)
|
||||||
self.object.project = self.project
|
self.object.project = self.project
|
||||||
self.object.save()
|
self.object.save()
|
||||||
return redirect('resource_upload', project=self.object.project_id, pk=self.object.pk)
|
return redirect(
|
||||||
|
"resource_upload", project=self.object.project_id, pk=self.object.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ResourceUploadView(ProjectMixin, UpdateView):
|
class ResourceUploadView(ProjectMixin, UpdateView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
model = models.Resource
|
model = models.Resource
|
||||||
fields = ['file']
|
fields = ["file"]
|
||||||
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('resource_list', project=self.kwargs['project'])
|
return resolve_url("resource_list", project=self.kwargs["project"])
|
||||||
|
|
||||||
|
|
||||||
class ResourceListView(ProjectMixin, ListView):
|
class ResourceListView(ProjectMixin, ListView):
|
||||||
model = models.Resource
|
model = models.Resource
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
if not self.request.is_admin:
|
if not self.request.is_admin: # type: ignore
|
||||||
qs = qs.filter(visible=True)
|
qs = qs.filter(visible=True)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class ResourceEditView(ProjectMixin, UpdateView):
|
class ResourceEditView(ProjectMixin, UpdateView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
model = models.Resource
|
model = models.Resource
|
||||||
fields = ['name', 'description', 'visible']
|
fields = ["name", "description", "visible"]
|
||||||
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('resource_list', project=self.kwargs['project'])
|
return resolve_url("resource_list", project=self.kwargs["project"])
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-06-18 05:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('library', '0014_auto_20230223_1422'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='collection',
|
||||||
|
name='settings',
|
||||||
|
field=models.JSONField(default=dict, help_text='Storage specific settings'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='doctype',
|
||||||
|
field=models.PositiveSmallIntegerField(choices=[(1, 'PDF'), (2, 'Audio'), (3, 'Video'), (4, 'Misc')], default=1),
|
||||||
|
),
|
||||||
|
]
|
||||||
+21
-20
@@ -5,7 +5,6 @@ from django.db import models
|
|||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.core.files.storage import get_storage_class
|
|
||||||
from django.db.models import Q, Count, Min, Max
|
from django.db.models import Q, Count, Min, Max
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@@ -44,13 +43,13 @@ class Orchestration(models.Model):
|
|||||||
def as_list(self):
|
def as_list(self):
|
||||||
tags = [ t.strip() for t in self.instruments.split(' ') ]
|
tags = [ t.strip() for t in self.instruments.split(' ') ]
|
||||||
return [ (t, MusicTag.from_tag(t)) for t in tags if t ]
|
return [ (t, MusicTag.from_tag(t)) for t in tags if t ]
|
||||||
|
|
||||||
def tag_order(self):
|
def tag_order(self):
|
||||||
tags = [ t.strip() for t in self.instruments.split(' ') if t ]
|
tags = [ t.strip() for t in self.instruments.split(' ') if t ]
|
||||||
order = {'score': 0}
|
order = {'score': 0}
|
||||||
for i, t in enumerate(tags):
|
for i, t in enumerate(tags):
|
||||||
order.setdefault(t.strip('-0123456789'), i*2+1)
|
order.setdefault(t.strip('-0123456789'), i*2+1)
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
def sorter(self):
|
def sorter(self):
|
||||||
@@ -79,7 +78,6 @@ class ProjectItem(models.Model):
|
|||||||
approved_by = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
approved_by = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
||||||
order = models.SmallIntegerField(default=0)
|
order = models.SmallIntegerField(default=0)
|
||||||
section = models.CharField(max_length=100, blank=True)
|
section = models.CharField(max_length=100, blank=True)
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['order', 'work']
|
ordering = ['order', 'work']
|
||||||
@@ -93,16 +91,18 @@ class Collection(models.Model):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=255,
|
name = models.CharField(max_length=255,
|
||||||
help_text="Name of the collection")
|
help_text="Name of the collection")
|
||||||
prefix = models.SlugField(max_length=30, default="default",
|
prefix = models.CharField(max_length=255, default="default",
|
||||||
help_text="Folder to store works in")
|
help_text="Folder to store works in")
|
||||||
administrators = models.ManyToManyField('auth.User', related_name="collections",
|
administrators = models.ManyToManyField('auth.User', related_name="collections",
|
||||||
help_text="Administrators for this collection")
|
help_text="Administrators for this collection")
|
||||||
location = models.CharField(max_length=100,
|
location = models.CharField(max_length=100,
|
||||||
help_text="Physical location (institution, town...)", blank=True)
|
help_text="Physical location (institution, town...)", blank=True)
|
||||||
storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True,
|
storage = models.ForeignKey('byostorage.UserStorage', on_delete=models.CASCADE, null=True, blank=True,
|
||||||
help_text="User storage for documents")
|
help_text="User storage for documents")
|
||||||
notes = models.TextField(blank=True,
|
notes = models.TextField(blank=True,
|
||||||
help_text="Publicly visible notes about collection and loans policy (markdown format)")
|
help_text="Publicly visible notes about collection and loans policy (markdown format)")
|
||||||
|
settings = models.JSONField(default=dict, blank=True,
|
||||||
|
help_text="Storage specific settings")
|
||||||
nonce = models.SmallIntegerField(default=1,
|
nonce = models.SmallIntegerField(default=1,
|
||||||
help_text="Increment this to reset the authentication links")
|
help_text="Increment this to reset the authentication links")
|
||||||
|
|
||||||
@@ -116,8 +116,8 @@ class Collection(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def genres(self):
|
def genres(self):
|
||||||
return self.meta('genre')
|
return self.meta('genre')
|
||||||
|
|
||||||
def has_administrator(self, user):
|
def has_administrator(self, user):
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
@@ -130,7 +130,7 @@ class Collection(models.Model):
|
|||||||
|
|
||||||
def auth(self):
|
def auth(self):
|
||||||
return sign_data(f'{self.pk}-{self.nonce}', 12)
|
return sign_data(f'{self.pk}-{self.nonce}', 12)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ class Work(models.Model):
|
|||||||
(LICENCE_PERUSAL, 'Perusal Licence'),
|
(LICENCE_PERUSAL, 'Perusal Licence'),
|
||||||
(LICENCE_NONE, 'Internal use only'),
|
(LICENCE_NONE, 'Internal use only'),
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(max_length=255, help_text="Original name of the work")
|
name = models.CharField(max_length=255, help_text="Original name of the work")
|
||||||
edition = models.CharField(max_length=255, blank=True,
|
edition = models.CharField(max_length=255, blank=True,
|
||||||
help_text="Edition details to distinguish multiple versions")
|
help_text="Edition details to distinguish multiple versions")
|
||||||
@@ -191,7 +191,7 @@ class Work(models.Model):
|
|||||||
code = models.CharField(max_length=100, blank=True, help_text="Collection specific code or number. Will be auto-generated if not supplied")
|
code = models.CharField(max_length=100, blank=True, help_text="Collection specific code or number. Will be auto-generated if not supplied")
|
||||||
licence = models.PositiveSmallIntegerField(choices=LICENCE_TYPES, default=6, help_text="Copyright status")
|
licence = models.PositiveSmallIntegerField(choices=LICENCE_TYPES, default=6, help_text="Copyright status")
|
||||||
max_projects = models.IntegerField(default=1, help_text="How many active projects can this work be attached to")
|
max_projects = models.IntegerField(default=1, help_text="How many active projects can this work be attached to")
|
||||||
|
|
||||||
# Extra info
|
# Extra info
|
||||||
running_time = models.DurationField(null=True, blank=True, help_text="Running time in mm:ss format")
|
running_time = models.DurationField(null=True, blank=True, help_text="Running time in mm:ss format")
|
||||||
notes = models.TextField(blank=True)
|
notes = models.TextField(blank=True)
|
||||||
@@ -219,7 +219,7 @@ class Work(models.Model):
|
|||||||
#return [ s[1] for s in sections ]
|
#return [ s[1] for s in sections ]
|
||||||
sections = list(dict(sections).items()) # primitive unique()
|
sections = list(dict(sections).items()) # primitive unique()
|
||||||
return sections
|
return sections
|
||||||
|
|
||||||
def pdfs(self):
|
def pdfs(self):
|
||||||
return self.docs.filter(doctype=Document.DOCTYPE_PDF)
|
return self.docs.filter(doctype=Document.DOCTYPE_PDF)
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ class WorkMeta(models.Model):
|
|||||||
('style', 'Style'),
|
('style', 'Style'),
|
||||||
('orchestration', 'Orchestration'),
|
('orchestration', 'Orchestration'),
|
||||||
)
|
)
|
||||||
|
|
||||||
work = models.ForeignKey(Work, on_delete=models.CASCADE, related_name='meta_info')
|
work = models.ForeignKey(Work, on_delete=models.CASCADE, related_name='meta_info')
|
||||||
name = models.SlugField(max_length=20, choices=META_CHOICES)
|
name = models.SlugField(max_length=20, choices=META_CHOICES)
|
||||||
value = models.CharField(max_length=255)
|
value = models.CharField(max_length=255)
|
||||||
@@ -348,7 +348,7 @@ class Document(models.Model):
|
|||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
self.upload.delete(save=False)
|
self.upload.delete(save=False)
|
||||||
return super().delete(*args, **kwargs)
|
return super().delete(*args, **kwargs)
|
||||||
|
|
||||||
def filename(self):
|
def filename(self):
|
||||||
return os.path.basename(self.upload.name)
|
return os.path.basename(self.upload.name)
|
||||||
|
|
||||||
@@ -359,7 +359,7 @@ class Section(models.Model):
|
|||||||
"""
|
"""
|
||||||
Section is a tagged portion of a Document
|
Section is a tagged portion of a Document
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PAGE_AUTO = 0
|
PAGE_AUTO = 0
|
||||||
PAGE_LEFT = 1
|
PAGE_LEFT = 1
|
||||||
PAGE_RIGHT = 2
|
PAGE_RIGHT = 2
|
||||||
@@ -396,7 +396,7 @@ class Section(models.Model):
|
|||||||
def filename(self):
|
def filename(self):
|
||||||
return slugify(f'{self.doc.work.name} - {self.name}').title() + '.pdf'
|
return slugify(f'{self.doc.work.name} - {self.name}').title() + '.pdf'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pagerange(self):
|
def pagerange(self):
|
||||||
if self.start:
|
if self.start:
|
||||||
if self.end:
|
if self.end:
|
||||||
@@ -405,4 +405,5 @@ class Section(models.Model):
|
|||||||
return "all"
|
return "all"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
{{ json_data|json_script:"data" }}
|
{{ json_data|json_script:"data" }}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
let url = "{{ document.upload.url|safe }}";
|
let url = "{% url 'document_download' collection.pk document.pk %}";
|
||||||
|
|
||||||
// Loaded via <script> tag, create shortcut to access PDF.js exports.
|
// Loaded via <script> tag, create shortcut to access PDF.js exports.
|
||||||
let pdfjsLib = window['pdfjs-dist/build/pdf'];
|
let pdfjsLib = window['pdfjs-dist/build/pdf'];
|
||||||
@@ -447,4 +447,4 @@
|
|||||||
window.addEventListener('beforeunload', checkSaved);
|
window.addEventListener('beforeunload', checkSaved);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{% load path_filters %}
|
||||||
|
<div>
|
||||||
|
<h3>Listing for {{ collection }}</h3>
|
||||||
|
<ul>
|
||||||
|
{% for item in folders %}
|
||||||
|
<li><a href="{% url 'storage_browser_folder' collection=collection.pk folder=item %}">{{ item|basename }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
{% for item in files %}
|
||||||
|
<li>{{ item|basename }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -25,46 +25,46 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="subtitle">{% firstof work.composer "Unattributed" %}{% if work.edition %} - {{ work.edition }}{% endif %}</p>
|
<p class="subtitle">{% firstof work.composer "Unattributed" %}{% if work.edition %} - {{ work.edition }}{% endif %}</p>
|
||||||
<section class="block">
|
|
||||||
<p class="block">{{ work.notes }}</p>
|
|
||||||
|
|
||||||
<p class="block">
|
|
||||||
<table class="table">
|
|
||||||
<tr>
|
|
||||||
<th>Location:</th><td><a href="{% url 'collection_work_list' work.collection.pk %}">{{ work.collection }}</a> [{{ work.identifier }}]</td>
|
|
||||||
<th>Orchestration:</th><td>{{ work.orchestration }}</td>
|
|
||||||
</tr><tr>
|
|
||||||
<th>Running time:</th><td>{% firstof work.duration 'Unknown' %}</td>
|
|
||||||
<th>Licence:</th><td>{{ work.get_licence_display }}</td>
|
|
||||||
</tr><tr>
|
|
||||||
<td colspan="4">
|
|
||||||
{% for meta in work.meta %}
|
|
||||||
<a href="{% url 'collection_work_list' work.collection.pk %}?filter={{ meta.name}}:{{ meta.value }}" class="tag" >
|
|
||||||
{{ meta.get_name_display }}:
|
|
||||||
{{ meta.value }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% if work.parent %}
|
|
||||||
<p>From <a href="{% url 'work_detail' work.parent.collection.pk work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if work.related_works.count %}
|
|
||||||
<h3>Related</h3>
|
|
||||||
<ul>
|
|
||||||
{% for related in work.related_works.all %}
|
|
||||||
<li><a href="{% url 'work_detail' related.collection.pk related.pk %}">{{ related.name }} - {{ related.composer }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
<section class="block">
|
<section class="block">
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
<div class="column is-half">
|
||||||
|
<p class="block">{{ work.notes }}</p>
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th>Location:</th><td><a href="{% url 'collection_work_list' work.collection.pk %}">{{ work.collection }}</a> [{{ work.identifier }}]</td>
|
||||||
|
<th>Orchestration:</th><td>{{ work.orchestration }}</td>
|
||||||
|
</tr><tr>
|
||||||
|
<th>Running time:</th><td>{% firstof work.duration 'Unknown' %}</td>
|
||||||
|
<th>Licence:</th><td>{{ work.get_licence_display }}</td>
|
||||||
|
</tr><tr>
|
||||||
|
<td colspan="4">
|
||||||
|
{% for meta in work.meta %}
|
||||||
|
<a href="{% url 'collection_work_list' work.collection.pk %}?filter={{ meta.name}}:{{ meta.value }}" class="tag" >
|
||||||
|
{{ meta.get_name_display }}:
|
||||||
|
{{ meta.value }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if work.parent %}
|
||||||
|
<p>From <a href="{% url 'work_detail' work.parent.collection.pk work.parent.pk %}">{{ work.parent.name }} - {{ work.parent.composer }}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if work.related_works.count %}
|
||||||
|
<h3>Related</h3>
|
||||||
|
<ul>
|
||||||
|
{% for related in work.related_works.all %}
|
||||||
|
<li><a href="{% url 'work_detail' related.collection.pk related.pk %}">{{ related.name }} - {{ related.composer }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h4 class="subtitle is-size-4">
|
<h4 class="subtitle is-size-4">
|
||||||
@@ -79,8 +79,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="column is-half">
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h4 class="subtitle is-size-4">
|
<h4 class="subtitle is-size-4">
|
||||||
<span class="icon"><i class="fas fa-print"></i></span>
|
<span class="icon"><i class="fas fa-print"></i></span>
|
||||||
@@ -206,4 +204,4 @@
|
|||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<h3 class="subtitle">{{ work.name }}</h3>
|
<h3 class="subtitle">{{ work.name }}</h3>
|
||||||
<div class="block">
|
<div class="block tags">
|
||||||
{% for tag, name in work.digital_parts %}
|
{% for tag, name in work.digital_parts %}
|
||||||
<a class="tag is-info" href="{% url 'work_download' work.collection_id work.pk %}?tag={{ tag }}">{{ name }}</a>
|
<a class="tag is-info" href="{% url 'work_download' work.collection_id work.pk %}?tag={{ tag }}" target="polyphonic_parts">{{ name }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
<h3 class="subtitle">Files</h3>
|
<h3 class="subtitle">Files</h3>
|
||||||
<table class="table is-narrow is-fullwidth">
|
<table class="table is-narrow is-fullwidth">
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
-->
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'work_detail' work.collection_id work.pk %}">More details...</a>
|
<a href="{% url 'work_detail' work.collection_id work.pk %}">More details...</a>
|
||||||
|
|||||||
@@ -35,10 +35,13 @@ urlpatterns = [
|
|||||||
path('collections/<int:collection>/docs/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
|
path('collections/<int:collection>/docs/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
|
||||||
|
|
||||||
path('collections/<int:collection>/download/<int:section>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
|
path('collections/<int:collection>/download/<int:section>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
|
||||||
|
path('collections/<int:collection>/browse', views.StorageBrowserView.as_view(), name="storage_browser"),
|
||||||
|
path('collections/<int:collection>/browse/<path:folder>', views.StorageBrowserView.as_view(), name="storage_browser_folder"),
|
||||||
|
|
||||||
#path('api/', include(router.urls))
|
#path('api/', include(router.urls))
|
||||||
path('api/collections/<int:pk>', api.CollectionExportView.as_view(), name="collection_export"),
|
path('api/collections/<int:pk>', api.CollectionExportView.as_view(), name="collection_export"),
|
||||||
path('api/collections/<int:collection>/works/<int:pk>', api.WorkExportView.as_view(), name="work_export"),
|
path('api/collections/<int:collection>/works/<int:pk>', api.WorkExportView.as_view(), name="work_export"),
|
||||||
path('api/collections/<int:collection>/import', api.WorkImportView.as_view(), name="work_import"),
|
path('api/collections/<int:collection>/import', api.WorkImportView.as_view(), name="work_import"),
|
||||||
path('api/collections/<int:collection>/bulk_import', api.CollectionImportView.as_view(), name="collection_import"),
|
path('api/collections/<int:collection>/bulk_import', api.CollectionImportView.as_view(), name="collection_import"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
||||||
|
from django.views.generic import TemplateView
|
||||||
from django.views.generic.detail import DetailView, SingleObjectMixin, View
|
from django.views.generic.detail import DetailView, SingleObjectMixin, View
|
||||||
from django.views.generic.list import ListView, MultipleObjectMixin
|
from django.views.generic.list import ListView, MultipleObjectMixin
|
||||||
from django.views.generic.edit import CreateView, FormView, UpdateView, DeleteView
|
from django.views.generic.edit import CreateView, FormView, UpdateView, DeleteView
|
||||||
@@ -18,6 +19,7 @@ import re
|
|||||||
|
|
||||||
from interface.views import EnsembleMixin, ProjectMixin, AuthorizedResourceMixin
|
from interface.views import EnsembleMixin, ProjectMixin, AuthorizedResourceMixin
|
||||||
from interface.models import Project
|
from interface.models import Project
|
||||||
|
from interface.utils import signed_url
|
||||||
from library.models import Collection, Work, Document, Section
|
from library.models import Collection, Work, Document, Section
|
||||||
from library.music_tags import MUSIC_TAGS, MusicTag, auto_tag
|
from library.music_tags import MUSIC_TAGS, MusicTag, auto_tag
|
||||||
from library import forms, models
|
from library import forms, models
|
||||||
@@ -416,9 +418,11 @@ class DocumentDownloadView(DocumentMixin, SingleObjectMixin, View):
|
|||||||
self.args = args
|
self.args = args
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
|
||||||
#response = FileResponse(self.object.upload, content_type="application/pdf")
|
if request.GET.get('method') == 'direct':
|
||||||
#return response
|
return redirect(self.object.upload.url)
|
||||||
return redirect(self.object.upload.url)
|
|
||||||
|
response = FileResponse(self.object.upload.open('rb'), content_type="application/pdf")
|
||||||
|
return response
|
||||||
|
|
||||||
class DocumentAnnotateView(DocumentMixin, DetailView):
|
class DocumentAnnotateView(DocumentMixin, DetailView):
|
||||||
template_name = 'library/document_annotate.html'
|
template_name = 'library/document_annotate.html'
|
||||||
@@ -446,6 +450,8 @@ class DocumentAnnotateView(DocumentMixin, DetailView):
|
|||||||
for part in data['document'].sections.all():
|
for part in data['document'].sections.all():
|
||||||
pages.append((part.tag, part.start, part.end))
|
pages.append((part.tag, part.start, part.end))
|
||||||
|
|
||||||
|
data['url'] = signed_url('document_download', collection=data['collection'].pk, pk=data['document'].pk)
|
||||||
|
|
||||||
data['json_data'] = {'pageTags': pages, 'instruments': dict(MUSIC_TAGS)}
|
data['json_data'] = {'pageTags': pages, 'instruments': dict(MUSIC_TAGS)}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -479,4 +485,15 @@ class PartDownloadView(CollectionMixin, SingleObjectMixin, View):
|
|||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
|
|
||||||
return Section.objects.filter(doc__work__collection=self.collection).select_related('doc', 'doc__work').get(pk=self.kwargs['section'])
|
return Section.objects.filter(doc__work__collection=self.collection).select_related('doc', 'doc__work').get(pk=self.kwargs['section'])
|
||||||
|
|
||||||
|
|
||||||
|
class StorageBrowserView(CollectionMixin, TemplateView):
|
||||||
|
template_name = "library/storage_browser.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
data = super().get_context_data(**kwargs)
|
||||||
|
folder = self.kwargs.get('folder') or data['collection'].prefix
|
||||||
|
data['folders'], data['files'] = data['collection'].storage.instance().listdir(folder)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
from .default_settings import *
|
||||||
try:
|
try:
|
||||||
from .local_settings import *
|
from .local_settings import *
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from .default_settings import *
|
pass
|
||||||
|
|
||||||
|
INSTALLED_APPS += POLYPHONIC_MODULES
|
||||||
INSTALLED_APPS += POLYPHONIC_MODULES
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
asgiref==3.4.1
|
asgiref==3.8.1
|
||||||
boto3==1.18.34
|
boto3==1.18.34
|
||||||
botocore==1.21.34
|
botocore==1.21.34
|
||||||
certifi==2022.12.7
|
certifi==2022.12.7
|
||||||
charset-normalizer==2.1.1
|
charset-normalizer==2.1.1
|
||||||
crispy-bulma==0.8.0
|
crispy-bulma==0.8.0
|
||||||
Django==3.2.7
|
Django==5.1
|
||||||
#-e git+ssh://git@gitea.tfconsulting.com.au/tris/django-byostorage.git@227ce5fccb5864657f080c25528263b655c2b6b7#egg=django_byostorage
|
django-byostorage @ git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@9903bb00888f20dfd2d39754e5ee22eeb5f36298
|
||||||
git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git
|
|
||||||
django-crispy-forms==1.14.0
|
django-crispy-forms==1.14.0
|
||||||
django-markdown2==0.3.1
|
django-markdown2==0.3.1
|
||||||
django-rest-framework==0.1.0
|
django-rest-framework==0.1.0
|
||||||
@@ -14,11 +13,14 @@ django-storages==1.13.1
|
|||||||
django_debug_toolbar==3.8.1
|
django_debug_toolbar==3.8.1
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
djantic==0.7.0
|
djantic==0.7.0
|
||||||
|
greenlet==3.0.3
|
||||||
idna==3.4
|
idna==3.4
|
||||||
jmespath==0.10.0
|
jmespath==0.10.0
|
||||||
markdown2==2.4.1
|
markdown2==2.4.1
|
||||||
|
msgpack==1.0.8
|
||||||
pydantic==1.10.2
|
pydantic==1.10.2
|
||||||
pydantic-django==0.1.1
|
pydantic-django==0.1.1
|
||||||
|
pynvim==0.5.0
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
pytz==2021.1
|
pytz==2021.1
|
||||||
requests==2.28.1
|
requests==2.28.1
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[project]
|
||||||
|
name = "polyphonic"
|
||||||
|
version = "0.8.3"
|
||||||
|
description = "Polyphonic Ensemble Manager"
|
||||||
|
authors = [
|
||||||
|
{name = "Tris Forster",email = "tris@tfconsulting.com.au"}
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"django (==6.0.4)",
|
||||||
|
"django-crispy-forms",
|
||||||
|
"django-markdown2",
|
||||||
|
"django-rest-framework",
|
||||||
|
"crispy-bulma (>=0.12.0,<0.13.0)",
|
||||||
|
"django-byostorage @ git+https://gitea.tfconsulting.com.au/tris/django-byostorage.git@9903bb00888f20dfd2d39754e5ee22eeb5f36298",
|
||||||
|
"requests (>=2.32.5,<3.0.0)",
|
||||||
|
"django-storages (>=1.14.6,<2.0.0)",
|
||||||
|
"boto3 (>=1.40.20,<2.0.0)"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
packages = [{include = "*", from="app"}]
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
django-debug-toolbar = "5.2"
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
manage = "manage:main"
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
Reference in New Issue
Block a user