Finally got authentication to somewhere I'm happy
This commit is contained in:
parent
bc9f292a2e
commit
b85440d25c
@ -3,7 +3,7 @@ from django.contrib import admin
|
|||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
class EnsembleAdmin(admin.ModelAdmin):
|
class EnsembleAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'ensemble_code', 'slug']
|
list_display = ['name', 'slug']
|
||||||
|
|
||||||
class ModuleInline(admin.StackedInline):
|
class ModuleInline(admin.StackedInline):
|
||||||
model = models.Module
|
model = models.Module
|
||||||
|
|||||||
27
app/interface/migrations/0003_auto_20230209_0910.py
Normal file
27
app/interface/migrations/0003_auto_20230209_0910.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2023-02-08 22:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('interface', '0002_auto_20230202_0804'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='ensemble',
|
||||||
|
options={'ordering': ('slug',)},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ensemble',
|
||||||
|
name='auth',
|
||||||
|
field=models.SmallIntegerField(default=1, help_text='Increment this to reset the authentication links'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='auth',
|
||||||
|
field=models.SmallIntegerField(default=1, help_text='Increment this to reset the authentication links'),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
app/interface/migrations/0004_auto_20230210_0938.py
Normal file
23
app/interface/migrations/0004_auto_20230210_0938.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2023-02-09 22:38
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('interface', '0003_auto_20230209_0910'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='ensemble',
|
||||||
|
old_name='auth',
|
||||||
|
new_name='nonce',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='project',
|
||||||
|
old_name='auth',
|
||||||
|
new_name='nonce',
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -11,6 +11,7 @@ import random
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
from .utils import sign_data
|
||||||
|
|
||||||
MEDIA_TYPES = [
|
MEDIA_TYPES = [
|
||||||
('audio', "Audio"),
|
('audio', "Audio"),
|
||||||
@ -48,15 +49,13 @@ class Ensemble(models.Model):
|
|||||||
help_text="Display name")
|
help_text="Display name")
|
||||||
slug = models.SlugField(max_length=100, editable=False, unique=True,
|
slug = models.SlugField(max_length=100, editable=False, unique=True,
|
||||||
help_text="Short name for the ensemble - used for folders")
|
help_text="Short name for the ensemble - used for folders")
|
||||||
#code = models.CharField(max_length=9, default=generate_code,
|
|
||||||
# help_text="Ensemble registration code")
|
|
||||||
#passphrase = models.CharField(max_length=100,
|
|
||||||
# help_text="Used to register ensembles")
|
|
||||||
admins = models.ManyToManyField('auth.User', related_name='ensembles')
|
admins = models.ManyToManyField('auth.User', related_name='ensembles')
|
||||||
details = models.TextField(blank=True,
|
details = models.TextField(blank=True,
|
||||||
help_text="Description of the ensemble (markdown)")
|
help_text="Description of the ensemble (markdown)")
|
||||||
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL,
|
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL,
|
||||||
help_text="Default storage for this ensemble")
|
help_text="Default storage for this ensemble")
|
||||||
|
nonce = models.SmallIntegerField(default=1,
|
||||||
|
help_text="Increment this to reset the authentication links")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('slug', )
|
ordering = ('slug', )
|
||||||
@ -64,10 +63,6 @@ class Ensemble(models.Model):
|
|||||||
def active_projects(self):
|
def active_projects(self):
|
||||||
return self.projects.filter(active=True, event_date__gte=timezone.now())
|
return self.projects.filter(active=True, event_date__gte=timezone.now())
|
||||||
|
|
||||||
def ensemble_code(self):
|
|
||||||
code = str(self.code)
|
|
||||||
return "{}-{}-{}".format(code[:3], code[3:6], code[6:])
|
|
||||||
|
|
||||||
def has_admin(self, user):
|
def has_admin(self, user):
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
@ -80,6 +75,12 @@ class Ensemble(models.Model):
|
|||||||
self.slug = slugify(self.name)
|
self.slug = slugify(self.name)
|
||||||
super(Ensemble, self).save(**kwargs)
|
super(Ensemble, self).save(**kwargs)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return resolve_url('ensemble_detail', ensemble=self.slug)
|
||||||
|
|
||||||
|
def auth(self):
|
||||||
|
return sign_data(f'{self.pk}-{self.nonce}', 12)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@ -94,6 +95,8 @@ class Project(models.Model):
|
|||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
event_date =models.DateTimeField(null=True, blank=True)
|
event_date =models.DateTimeField(null=True, blank=True)
|
||||||
owner = models.CharField(max_length=255, blank=True)
|
owner = models.CharField(max_length=255, blank=True)
|
||||||
|
nonce = models.SmallIntegerField(default=1,
|
||||||
|
help_text="Increment this to reset the authentication links")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['active', 'event_date']
|
ordering = ['active', 'event_date']
|
||||||
@ -125,6 +128,12 @@ class Project(models.Model):
|
|||||||
def active_modules(self):
|
def active_modules(self):
|
||||||
return self.modules.values_list('name', flat=True)
|
return self.modules.values_list('name', flat=True)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return resolve_url('project_detail', project=self.pk)
|
||||||
|
|
||||||
|
def auth(self):
|
||||||
|
return sign_data(f'{self.pk}-{self.nonce}', 12)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<span class="icon fancy is-size-2 mx-4"><i class="fas fa-random"></i></span>
|
<span class="icon fancy is-size-2 mx-4"><i class="fas fa-random"></i></span>
|
||||||
<span class="fancy is-size-2">Polyphonic</span>
|
<span class="fancy is-size-2">Polyphonic</span>
|
||||||
</a>
|
</a>
|
||||||
<span class="navbar-item is-hidden-mobile fancy is-size-5">Music Ensemble Manager</span>
|
<span class="navbar-item is-hidden-mobile fancy is-size-5">Musical Ensemble Manager</span>
|
||||||
|
|
||||||
|
|
||||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="projectMenu">
|
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="projectMenu">
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
{% include 'interface/project_items.html' %}
|
{% include 'interface/project_items.html' %}
|
||||||
|
|
||||||
|
{% if request.is_admin %}
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card header">
|
<header class="card header">
|
||||||
@ -46,4 +47,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'forget_resource' 'ensemble' ensemble.slug %}">Forget this ensemble</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -13,41 +13,31 @@
|
|||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a href="{% url 'ensemble_list' %}">Ensembles</a></li>
|
<li><a href="{% url 'ensemble_list' %}">Ensembles</a></li>
|
||||||
<li><a href="{% url 'project_list' %}">Projects</a></li>
|
<li><a href="{% url 'project_list' %}">Projects</a></li>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
<li><a href="{% url 'work_list' %}">Library</a></li>
|
<li><a href="{% url 'work_list' %}">Library</a></li>
|
||||||
|
<li><a href="{% url 'collection_list' %}">Collections</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% if project %}
|
{% if project %}
|
||||||
<p class="menu-label">This Project</p>
|
<p class="menu-label">This Project</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a role="tab" href="{% url 'project_detail' project=project.id %}">Project Info</a></li>
|
<li><a role="tab" href="{% url 'project_detail' project=project.id %}">Project Info</a></li>
|
||||||
{% for page in project.wiki_pages.all %}
|
{% if 'library' in modules %}
|
||||||
<li><a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
|
<li><a class="nav-link" href="{% url 'item_list' project=project.pk %}">My Music</a></li>
|
||||||
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
<li><a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a></li>
|
|
||||||
|
|
||||||
{% if 'library' in modules %}
|
|
||||||
<li><a href="{% url 'item_list' project=project.id %}">My Music</a></li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if 'submission' in modules %}
|
|
||||||
<!--a role="tab" href="">Record a submission</a-->
|
|
||||||
{% if request.is_admin %}
|
|
||||||
<li><a role="tab" class="admin-link" href="{% url 'submission_list' project=project.id %}">Submissions</a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% for page in project.wiki_pages.all %}
|
||||||
<li><a role="tab" href="{% url 'submission_create' project=project.id %}">Send a File</a></li>
|
<li><a class="nav-link
|
||||||
{% endif %}
|
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a></li>
|
||||||
</ul>
|
{% endfor %}
|
||||||
|
<li><a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a></li>
|
||||||
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p class="menu-label">Admin</p>
|
<p class="menu-label">Admin</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
{% if request.is_admin or request.user.is_superuser %}
|
{% if request.user.is_staff %}
|
||||||
<li><a class="admin-link" href="{% url 'collection_list' %}">Collections</a></li>
|
<li><a href="/admin" target="polyphonic_admin" rel="noopener noreferrer">Django Admin</a></li>
|
||||||
{% endif %}
|
|
||||||
{% if request.user.is_superuser %}
|
|
||||||
<li><a class="admin-link" href="/admin" target="polyphonic_admin" rel="noopener noreferrer">Django Admin</a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
{% if project.has_happened %}
|
{% if project.has_happened %}
|
||||||
({{ project.event_date|roughtimesince }} ago)
|
({{ project.event_date|roughtimesince }} ago)
|
||||||
{% else %}
|
{% else %}
|
||||||
(in {{ project.event_date|roughtimeuntil }}...)
|
(in {{ project.event_date|roughtimeuntil }})
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -65,4 +65,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'forget_resource' 'project' project.pk %}">Forget this project</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
41
app/interface/templates/interface/project_items.html
Normal file
41
app/interface/templates/interface/project_items.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% load md2 %}
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
{% for project in object_list %}
|
||||||
|
<div class="column is-half-tablet is-one-third-widescreen">
|
||||||
|
<div class="card">
|
||||||
|
<a class="" href="{% url 'project_detail' project=project.id %}">
|
||||||
|
<header class="card-header{% if not project.active %} has-background-light{% endif %}">
|
||||||
|
<p class="card-header-title">{% if not ensemble %}{{ project.ensemble }}{% endif%} {{ project.name }}</p>
|
||||||
|
<p class="card-header-icon" style="color: black;">{{ project.rough_date }}</p>
|
||||||
|
</header>
|
||||||
|
</a>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="content">
|
||||||
|
{{ project.description | markdown }}
|
||||||
|
</div>
|
||||||
|
<p><small>
|
||||||
|
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
|
||||||
|
{% if project.submissions.count %}{{ project.submissions.count }} submissions<br/>{% endif %}
|
||||||
|
</small></p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
{% if 'library' in project.active_modules %}
|
||||||
|
{% with project.works.count as c %}
|
||||||
|
<a class="card-footer-item" href="{% url 'item_list' project=project.pk %}">
|
||||||
|
{{ c }} work{{ c | pluralize }}
|
||||||
|
</a>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="hero">
|
||||||
|
<div class="hero-body">
|
||||||
|
<p class="title">No projects currently planned</p>
|
||||||
|
<p class="subtitle">Go put your feet up!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
8
app/interface/templates/interface/project_list.html
Normal file
8
app/interface/templates/interface/project_list.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% extends "interface/project_base.html" %}
|
||||||
|
{% load md2 %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<h3 class="title">My Projects</h3>
|
||||||
|
|
||||||
|
{% include 'interface/project_items.html' %}
|
||||||
|
{% endblock %}
|
||||||
@ -10,7 +10,7 @@ class AccessTestCase(TestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
admin = User.objects.create_user(username='admin', password='foobar', is_superuser=True)
|
admin = User.objects.create_user(username='admin', password='foobar', is_superuser=True, is_staff=True)
|
||||||
homer = User.objects.create_user(username='homer', password='maggie')
|
homer = User.objects.create_user(username='homer', password='maggie')
|
||||||
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
@ -75,7 +75,7 @@ class AccessTestCase(TestCase):
|
|||||||
'/ensembles/party-posse/new-project': False,
|
'/ensembles/party-posse/new-project': False,
|
||||||
})
|
})
|
||||||
|
|
||||||
self.authorize('ensemble_detail', ensemble='bleeding-gums')
|
self.authorize(models.Ensemble, slug='bleeding-gums')
|
||||||
self.assertAccess({
|
self.assertAccess({
|
||||||
'/ensembles/the-be-sharps': True,
|
'/ensembles/the-be-sharps': True,
|
||||||
'/ensembles/bleeding-gums': True,
|
'/ensembles/bleeding-gums': True,
|
||||||
@ -105,7 +105,7 @@ class AccessTestCase(TestCase):
|
|||||||
'/projects/4/resources/add': False,
|
'/projects/4/resources/add': False,
|
||||||
})
|
})
|
||||||
|
|
||||||
self.client.get(utils.signed_url('project_detail', project=4))
|
self.authorize(models.Project, pk=4)
|
||||||
response = self.client.get('/projects')
|
response = self.client.get('/projects')
|
||||||
self.assertObjectList(response, ['Open Mic Night', 'Baby on Board'])
|
self.assertObjectList(response, ['Open Mic Night', 'Baby on Board'])
|
||||||
response = self.client.get('/projects/4')
|
response = self.client.get('/projects/4')
|
||||||
@ -117,7 +117,8 @@ class AccessTestCase(TestCase):
|
|||||||
self.assertContains(response, 'You don\'t currently have access to any ensembles')
|
self.assertContains(response, 'You don\'t currently have access to any ensembles')
|
||||||
|
|
||||||
def test_anon_authorized_ensemble(self):
|
def test_anon_authorized_ensemble(self):
|
||||||
response = self.client.get(utils.signed_url('ensemble_detail', ensemble='party-posse'))
|
self.authorize(models.Ensemble, slug='party-posse')
|
||||||
|
response = self.client.get('/ensembles/party-posse')
|
||||||
self.assertContains(response, 'Party Posse')
|
self.assertContains(response, 'Party Posse')
|
||||||
|
|
||||||
response = self.client.get('/ensembles')
|
response = self.client.get('/ensembles')
|
||||||
@ -133,9 +134,9 @@ class AccessTestCase(TestCase):
|
|||||||
self.assertObjectList(response, ['Navy Recruitment Day'])
|
self.assertObjectList(response, ['Navy Recruitment Day'])
|
||||||
|
|
||||||
def test_anon_authorized_project(self):
|
def test_anon_authorized_project(self):
|
||||||
self.authorize('project_detail', project=4)
|
self.authorize(models.Project, pk=4)
|
||||||
self.assertObjectList(self.client.get('/projects'), ['Open Mic Night'])
|
self.assertObjectList(self.client.get('/projects'), ['Open Mic Night'])
|
||||||
self.assertObjectList(self.client.get('/ensembles'), [])
|
self.assertObjectList(self.client.get('/ensembles'), ['Lisa and the Bleeding Gums'])
|
||||||
|
|
||||||
self.assertAccess({
|
self.assertAccess({
|
||||||
'/projects/4': True,
|
'/projects/4': True,
|
||||||
@ -153,9 +154,10 @@ class AccessTestCase(TestCase):
|
|||||||
'/ensembles/unknown': False,
|
'/ensembles/unknown': False,
|
||||||
})
|
})
|
||||||
|
|
||||||
def authorize(self, url, **kwargs):
|
def authorize(self, model, **kwargs):
|
||||||
response = self.client.get(utils.signed_url(url, **kwargs))
|
object = model.objects.get(**kwargs)
|
||||||
self.assertEqual(response.status_code, 200)
|
response = self.client.get(f'{object.get_absolute_url()}?auth={object.auth()}')
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
def assertAccess(self, urls):
|
def assertAccess(self, urls):
|
||||||
for url, expected in urls.items():
|
for url, expected in urls.items():
|
||||||
|
|||||||
@ -10,13 +10,11 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('login', auth_views.LoginView.as_view(), name='login'),
|
path('login', auth_views.LoginView.as_view(), name='login'),
|
||||||
path('logout', auth_views.LogoutView.as_view(), name='logout'),
|
path('logout', auth_views.LogoutView.as_view(), name='logout'),
|
||||||
#path('register/<group>/<int:pk>', views.register, name='register'),
|
path('forget/<resource>/<key>', views.ForgetResourceView.as_view(), name="forget_resource"),
|
||||||
#path('deregister/<group>/<int:pk>', views.deregister, name='deregister'),
|
|
||||||
|
|
||||||
path('ensembles', views.EnsembleListView.as_view(), name="ensemble_list"),
|
path('ensembles', views.EnsembleListView.as_view(), name="ensemble_list"),
|
||||||
path('ensembles/<slug:ensemble>', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
|
path('ensembles/<slug:ensemble>', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
|
||||||
path('ensembles/<slug:ensemble>/new-project', views.ProjectCreateView.as_view(), name="project_create"),
|
path('ensembles/<slug:ensemble>/new-project', views.ProjectCreateView.as_view(), name="project_create"),
|
||||||
#path('ensembles/<int:pk>/forget', views.EnsembleForgetView.as_view(), name='ensemble_forget'),
|
|
||||||
|
|
||||||
path('projects', views.ProjectListView.as_view(), name="project_list"),
|
path('projects', views.ProjectListView.as_view(), name="project_list"),
|
||||||
path('projects/<int:project>', views.ProjectDetailView.as_view(), name="project_detail"),
|
path('projects/<int:project>', views.ProjectDetailView.as_view(), name="project_detail"),
|
||||||
|
|||||||
@ -4,18 +4,33 @@ from django.core.exceptions import SuspiciousOperation
|
|||||||
|
|
||||||
signer = Signer()
|
signer = Signer()
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def sign_data(data, l=None):
|
||||||
|
sig = signer.sign(data)
|
||||||
|
p = len(data) + 1
|
||||||
|
if l:
|
||||||
|
l += p
|
||||||
|
return sig[p:l]
|
||||||
|
|
||||||
def signed_url(name, **kwargs):
|
def signed_url(name, **kwargs):
|
||||||
"""
|
"""
|
||||||
>>> signed_url('foo/bar')
|
>>> signed_url('foo/bar')
|
||||||
"""
|
"""
|
||||||
url = resolve_url(name, **kwargs)
|
url = resolve_url(name, **kwargs)
|
||||||
sig = signer.sign(url)
|
sig = signer.sign(url)
|
||||||
return sig.replace(":", "?auth=")
|
sep = "&" if "?" in url else "?"
|
||||||
|
return sig.replace(":", f"{sep}auth=")
|
||||||
|
|
||||||
def check_signed_url(url, auth):
|
def check_signed_url(full_path):
|
||||||
sig = signer.sign(url)
|
p = full_path.rfind('auth')
|
||||||
if sig[len(url)+1:] != auth:
|
url = full_path[:p-1]
|
||||||
sig = "_HIDDEN_"
|
logger.debug("check_signed_url: %s", url)
|
||||||
|
signed = signed_url(url)
|
||||||
|
if signed != full_path:
|
||||||
|
logger.debug("Mismatch: %s != %s", full_path, signed)
|
||||||
|
signed = "_HIDDEN_"
|
||||||
raise SuspiciousOperation("Bad auth code")
|
raise SuspiciousOperation("Bad auth code")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -3,29 +3,30 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from django.shortcuts import render, get_object_or_404, redirect, resolve_url
|
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
||||||
from django.views.generic import TemplateView, RedirectView
|
from django.views.generic import RedirectView
|
||||||
from django.views.generic.detail import DetailView
|
from django.views.generic.detail import DetailView
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
from django.views.generic.edit import CreateView, UpdateView
|
from django.views.generic.edit import CreateView, UpdateView
|
||||||
from django.core.exceptions import SuspiciousOperation, PermissionDenied
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.http import Http404, HttpResponseForbidden
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.contrib import auth
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from markdown2 import markdown
|
from markdown2 import markdown
|
||||||
from functools import cached_property
|
|
||||||
|
|
||||||
from . import models, forms
|
from . import models, forms
|
||||||
from interface.utils import signed_url, 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 two parts of the permission system, signed urls and persistent authenticated resources
|
Handles these parts of the permission system:
|
||||||
|
* signed urls
|
||||||
|
* persistent authenticated resources
|
||||||
|
* Admin enforcing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SESSION_KEY = 'authorized'
|
SESSION_KEY = 'authorized'
|
||||||
@ -34,10 +35,11 @@ class AuthorizedResourceMixin(object):
|
|||||||
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:
|
||||||
|
logger.debug("is_authorized: superuser")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if 'auth' in self.request.GET:
|
if 'sig' in self.request.GET:
|
||||||
check_signed_url(self.request.path, self.request.GET['auth'])
|
check_signed_url(self.request.get_full_path())
|
||||||
self.on_signed_request()
|
self.on_signed_request()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -46,25 +48,43 @@ class AuthorizedResourceMixin(object):
|
|||||||
def on_signed_request(self):
|
def on_signed_request(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def is_authorized_key(self, resource, key, auth):
|
||||||
|
current = self.get_authorized_keys(resource).get(str(key), None)
|
||||||
|
if current is None:
|
||||||
|
logger.debug("is_authorized_key: %s %s not in session", resource, key)
|
||||||
|
return False
|
||||||
|
if auth == current:
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info("Authorisation revoked")
|
||||||
|
self.del_authorized_key(resource, key)
|
||||||
|
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 set(self._authorized.get(resource, []))
|
return self._authorized.get(resource, {})
|
||||||
|
|
||||||
def add_authorized_key(self, resource, key):
|
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.add(key)
|
current[str(key)] = auth
|
||||||
self._authorized[resource] = list(current)
|
self._authorized[resource] = current
|
||||||
self.request.session[self.SESSION_KEY] = self._authorized
|
self.request.session[self.SESSION_KEY] = self._authorized
|
||||||
|
|
||||||
def del_authorized_key(self, resource, key):
|
def del_authorized_key(self, resource, key):
|
||||||
|
logger.info("Revoking authorization for %s %s", resource, key)
|
||||||
current = self.get_authorized_keys(resource)
|
current = self.get_authorized_keys(resource)
|
||||||
current.discard(key)
|
if current.pop(key, None) is None:
|
||||||
|
return False
|
||||||
if current:
|
if current:
|
||||||
self._authorized[resource] = list(current)
|
self._authorized[resource] = current
|
||||||
else:
|
else:
|
||||||
self._authorized.pop(current)
|
self._authorized.pop(resource)
|
||||||
self.request.session[self.SESSION_KEY] = self._authorized
|
self.request.session[self.SESSION_KEY] = self._authorized
|
||||||
|
return True
|
||||||
|
|
||||||
|
def request_denied(self):
|
||||||
|
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):
|
||||||
|
|
||||||
@ -72,81 +92,83 @@ class AuthorizedResourceMixin(object):
|
|||||||
request.is_admin = request.user.is_superuser
|
request.is_admin = request.user.is_superuser
|
||||||
|
|
||||||
if not self.is_authorized():
|
if not self.is_authorized():
|
||||||
raise Http404("Either the given resource doesn't exist or you dont have access to it.")
|
return self.request_denied()
|
||||||
|
|
||||||
if self.admin_required and not request.is_admin:
|
if self.admin_required and not request.is_admin:
|
||||||
raise PermissionDenied("You must be an ensemble admin.")
|
return self.request_denied()
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
class ForgetResourceView(AuthorizedResourceMixin, RedirectView):
|
||||||
|
|
||||||
|
def is_authorized(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_redirect_url(self, resource, key):
|
||||||
|
self.del_authorized_key(resource, key)
|
||||||
|
return "/"
|
||||||
|
|
||||||
class EnsembleMixin(AuthorizedResourceMixin):
|
class EnsembleMixin(AuthorizedResourceMixin):
|
||||||
|
|
||||||
def is_authorized(self):
|
ensemble_slug_kwarg = 'ensemble'
|
||||||
if 'forget' in self.request.GET:
|
limited_project_access = False
|
||||||
self.del_authorized_key('ensemble', self.kwargs['ensemble'])
|
|
||||||
raise Http404("Access removed")
|
|
||||||
super().is_authorized()
|
|
||||||
try:
|
|
||||||
self.ensemble = self.get_ensemble()
|
|
||||||
return True
|
|
||||||
except models.Ensemble.DoesNotExist:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_ensemble(self):
|
def is_authorized(self):
|
||||||
ensemble = self.get_queryset().get(slug=self.kwargs['ensemble'])
|
ensemble_slug = self.kwargs[self.ensemble_slug_kwarg]
|
||||||
|
self.ensemble = get_object_or_404(models.Ensemble, slug=ensemble_slug)
|
||||||
|
|
||||||
|
if super().is_authorized():
|
||||||
|
return True
|
||||||
|
|
||||||
self.request.is_admin = ensemble.has_admin(self.request.user)
|
if self.ensemble.has_admin(self.request.user):
|
||||||
|
self.request.is_admin = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.is_authorized_key('ensemble', ensemble_slug, self.ensemble.nonce):
|
||||||
|
return True
|
||||||
|
|
||||||
return ensemble
|
authorized = set([ int(x) for x in self.get_authorized_keys('project').keys() ])
|
||||||
|
projects = set(self.ensemble.projects.values_list('pk', flat=True))
|
||||||
|
logger.debug("is_authorized: %r & %r", authorized, projects)
|
||||||
|
if authorized & projects:
|
||||||
|
self.limited_project_access = True
|
||||||
|
logger.debug("is_authorized: allowing due to project link")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
return self.ensemble
|
return self.ensemble
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
ensembles = models.Ensemble.objects.all()
|
|
||||||
|
|
||||||
if self.request.is_admin:
|
|
||||||
return ensembles
|
|
||||||
|
|
||||||
# limit to registered ensembles
|
|
||||||
f = Q(slug__in=self.get_authorized_keys('ensemble'))
|
|
||||||
|
|
||||||
# or ensembles where the user is admin
|
|
||||||
if self.request.user.is_authenticated:
|
|
||||||
f |= Q(admins=self.request.user.pk)
|
|
||||||
|
|
||||||
return ensembles.filter(f)
|
|
||||||
|
|
||||||
class ProjectMixin(AuthorizedResourceMixin):
|
class ProjectMixin(AuthorizedResourceMixin):
|
||||||
|
|
||||||
|
project_kwarg = 'project'
|
||||||
|
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
super().is_authorized()
|
project_id = self.kwargs[self.project_kwarg]
|
||||||
try:
|
self.project = get_object_or_404(models.Project.objects.select_related('ensemble'), pk=project_id)
|
||||||
self.project = self.get_project()
|
|
||||||
|
if super().is_authorized():
|
||||||
return True
|
return True
|
||||||
except models.Project.DoesNotExist:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_project(self):
|
|
||||||
project = self.get_project_queryset().get(pk=self.kwargs['project'])
|
|
||||||
self.request.is_admin = project.ensemble.has_admin(self.request.user)
|
|
||||||
return project
|
|
||||||
|
|
||||||
def get_project_queryset(self):
|
|
||||||
projects = models.Project.objects.select_related('ensemble')
|
|
||||||
if self.request.is_admin:
|
|
||||||
return projects
|
|
||||||
|
|
||||||
f = Q(pk__in=self.get_authorized_keys('project')) | Q(ensemble__slug__in=self.get_authorized_keys('ensemble'))
|
|
||||||
|
|
||||||
if self.request.user.is_authenticated:
|
|
||||||
f |= Q(ensemble__admins=self.request.user.pk)
|
# check if the current user is an admin on the ensemble
|
||||||
else:
|
if self.project.ensemble.has_admin(self.request.user):
|
||||||
f &= Q(active=True)
|
logger.debug("is_authorized: ensemble admin for project")
|
||||||
|
self.request.is_admin = True
|
||||||
|
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 projects.filter(f)
|
if self.is_authorized_key('project', project_id, self.project.nonce):
|
||||||
|
logger.debug('is_authorized: has project link')
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
@ -166,26 +188,55 @@ class CrispyFormMixin(object):
|
|||||||
|
|
||||||
""" ENSEMBLE VIEWS """
|
""" ENSEMBLE VIEWS """
|
||||||
|
|
||||||
class EnsembleListView(EnsembleMixin, ListView):
|
class EnsembleListView(AuthorizedResourceMixin, ListView):
|
||||||
model = models.Ensemble
|
|
||||||
|
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
ensembles = models.Ensemble.objects.all()
|
||||||
|
|
||||||
|
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 on_signed_request(self):
|
def request_denied(self):
|
||||||
self.add_authorized_key('ensemble', self.kwargs['ensemble'])
|
if 'auth' in self.request.GET:
|
||||||
|
if self.request.GET['auth'] != self.ensemble.auth():
|
||||||
|
raise SuspiciousOperation("Bad ensemble link")
|
||||||
|
self.add_authorized_key('ensemble', self.ensemble.slug, self.ensemble.nonce)
|
||||||
|
return HttpResponseRedirect(resolve_url('ensemble_detail', self.ensemble.slug))
|
||||||
|
|
||||||
|
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)
|
||||||
data['inactive'] = 'inactive' in self.request.GET
|
inactive = 'inactive' in self.request.GET and self.request.is_admin
|
||||||
if data['inactive']:
|
|
||||||
data['object_list'] = self.object.projects.all().order_by('-pk')
|
projects = self.ensemble.projects.all()
|
||||||
|
if self.limited_project_access:
|
||||||
|
projects = projects.filter(pk__in=self.get_authorized_keys('project').keys())
|
||||||
|
|
||||||
|
if inactive:
|
||||||
|
projects = projects.order_by('-pk')
|
||||||
else:
|
else:
|
||||||
data['object_list'] = self.object.active_projects()
|
projects = projects.filter(active=True, event_date__gte=timezone.now()-timezone.timedelta(7))
|
||||||
|
|
||||||
|
data['inactive'] = inactive
|
||||||
|
data['object_list'] = projects
|
||||||
if self.request.is_admin:
|
if self.request.is_admin:
|
||||||
data['ensemble_link'] = signed_url(self.request.path)
|
#data['ensemble_link'] = signed_url(f'{self.request.path}?nonce={self.ensemble.nonce}')
|
||||||
|
data['ensemble_link'] = self.request.path + "?auth=" + self.ensemble.auth()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
""" PROJECT VIEWS """
|
""" PROJECT VIEWS """
|
||||||
@ -194,9 +245,22 @@ class ProjectListView(ProjectMixin, ListView):
|
|||||||
|
|
||||||
def is_authorized(self):
|
def is_authorized(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_project_queryset(self):
|
||||||
|
projects = models.Project.objects.select_related('ensemble')
|
||||||
|
if self.request.is_admin:
|
||||||
|
return projects
|
||||||
|
|
||||||
|
f = Q(pk__in=self.get_authorized_keys('project').keys()) | Q(ensemble__slug__in=self.get_authorized_keys('ensemble').keys())
|
||||||
|
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
f |= Q(ensemble__admins=self.request.user.pk)
|
||||||
|
|
||||||
|
return projects.filter(f)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.get_project_queryset().filter(active=True, event_date__gte=timezone.now()-timezone.timedelta(7))
|
qs = self.get_project_queryset()
|
||||||
|
return qs.filter(active=True, event_date__gte=timezone.now()-timezone.timedelta(7))
|
||||||
|
|
||||||
class ProjectCreateView(EnsembleMixin, CreateView):
|
class ProjectCreateView(EnsembleMixin, CreateView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
@ -207,22 +271,29 @@ class ProjectCreateView(EnsembleMixin, CreateView):
|
|||||||
|
|
||||||
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_id = self.kwargs['pk']
|
self.object.ensemble = self.ensemble
|
||||||
self.object.owner = self.request.user
|
self.object.owner = self.request.user
|
||||||
self.object.save()
|
self.object.save()
|
||||||
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 on_signed_request(self):
|
def request_denied(self):
|
||||||
self.add_authorized_key('project', self.kwargs['project'])
|
if 'auth' in self.request.GET:
|
||||||
|
if self.request.GET['auth'] != self.project.auth():
|
||||||
|
raise SuspiciousOperation("Bad project link")
|
||||||
|
self.add_authorized_key('project', self.project.pk, self.project.nonce)
|
||||||
|
return HttpResponseRedirect(resolve_url('project_detail', self.project.pk))
|
||||||
|
return super().request_denied()
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
return self.project
|
return self.project
|
||||||
|
|
||||||
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['project_link'] = signed_url(self.request.path)
|
if self.request.is_admin:
|
||||||
|
#data['project_link'] = signed_url(f'{self.request.path}?nonce={self.project.nonce}')
|
||||||
|
data['project_link'] = self.request.path + "?auth=" + self.project.auth()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class ProjectUpdateView(ProjectMixin, UpdateView):
|
class ProjectUpdateView(ProjectMixin, UpdateView):
|
||||||
|
|||||||
@ -64,7 +64,7 @@ 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)
|
||||||
#version = models.CharField(max_length=30, blank=True, help_text="Limited to specific version tag")
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['order', 'work']
|
ordering = ['order', 'work']
|
||||||
@ -101,6 +101,12 @@ class Collection(models.Model):
|
|||||||
def genres(self):
|
def genres(self):
|
||||||
return self.meta('genre')
|
return self.meta('genre')
|
||||||
|
|
||||||
|
def has_administrator(self, user):
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
return user.pk in self.administrators.values_list('pk', flat=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@ -8,14 +8,14 @@
|
|||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<a class="" href="{% url 'collection_work_list' pk=collection.id %}">
|
<a class="" href="{% url 'collection_work_list' collection=collection.id %}">
|
||||||
<p class="card-header-title">{{ collection.name }}</p>
|
<p class="card-header-title">{{ collection.name }}</p>
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>
|
<p>
|
||||||
{% if collection.location %}{{ collection.location }},{% endif %}
|
{% if collection.location %}{{ collection.location }},{% endif %}
|
||||||
{{ collection.works.count }} items.
|
{{ collection.works.count }} item{{ collection.works.count|pluralize }}.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{% for tag in collection.tags %}
|
{% for tag in collection.tags %}
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
{% if project %}
|
{% if project %}
|
||||||
<li><a href="{% url 'item_list' project=project.pk %}">My Music</a></li>
|
<li><a href="{% url 'item_list' project=project.pk %}">My Music</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.is_admin %}
|
{% if request.user.is_authenticated %}
|
||||||
<li><a href="{% url 'work_list' %}">Library</a></li>
|
<li><a href="{% url 'work_list' %}">Library</a></li>
|
||||||
|
<li><a href="{% url 'collection_list' %}">Collections</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -29,8 +29,8 @@
|
|||||||
<th>Work</th>
|
<th>Work</th>
|
||||||
<th>Composer</th>
|
<th>Composer</th>
|
||||||
<th class="is-hidden-mobile">Edition</th>
|
<th class="is-hidden-mobile">Edition</th>
|
||||||
{% if request.is_admin %}
|
|
||||||
<th class="is-hidden-touch">Collection</th>
|
<th class="is-hidden-touch">Collection</th>
|
||||||
|
{% if request.is_admin %}
|
||||||
<th class="is-hidden-mobile">Copies</th>
|
<th class="is-hidden-mobile">Copies</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
@ -38,11 +38,11 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for work in object_list %}
|
{% for work in object_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'work_detail' pk=work.pk %}">{{ work.name }}</a></td>
|
<td><a href="{% url 'work_detail' collection=work.collection.pk pk=work.pk %}">{{ work.name }}</a></td>
|
||||||
<td title="{{ work.composer }}">{{ work.composer|truncatewords:3 }}</td>
|
<td title="{{ work.composer }}">{{ work.composer|truncatewords:3 }}</td>
|
||||||
<td class="is-hidden-mobile" title="{{ work.edition }}">{{ work.edition|truncatewords:2 }}</td>
|
<td class="is-hidden-mobile" title="{{ work.edition }}">{{ work.edition|truncatewords:2 }}</td>
|
||||||
{% if request.is_admin %}
|
|
||||||
<td class="is-hidden-touch">{{ work.collection.name }}</td>
|
<td class="is-hidden-touch">{{ work.collection.name }}</td>
|
||||||
|
{% if request.is_admin %}
|
||||||
<td class="is-hidden-mobile {{ work.is_available|yesno:'has-text-success,has-text-danger' }}">{{ work.available }}</td>
|
<td class="is-hidden-mobile {{ work.is_available|yesno:'has-text-success,has-text-danger' }}">{{ work.available }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -15,22 +15,23 @@ urlpatterns = [
|
|||||||
path('projects/<int:project>/items', views.ProjectItemListView.as_view(), name="item_list"),
|
path('projects/<int:project>/items', views.ProjectItemListView.as_view(), name="item_list"),
|
||||||
path('projects/<int:project>/items/manage', views.ProjectItemManageView.as_view(), name="item_list_manage"),
|
path('projects/<int:project>/items/manage', views.ProjectItemManageView.as_view(), name="item_list_manage"),
|
||||||
path('projects/<int:project>/items/append', views.ProjectItemAddView.as_view(), name="item_list_append"),
|
path('projects/<int:project>/items/append', views.ProjectItemAddView.as_view(), name="item_list_append"),
|
||||||
|
|
||||||
path('library/collections', views.CollectionListView.as_view(), name="collection_list"),
|
|
||||||
path('library/collections/<int:pk>', views.CollectionWorkListView.as_view(), name="collection_work_list"),
|
|
||||||
path('library/collections/<int:pk>/create', views.WorkAddView.as_view(), name="work_add"),
|
|
||||||
|
|
||||||
path('library/works', views.WorkListView.as_view(), name="work_list"),
|
path('library', views.WorkListView.as_view(), name="work_list"),
|
||||||
path('library/works/<int:pk>', views.WorkDetailView.as_view(), name="work_detail"),
|
|
||||||
path('library/works/<int:pk>/edit', views.WorkUpdateView.as_view(), name="work_edit"),
|
|
||||||
path('library/works/<int:pk>/partset', views.WorkPartSetView.as_view(), name="work_partset"),
|
|
||||||
path('library/works/<int:pk>/add_to_project', views.WorkAddToProject.as_view(), name="work_add_to_project"),
|
|
||||||
path('library/works/<int:pk>/upload', views.WorkAddDocumentView.as_view(), name="document_add"),
|
|
||||||
|
|
||||||
path('library/documents/<int:pk>/delete', views.DocumentDeleteView.as_view(), name="document_delete"),
|
path('collections', views.CollectionListView.as_view(), name="collection_list"),
|
||||||
path('library/documents/<int:pk>/download', views.DocumentDownloadView.as_view(), name="document_download"),
|
path('collections/<int:collection>', views.CollectionWorkListView.as_view(), name="collection_work_list"),
|
||||||
path('library/documents/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
|
path('collections/<int:collection>/add', views.WorkAddView.as_view(), name="work_add"),
|
||||||
path('library/parts/<int:pk>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
|
|
||||||
|
path('collections/<int:collection>/works/<int:pk>', views.WorkDetailView.as_view(), name="work_detail"),
|
||||||
|
path('collections/<int:collection>/works/<int:pk>/edit', views.WorkUpdateView.as_view(), name="work_edit"),
|
||||||
|
path('collections/<int:collection>/works/<int:pk>/partset', views.WorkPartSetView.as_view(), name="work_partset"),
|
||||||
|
path('collections/<int:collection>/works/<int:pk>/add_to_project', views.WorkAddToProject.as_view(), name="work_add_to_project"),
|
||||||
|
path('collections/<int:collection>/works/<int:pk>/upload', views.WorkAddDocumentView.as_view(), name="document_add"),
|
||||||
|
|
||||||
|
path('collections/<int:collection>/docs/<int:pk>/delete', views.DocumentDeleteView.as_view(), name="document_delete"),
|
||||||
|
path('collections/<int:collection>/docs/<int:pk>/download', views.DocumentDownloadView.as_view(), name="document_download"),
|
||||||
|
path('collections/<int:collection>/docs/<int:pk>/annotate', views.DocumentAnnotateView.as_view(), name="document_annotate"),
|
||||||
|
path('collections/<int:collection>/docs/<int:pk>/<str:filename>', views.PartDownloadView.as_view(), name="part_download"),
|
||||||
|
|
||||||
#path('api/', include(router.urls))
|
#path('api/', include(router.urls))
|
||||||
path('api/library/collections/<int:pk>/export', api.CollectionExportView.as_view(), name="collection_export"),
|
path('api/library/collections/<int:pk>/export', api.CollectionExportView.as_view(), name="collection_export"),
|
||||||
|
|||||||
@ -13,13 +13,18 @@ import json
|
|||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from interface.views import EnsembleMixin, ProjectMixin
|
from interface.views import EnsembleMixin, ProjectMixin, AuthorizedResourceMixin
|
||||||
from interface.models import Project
|
from interface.models import Project
|
||||||
from library.models import Collection, Work, Document, Section
|
from library.models import Collection, Work, Document, Section
|
||||||
from library.imslp import INSTRUMENT_TAGS, INSTRUMENTS
|
from library.imslp import INSTRUMENT_TAGS, INSTRUMENTS
|
||||||
from library import forms, models
|
from library import forms, models
|
||||||
from library.pdf_utils import extract_pages, extract_and_concat
|
from library.pdf_utils import extract_pages, extract_and_concat
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ProjectItemListView(ProjectMixin, ListView):
|
class ProjectItemListView(ProjectMixin, ListView):
|
||||||
template_name = "library/item_list.html"
|
template_name = "library/item_list.html"
|
||||||
model = models.ProjectItem
|
model = models.ProjectItem
|
||||||
@ -76,6 +81,7 @@ class ProjectItemListView(ProjectMixin, ListView):
|
|||||||
# data['running_time'] = "-:--"
|
# data['running_time'] = "-:--"
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class ProjectItemManageView(ProjectMixin, ListView):
|
class ProjectItemManageView(ProjectMixin, ListView):
|
||||||
template_name = "library/item_list_manage.html"
|
template_name = "library/item_list_manage.html"
|
||||||
model = models.ProjectItem
|
model = models.ProjectItem
|
||||||
@ -109,25 +115,60 @@ class ProjectItemAddView(ProjectMixin, UpdateView):
|
|||||||
def get_object(self):
|
def get_object(self):
|
||||||
return self.get_project()
|
return self.get_project()
|
||||||
|
|
||||||
class CollectionListView(EnsembleMixin, ListView):
|
""" COLLECTION VIEWS """
|
||||||
|
|
||||||
|
class CollectionMixin(AuthorizedResourceMixin):
|
||||||
|
def is_authorized(self):
|
||||||
|
super().is_authorized()
|
||||||
|
try:
|
||||||
|
self.collection = self.get_collection()
|
||||||
|
return True
|
||||||
|
except (models.Collection.DoesNotExist, KeyError):
|
||||||
|
return False
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Collection.objects.filter(administrators=self.request.user)
|
return super().get_queryset().filter(collection=self.collection)
|
||||||
|
|
||||||
|
def get_collection(self):
|
||||||
|
collection = self.get_collection_queryset().get(pk=self.kwargs['collection'])
|
||||||
|
self.request.is_admin = collection.has_administrator(self.request.user)
|
||||||
|
return collection
|
||||||
|
|
||||||
class WorkListView(EnsembleMixin, ListView):
|
def get_collection_queryset(self):
|
||||||
|
collections = models.Collection.objects.all()
|
||||||
|
|
||||||
|
if self.request.user.is_anonymous:
|
||||||
|
return models.Collection.objects.none()
|
||||||
|
|
||||||
|
if self.request.is_admin:
|
||||||
|
return collections
|
||||||
|
|
||||||
|
return collections.filter(Q(administrators=self.request.user) | Q(allowed_ensembles__ensemble__admins=self.request.user))
|
||||||
|
|
||||||
|
class CollectionListView(CollectionMixin, ListView):
|
||||||
|
paginate_by = 20
|
||||||
|
|
||||||
|
def is_authorized(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_collection_queryset()
|
||||||
|
|
||||||
|
class WorkListView(CollectionMixin, ListView):
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
|
|
||||||
def get_works(self):
|
def get_works(self):
|
||||||
return Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id).order_by('name').distinct().select_related('collection')
|
collections = CollectionMixin.get_queryset(self)
|
||||||
|
return Work.objects.filter(collection__in=collections).order_by('name').distinct().select_related('collection')
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
data = super(WorkListView, self).get_context_data(*args, **kwargs)
|
data = super(WorkListView, self).get_context_data(*args, **kwargs)
|
||||||
data['title'] = f'Music available to {self.ensemble.name}'
|
#data['title'] = f'Music available to {self.ensemble.name}'
|
||||||
|
data['title'] = "My Library"
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
works = self.get_works().order_by('name')
|
works = self.get_works()
|
||||||
|
|
||||||
q = self.request.GET.get('filter')
|
q = self.request.GET.get('filter')
|
||||||
if q:
|
if q:
|
||||||
@ -142,7 +183,7 @@ class WorkListView(EnsembleMixin, ListView):
|
|||||||
class CollectionWorkListView(WorkListView):
|
class CollectionWorkListView(WorkListView):
|
||||||
|
|
||||||
def get_works(self):
|
def get_works(self):
|
||||||
works = Work.objects.filter(collection=self.kwargs['pk']).distinct()
|
works = Work.objects.filter(collection=self.kwargs['collection']).distinct()
|
||||||
|
|
||||||
loan_count = Count('project_items', Q(project_items__checkout__lte=now(), project_items__returned=None))
|
loan_count = Count('project_items', Q(project_items__checkout__lte=now(), project_items__returned=None))
|
||||||
works = works.annotate(loan_count=loan_count)
|
works = works.annotate(loan_count=loan_count)
|
||||||
@ -150,11 +191,10 @@ class CollectionWorkListView(WorkListView):
|
|||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
data = super(CollectionWorkListView, self).get_context_data(*args, **kwargs)
|
data = super(CollectionWorkListView, self).get_context_data(*args, **kwargs)
|
||||||
data['title'] = Collection.objects.get(pk=self.kwargs['pk']).name
|
data['title'] = self.collection.name
|
||||||
data['collection_id'] = self.kwargs['pk']
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class WorkAddView(EnsembleMixin, FormView):
|
class WorkAddView(CollectionMixin, FormView):
|
||||||
template_name = "interface/default_form.html"
|
template_name = "interface/default_form.html"
|
||||||
form_class = forms.WorkCreateForm
|
form_class = forms.WorkCreateForm
|
||||||
|
|
||||||
@ -185,10 +225,10 @@ class WorkMixin(object):
|
|||||||
|
|
||||||
return Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id)
|
return Work.objects.filter(collection__allowed_ensembles__ensemble=self.request.ensemble_id)
|
||||||
|
|
||||||
class WorkDetailView(EnsembleMixin, WorkMixin, DetailView):
|
class WorkDetailView(CollectionMixin, WorkMixin, DetailView):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class WorkUpdateView(EnsembleMixin, WorkMixin, UpdateView):
|
class WorkUpdateView(CollectionMixin, WorkMixin, UpdateView):
|
||||||
fields = ['name', 'composer', 'edition', 'code', 'licence', 'max_projects', 'running_time', 'notes']
|
fields = ['name', 'composer', 'edition', 'code', 'licence', 'max_projects', 'running_time', 'notes']
|
||||||
template_name = 'interface/default_form.html'
|
template_name = 'interface/default_form.html'
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user