115 Commits
Author SHA1 Message Date
tris 5468f6d3e7 Added handling of gdrive resource_keys - fixes #16 2026-05-27 22:46:08 +10:00
tris e46d8145a7 Install from git 2026-05-27 09:35:15 +10:00
tris 3444cdbc59 Merge pull request 'gdrive' (#15) from gdrive into master
Reviewed-on: #15
2026-05-24 16:45:13 +10:00
tris 6e77474d15 Couple of fixes 2026-05-24 16:42:51 +10:00
tris 72bad08a61 Merge branch 'master' into gdrive 2026-05-24 14:24:34 +10:00
tris b217fbea5e Sorted out linting 2026-05-24 14:23:59 +10:00
tris cec64ecd2f Working with manual sync 2026-05-24 13:51:23 +10:00
tris d30005d5b6 Indexing bug fix 2026-05-24 11:48:42 +10:00
tris ef981e06e8 Shared gdrive access 2026-05-24 11:35:18 +10:00
tris 4cef5800bc Merge pull request 'Repackaged as a single project' (#13) from repackage into master
Reviewed-on: #13
2026-05-24 11:11:27 +10:00
tris 78441dc142 Version bump 2026-05-24 11:07:49 +10:00
tris c96a2ad80b Refactored docker 2026-05-24 11:00:05 +10:00
tris 7e0c77f260 Refactored to polyphonic package 2026-05-24 10:51:43 +10:00
tris ff17114514 Minor test change 2026-05-24 10:07:05 +10:00
tris 533d6f09e8 Fixed compose launch 2026-05-23 22:10:37 +10:00
tris eeb35ce4f6 Modify PYTHONPATH 2026-05-23 21:46:51 +10:00
tris 947626c2af Added indexer tests 2026-05-23 11:38:40 +10:00
tris c1f0e48f80 Another go at sorting the settings mess 2026-05-23 10:00:09 +10:00
tris ab7d32d46e Renamed to poly-tool 2026-05-23 00:05:29 +10:00
tris 5116247ae8 Simplified settings 2026-05-22 23:51:25 +10:00
tris 93c4926dfd Updated docker build 2026-05-22 14:11:03 +10:00
tris 27d1b03c3c Added fuzzy matching 2026-05-14 11:02:55 +10:00
tris 4e102c07ac Skip migrations 2026-05-13 11:08:03 +10:00
tris b1ea75cec0 Merge pull request 'GdriveFolder code' (#12) from gdrive into master
Reviewed-on: #12
2026-05-13 09:35:48 +10:00
tris 7d041e1fd0 GdriveFolder code 2026-05-13 09:32:22 +10:00
tris 504c2ee56b Moving makefile to poetry 2026-05-12 23:24:46 +10:00
tris cfd6d45189 Added linting and got tests passing 2026-05-12 13:26:56 +10:00
tris ca62ed693a Code cleanup 2026-05-12 11:10:21 +10:00
tris 5e0e165037 Interface cleanup 2026-05-12 11:04:22 +10:00
tris 4164d56dea Library cleanup 2026-05-12 11:03:11 +10:00
tris 75dced77b8 Fixed logout + cleanup 2026-05-11 22:53:31 +10:00
tris b3675e28af Cleanup 2026-05-11 22:06:05 +10:00
tris b86c867bd2 Merge pull request 'whoosh_search' (#11) from whoosh_search into master
Reviewed-on: #11
2026-05-11 21:55:52 +10:00
tris 9248692e5d Merge branch 'master' into whoosh_search 2026-05-11 21:51:15 +10:00
tris 406609262d Cleanup 2026-05-11 21:50:57 +10:00
tris ffb31cc004 Implemented proper search engine 2026-05-11 21:50:21 +10:00
tris b1ef2b9dac Switch to Django 6.0 2026-04-30 17:09:05 +10:00
tris 5c56d40bf8 Cleanup 2026-04-30 16:01:53 +10:00
tris a1341d1edc Cleanup 2025-08-29 16:40:36 +10:00
tris 4d964291b2 Fixed poetry deps 2025-08-29 12:51:01 +10:00
tris ee5305ba6c Merge branch 'master' of gitea.tfconsulting.com.au:projects/polyphonic 2025-08-29 11:39:58 +10:00
tris 147c84550c Switching to poetry 2025-08-29 11:37:57 +10:00
Tris Forster 596445061f Upgraded to Django 5.1 2024-08-26 12:06:18 +10:00
Tris Forster 02858a76c0 Cleanup 2024-08-05 15:49:39 +10:00
Tris Forster 1bcec919cf PEP Cleanup 2024-08-05 12:53:58 +10:00
Tris Forster 78789c02ed Minor UI Tweaks 2024-06-21 15:42:09 +10:00
Tris Forster 04bf1ab1f3 Improved auto tagging 2023-03-27 10:34:20 +11:00
Tris Forster fb700b15d6 Added search bar to collection list 2023-03-04 17:12:58 +11:00
Tris Forster 0bd2596ebc Added search bar to collection list 2023-03-04 17:06:28 +11:00
Tris Forster 1f0a336ed4 Added search bar to collection list 2023-03-04 17:02:18 +11:00
Tris Forster 220163bca2 Added Number 2023-03-03 19:56:22 +11:00
Tris Forster d1328ae1b1 Many interface changes 2023-03-03 16:07:30 +11:00
Tris Forster 69f747d5b5 Fixed part selection dropdown 2023-03-02 16:25:38 +11:00
Tris Forster 2a6554471d List works on project page 2023-03-02 13:28:24 +11:00
Tris Forster 6edc612ed6 Fixed gitignore 2023-03-02 12:30:39 +11:00
Tris Forster f441940e8c Layout tweeks 2023-03-02 12:15:19 +11:00
Tris Forster b1eaf9c7bc Added document extractor 2023-03-02 10:05:26 +11:00
Tris Forster a892b0bc41 Got api work import working 2023-03-02 07:09:38 +11:00
Tris Forster f1757be96e Dont tag files that aren't PDFs 2023-03-01 14:42:05 +11:00
Tris Forster 95ee92cef6 Fixed related work links 2023-03-01 14:13:52 +11:00
Tris Forster b11cfcd6ea Added primitive doc type checking 2023-03-01 13:56:52 +11:00
Tris Forster 6e1b26440f Added coverage testing 2023-03-01 13:39:56 +11:00
Tris Forster 4268b66b27 Added module management 2023-03-01 13:15:09 +11:00
Tris Forster f22961ad44 Couple of little fixes 2023-03-01 12:41:42 +11:00
Tris Forster caeee16657 Extended PROTETED_URLS tests 2023-03-01 12:41:10 +11:00
Tris Forster 4344cb978b Fixed reference to section type 2023-03-01 11:25:38 +11:00
Tris Forster a3d8cf0a21 Fixed reference to section type 2023-03-01 11:21:06 +11:00
Tris Forster 70aa7ae3a3 Fixed reference to section type 2023-03-01 11:18:35 +11:00
Tris Forster f840ee3d8b Couple of interface tweeks 2023-02-28 16:56:22 +11:00
Tris Forster 7d4f959146 Fixing work edit 2023-02-23 20:33:38 +11:00
Tris Forster 1cb00ebc0b Fixing bulk importer 2023-02-23 20:29:04 +11:00
Tris Forster 928173976b Got my music working again 2023-02-23 19:29:20 +11:00
Tris Forster d7892d355d Moved project filtering to the right place - the queryset 2023-02-23 19:18:23 +11:00
Tris Forster dbbfa79f10 No date for perpetual projects 2023-02-23 19:03:01 +11:00
Tris Forster f49ff0fd0e Moved project list to model 2023-02-23 18:37:21 +11:00
Tris Forster 6ec5808275 Refactored tests 2023-02-23 18:36:39 +11:00
Tris Forster 948e9deb54 Got collection links working and tested 2023-02-23 14:27:50 +11:00
Tris Forster 75de40f2bd Cleaned up access tests 2023-02-23 10:22:58 +11:00
Tris Forster 295999eaef Refactored test access (eventually) 2023-02-22 18:46:12 +11:00
Tris Forster f9fa9d5e05 Stupid typo 2023-02-21 09:11:14 +11:00
Tris Forster c720cb773e Getting partset working 2023-02-20 14:21:34 +11:00
Tris Forster da15eebb0a Silly fix 2023-02-20 10:31:56 +11:00
Tris Forster e74043ae67 Fixed work create and some security issues 2023-02-20 10:28:38 +11:00
Tris Forster 5a201cef85 Fixed add work 2023-02-20 08:03:32 +11:00
Tris Forster 5fb5138383 Fix ensemble reference 2023-02-20 07:56:53 +11:00
Tris Forster 039b440a57 Tweak! 2023-02-20 03:38:23 +11:00
Tris Forster 7d394e2f34 Updated requirements 2023-02-16 20:47:16 +11:00
Tris Forster 13f466228b Got library working again 2023-02-16 17:22:18 +11:00
Tris Forster b85440d25c Finally got authentication to somewhere I'm happy 2023-02-10 12:01:32 +11:00
Tris Forster bc9f292a2e Major changes to permission system 2023-02-03 10:10:54 +11:00
Tris Forster 8a249de51c Tweeking section handling 2023-01-04 09:52:22 +11:00
Tris Forster 7e47eec4ae Orchestration cleanup 2022-12-10 10:18:45 +11:00
Tris Forster 59deeffefe Improved upload 2022-12-02 13:24:31 +11:00
Tris Forster 53ec846f98 Refactored into /app 2022-11-28 14:11:39 +11:00
Tris Forster 94bba3769a Pre refactor 2022-11-27 10:20:13 +11:00
Tris Forster 2726a8fe04 Adding font 2022-11-19 12:46:58 +11:00
Tris Forster 4731d18131 Add static folder 2022-11-19 12:32:17 +11:00
Tris Forster c639020ac9 Fixing migrations 2022-11-19 12:24:40 +11:00
Tris Forster 7f6875f3c4 Updated byostorage 2022-11-19 12:10:35 +11:00
Tris Forster 025e1344f0 Recreating migrations 2022-11-19 22:43:15 +11:00
Tris Forster bbc74a77f9 Removed old deps 2022-11-19 22:37:37 +11:00
Tris Forster dfe4a925c7 Added secret key! 2022-11-19 22:35:06 +11:00
Tris Forster 18e5893cc2 Updated requirements 2022-11-19 22:33:21 +11:00
Tris Forster 988161b599 Added default settings 2022-11-19 22:31:06 +11:00
Tris Forster f7aaa98000 Library schema semi-fixed 2022-11-19 21:50:01 +11:00
Tris Forster 8f18b9ab9d Removing old files 2022-11-19 21:30:59 +11:00
Tris 598ee5ad7e Got library sort of working 2021-09-04 10:29:22 +10:00
Tris 7aedc6bd07 Added composer field 2021-03-26 09:27:38 +11:00
Tris f50fe11ced Migration 2021-03-22 11:55:00 +11:00
Tris 12beebd486 Updated byostorage 2021-03-22 10:48:24 +11:00
Tris 9094cae63f Updated byostorage 2021-03-22 10:38:33 +11:00
Tris 1581f56b74 Library working: 2021-03-22 10:30:13 +11:00
Tris 35555c3321 Hooked up library app 2021-03-11 09:33:51 +11:00
Tris 526ae4f59b Created library app 2021-03-11 09:32:44 +11:00
Tris 98fe7b77ce Added slug fields to Ensemble and Project 2021-03-03 21:07:36 +11:00
166 changed files with 7185 additions and 2128 deletions
+2
View File
@@ -0,0 +1,2 @@
local_settings.py
db.sqlite3
+17 -6
View File
@@ -1,9 +1,20 @@
__pycache__
*.pyc
db.sqlite3
*.sqlite3
*.swp
credentials.json
credentials
polyphonic/settings.py
env
test.*
static
teststore
local_settings.py
.coverage
Session.vim
poetry.lock
/env
/data
/old
/static
/teststore
/cache
/local_storage
/media
/index
/dist
+25
View File
@@ -0,0 +1,25 @@
FROM alpine:latest
ENV TARGET=/opt/polyphonic
#ENV RELEASE=polyphonic-0.8.4-py3-none-any.whl
ENV RELEASE=git+https://gitea.tfconsulting.com.au/projects/polyphonic.git
RUN apk add --no-cache python3 py3-pip git ghostscript sqlite
WORKDIR /root
RUN python3 -m venv ${TARGET}
ENV PATH="${TARGET}/bin:$PATH"
#COPY dist/${RELEASE} .
RUN pip3 install ${RELEASE} --no-cache-dir
RUN pip3 install gunicorn whitenoise
WORKDIR ${TARGET}
RUN SECRET_KEY=_ poly-tool collectstatic --noinput
VOLUME ["/var/polyphonic"]
EXPOSE 8000/tcp
CMD ["gunicorn", "-b", "0.0.0.0", "polyphonic.config.wsgi"]
+19 -7
View File
@@ -1,15 +1,28 @@
PYTHON=env/bin/python
DROPZONE=5.7.0
test:
poetry run coverage run --include "polyphonic/*" --omit "*/migrations/*" polyphonic/manage.py test polyphonic
poetry run coverage html
poetry run coverage report
check:
poetry run ruff check polyphonic
poetry run ruff format --check polyphonic
pre-commit: check test
build:
poetry build
dev-setup:
env/bin/pip install -r requirements.txt
env/bin/pip install -r dev-requirements.txt
${PYTHON} manage.py migrate
${PYTHON} manage.py createsuperuser --username admin --email admin@localhost
poetry install --with=dev
poetry run manage migrate
poetry run manage createsuperuser --username admin --email admin@localhost
upgrade:
${PYTHON} manage.py migrate
${PYTHON} manage.py collectstatic
poetry run manage migrate
poetry run manage collectstatic
${MAKE} libraries
libraries: static/dropzone static/fonts/Quicksand_Book.otf
@@ -35,4 +48,3 @@ start_s3_storage:
stop_s3_storage:
kill `cat teststore/pid` | true
rm teststore/pid
+19 -2
View File
@@ -1,13 +1,28 @@
## Polyphonic
A simple web app for managing video uploads to an S3 bucket for virtual ensembles.
A simple web app managing scores, large files and submissions in a manor tailored to musical ensembles.
No registration required for ensemble participants - just a one time code and passphrase.
### Library App
Store all your scores on your own cloud account (Amazon S3, Google Files etc).
Tag up the scores so you can generate custom part sets and assign them to
projects so people can easily print just their parts.
### Submissions App
Accept video/audio submissions direct to your cloud storage. Was developed and
used during 2020 lockdown period for virtual choirs/orchestras but could have more uses.
### S3 Setup
#### Bucket setup [virtual-orchestra]
Default block public access
Permissions -> CORS
```xml
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
@@ -28,6 +43,7 @@ Permissions -> CORS
User
Create with programatic access (copy keys) and an inline policy for the bucket.
```json
{
"Version": "2012-10-17",
@@ -51,4 +67,5 @@ Create with programatic access (copy keys) and an inline policy for the bucket.
}
```
3.
3.
+19
View File
@@ -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
-1
View File
@@ -1 +0,0 @@
pylint==2.6.0
+14
View File
@@ -0,0 +1,14 @@
services:
polyphonic:
image: "polyphonic:0.8.4"
build: "."
ports:
- "8001:8000"
volumes:
- "./data:/var/polyphonic"
- "./local_settings.py:/opt/polyphonic/local_settings.py"
env_file: "compose.env"
environment:
DJANGO_SETTINGS_MODULE: local_settings
PYTHONPATH: /opt/polyphonic
WORK_DIR: /var/polyphonic
-31
View File
@@ -1,31 +0,0 @@
from django.contrib import admin
# Register your models here.
from . import models
class EnsembleAdmin(admin.ModelAdmin):
list_display = ['name', 'ensemble_code']
class ProjectAdmin(admin.ModelAdmin):
list_display = ['name', 'ensemble', 'deadline', 'active']
list_filter = ['ensemble', 'active']
class SubmissionAdmin(admin.ModelAdmin):
list_display = ['name', 'instrument', 'date', 'complete']
list_filter = ['project', 'complete']
class ResourceAdmin(admin.ModelAdmin):
list_display = ['name', 'media_type', 'project']
list_filter = ['project']
class WikiPageAdmin(admin.ModelAdmin):
list_display = ['title', 'project']
list_filter = ['project']
admin.site.register(models.Ensemble, EnsembleAdmin)
admin.site.register(models.Project, ProjectAdmin)
admin.site.register(models.Submission, SubmissionAdmin)
admin.site.register(models.Resource, ResourceAdmin)
admin.site.register(models.WikiPage, WikiPageAdmin)
-17
View File
@@ -1,17 +0,0 @@
from django import forms
from .models import Submission
class CodeForm(forms.Form):
code = forms.CharField(max_length=14,
widget=forms.TextInput(attrs={'placeholder': 'xxx-xxx-xxx', 'inputmode': 'numeric'}))
passphrase = forms.CharField(max_length=32)
class SubmissionForm(forms.ModelForm):
method = forms.ChoiceField(choices=(
('upload', 'I need to upload a file'),
('link', 'I have a link from my own cloud storage provider')
), initial='upload')
class Meta:
model = Submission
fields = ['name', 'instrument', 'method', 'notes']
-40
View File
@@ -1,40 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-04 09:59
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Project',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='WikiPage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('markdown', models.TextField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiki_pages', to='interface.project')),
],
),
migrations.CreateModel(
name='Submission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('instrument', models.CharField(max_length=100)),
('notes', models.TextField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='interface.project')),
],
),
]
@@ -1,31 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-04 10:04
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('interface', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='submission',
name='date',
field=models.DateField(auto_created=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='wikipage',
name='title',
field=models.CharField(default='', max_length=255),
preserve_default=False,
),
migrations.AlterField(
model_name='submission',
name='notes',
field=models.TextField(blank=True),
),
]
@@ -1,33 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-05 01:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('interface', '0002_auto_20200904_1004'),
]
operations = [
migrations.CreateModel(
name='Ensemble',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('code', models.CharField(max_length=12)),
('password', models.CharField(max_length=100)),
],
),
migrations.AddField(
model_name='project',
name='active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='project',
name='ensemble',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='interface.ensemble'),
),
]
@@ -1,34 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-05 01:27
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('interface', '0003_auto_20200905_0118'),
]
operations = [
migrations.AddField(
model_name='project',
name='bucket',
field=models.CharField(default='', max_length=100),
preserve_default=False,
),
migrations.AddField(
model_name='project',
name='deadline',
field=models.DateField(blank=True, null=True),
),
migrations.CreateModel(
name='Resource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('uri', models.CharField(max_length=255)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resources', to='interface.project')),
],
),
]
@@ -1,23 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-05 06:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0004_auto_20200905_0127'),
]
operations = [
migrations.AddField(
model_name='submission',
name='complete',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='submission',
name='date',
field=models.DateField(auto_now_add=True),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-05 09:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0005_auto_20200905_0638'),
]
operations = [
migrations.AddField(
model_name='submission',
name='key',
field=models.CharField(blank=True, max_length=255),
),
]
@@ -1,35 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-06 10:09
from django.db import migrations, models
import django.db.models.deletion
import interface.models
class Migration(migrations.Migration):
dependencies = [
('interface', '0006_submission_key'),
]
operations = [
migrations.RemoveField(
model_name='project',
name='bucket',
),
migrations.AddField(
model_name='ensemble',
name='bucket',
field=models.CharField(default='', max_length=100),
preserve_default=False,
),
migrations.AlterField(
model_name='ensemble',
name='code',
field=models.CharField(default=interface.models.generate_code, max_length=12),
),
migrations.AlterField(
model_name='submission',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='all_submissions', to='interface.project'),
),
]
@@ -1,24 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-06 11:22
from django.db import migrations, models
import interface.models
class Migration(migrations.Migration):
dependencies = [
('interface', '0007_auto_20200906_1009'),
]
operations = [
migrations.RenameField(
model_name='ensemble',
old_name='password',
new_name='passphrase',
),
migrations.AlterField(
model_name='ensemble',
name='code',
field=models.CharField(default=interface.models.generate_code, max_length=9),
),
]
@@ -1,27 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-07 01:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0008_auto_20200906_1122'),
]
operations = [
migrations.RemoveField(
model_name='submission',
name='key',
),
migrations.AddField(
model_name='submission',
name='location',
field=models.CharField(blank=True, max_length=512),
),
migrations.AlterField(
model_name='ensemble',
name='bucket',
field=models.CharField(max_length=255),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-07 01:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0009_auto_20200907_0103'),
]
operations = [
migrations.RenameField(
model_name='submission',
old_name='location',
new_name='key',
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-07 02:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0010_auto_20200907_0148'),
]
operations = [
migrations.AlterField(
model_name='submission',
name='date',
field=models.DateTimeField(auto_now_add=True),
),
]
@@ -1,17 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-07 04:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0011_auto_20200907_0234'),
]
operations = [
migrations.RemoveField(
model_name='ensemble',
name='bucket',
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-07 04:55
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0012_remove_ensemble_bucket'),
]
operations = [
migrations.RenameField(
model_name='resource',
old_name='uri',
new_name='key',
),
]
@@ -1,23 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-09 00:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0013_auto_20200907_1455'),
]
operations = [
migrations.AddField(
model_name='resource',
name='description',
field=models.TextField(blank=True),
),
migrations.AlterField(
model_name='resource',
name='key',
field=models.CharField(blank=True, max_length=255),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-09 01:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0014_auto_20200909_1016'),
]
operations = [
migrations.AddField(
model_name='resource',
name='media_type',
field=models.CharField(choices=[('audio', 'Audio'), ('video', 'Video'), ('*', 'General')], default='*', max_length=10),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-10 10:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0015_resource_media_type'),
]
operations = [
migrations.AlterField(
model_name='resource',
name='media_type',
field=models.CharField(choices=[('audio', 'Audio'), ('video', 'Video'), ('general', 'General')], default='*', max_length=10),
),
]
@@ -1,25 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-13 23:43
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('interface', '0016_auto_20200910_2025'),
]
operations = [
migrations.AddField(
model_name='ensemble',
name='admins',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='submission',
name='instrument',
field=models.CharField(max_length=100, verbose_name='Instrument / Voice'),
),
]
@@ -1,25 +0,0 @@
# Generated by Django 3.1.1 on 2020-09-14 00:09
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('interface', '0017_auto_20200914_0943'),
]
operations = [
migrations.AddField(
model_name='resource',
name='visible',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='ensemble',
name='admins',
field=models.ManyToManyField(related_name='ensembles', to=settings.AUTH_USER_MODEL),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-10-03 09:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0018_auto_20200914_1009'),
]
operations = [
migrations.AddField(
model_name='project',
name='owner',
field=models.CharField(blank=True, max_length=255),
),
]
@@ -1,23 +0,0 @@
# Generated by Django 3.1.1 on 2020-10-03 11:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0019_project_owner'),
]
operations = [
migrations.RenameField(
model_name='submission',
old_name='key',
new_name='url',
),
migrations.AddField(
model_name='submission',
name='private',
field=models.BooleanField(default=False),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 3.1.1 on 2020-10-05 03:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('interface', '0020_auto_20201003_2103'),
]
operations = [
migrations.AddField(
model_name='project',
name='description',
field=models.TextField(blank=True),
),
]
-146
View File
@@ -1,146 +0,0 @@
from django.db import models
from django.utils.text import slugify
from django.utils import timezone
from django.conf import settings
from django.shortcuts import resolve_url
import random
import boto3
from datetime import datetime
from urllib.parse import urlparse
import os.path
s3client = boto3.client('s3', **getattr(settings, 'S3_CREDENTIALS', {}))
BUCKET = settings.AWS_BUCKET
MEDIA_TYPES = [
('audio', "Audio"),
('video', "Video"),
('general', "General"),
]
def generate_code(length=9):
return "".join([ random.choice('0123456789') for _ in range(length) ])
class Ensemble(models.Model):
name = models.CharField(max_length=100)
code = models.CharField(max_length=9, default=generate_code)
passphrase = models.CharField(max_length=100)
admins = models.ManyToManyField('auth.User', related_name='ensembles')
def active_projects(self):
return self.projects.filter(active=True)
def ensemble_code(self):
code = str(self.code)
return "{}-{}-{}".format(code[:3], code[3:6], code[6:])
def __str__(self):
return self.name
class Project(models.Model):
name = models.CharField(max_length=100)
ensemble = models.ForeignKey(Ensemble, related_name='projects', on_delete=models.CASCADE, null=True)
description = models.TextField(blank=True)
active = models.BooleanField(default=True)
deadline =models.DateField(null=True, blank=True)
owner = models.CharField(max_length=255, blank=True)
@property
def submissions(self):
return self.all_submissions.filter(complete=True).order_by('-pk')
def presigned_post(self, object_name, fields=None, conditions=None, expires=3600):
key = os.path.join(slugify(self.name), object_name)
return s3client.generate_presigned_post(BUCKET, key, Fields=fields or {}, Conditions=conditions or [], ExpiresIn=expires)
def __str__(self):
return self.name
class Resource(models.Model):
project = models.ForeignKey(Project, related_name='resources', on_delete=models.CASCADE)
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
key = models.CharField(max_length=255, blank=True)
media_type = models.CharField(max_length=10, choices=MEDIA_TYPES, default='*')
visible = models.BooleanField(default=True)
def key_template(self):
return "{}/${{filename}}".format(slugify(self.name))
def accept(self):
if self.media_type == 'general':
return ".*"
return f"{self.media_type}/*"
def presigned_url(self):
if not self.key:
return ""
params = {'Bucket': BUCKET, 'Key': self.key}
return s3client.generate_presigned_url('get_object', Params=params, ExpiresIn=3600*24)
def __str__(self):
return self.name
class WikiPage(models.Model):
project = models.ForeignKey(Project, related_name='wiki_pages', on_delete=models.CASCADE)
title = models.CharField(max_length=255)
markdown = models.TextField()
def get_absolute_url(self):
return resolve_url('wiki', project=self.project_id, pk=self.pk)
def __str__(self):
return self.title
class Submission(models.Model):
project = models.ForeignKey(Project, related_name='all_submissions', on_delete=models.CASCADE)
date = models.DateTimeField(auto_now_add=True, )
name = models.CharField(max_length=255)
instrument = models.CharField(max_length=100, verbose_name="Instrument / Voice")
notes = models.TextField(blank=True)
complete = models.BooleanField(default=False)
url = models.CharField(max_length=512, blank=True)
private = models.BooleanField(default=False)
@property
def download_url(self):
if not self.complete:
raise RuntimeError("Submission not complete")
if self.private:
return self.url
params = {'Bucket': BUCKET, 'Key': self.url}
return s3client.generate_presigned_url('get_object', Params=params, ExpiresIn=3600)
@property
def download_name(self):
uri = urlparse(self.download_url)
_, name = os.path.split(uri.path)
return name or "<Unknown>"
def key_template(self):
return "submissions/{}_{}_{}_${{filename}}".format(
timezone.localtime(self.date).isoformat(timespec='seconds').replace(':', '')[:17],
slugify(self.name),
slugify(self.instrument)
)
@property
def short_name(self):
_, ext = os.path.splitext(self.download_name)
return "{}_{}_{}{}".format(
#timezone.localtime(self.date).strftime("%Y%m%d%H%M%S"),
slugify(self.name),
slugify(self.instrument),
self.pk,
ext
)
def __str__(self):
return f"{self.name}: {self.date}"
@@ -1,279 +0,0 @@
:root {
--border-color: #292929;
--gray-blue: #667788;
--light-blue: #c5eff7;
}
@font-face {
font-family: 'Quicksand';
src: url('../../fonts/Quicksand_Book.otf');
}
@font-face {
font-family: 'QuicksandBold';
src: url('../../fonts/Quicksand_Bold_Oblique.otf');
}
.debug DIV {
border: 1px dashed #DDD;
}
HTML {
height: 100%;
}
BODY {
background-image: url('../background.png');
background-position: center top;
background-size: 100%;
background-repeat: no-repeat;
margin: 0px;
height: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
}
.main {
max-width: 1000px;
margin: 10px auto;
border: 1px solid var(--border-color);
border-radius: 5px;
font-family: 'Quicksand', Arial, Helvetica, sans-serif;
font-size: 14pt;
background-color: white;
}
.content {
margin: 20px;
flex-direction: column;
}
.narrow {
max-width: 500px;
margin: 0px auto;
}
.collapse {
display: flex;
flex-direction: row;
justify-content: space-around;
}
@media all and (max-width: 900px) {
.mdhide {
display: none;
}
}
@media all and (max-width: 700px) {
.smhide {
display: none;
}
.collapse {
flex-direction: column;
}
}
/* HEADER BAR */
.navigation {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 50px;
background-color: var(--gray-blue);
color: var(--light-blue) !important;
}
.navigation > * {
white-space: nowrap;
}
.navigation A,
.navigation A:visited {
color: var(--light-blue);
text-decoration: none;
}
.navigation .brand {
font-family: 'QuicksandBold', 'Quicksand', Arial, Helvetica, sans-serif;
font-size: 1.5rem;
margin: auto 20px;
}
UL.nav-buttons {
display: flex;
list-style: none;
}
UL.nav-buttons > LI {
margin: 2px 10px;
}
/* FORMS */
FORM.vertical {
display: flex;
flex-direction: column;
max-width: 400px;
margin: 0px auto;
}
LABEL {
margin-top: 20px;
}
TEXTAREA {
height: 50px;
}
INPUT[type=checkbox] {
margin-right: auto;
}
.form-actions {
text-align: right;
margin-top: 20px;
}
.btn {
background-color: var(--gray-blue);
display: inline-block;
border: none;
color: var(--light-blue);
text-decoration: none;
border-radius: 1em;
font-size: 1em;
}
.btn:hover {
cursor: pointer;
color: white;
}
.pills {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 40px;
}
.pills A {
border: 1px solid var(--gray-blue);
padding: 4px 10px 2px 10px;
margin: 10px 5px;
border-radius: 10px;
white-space: nowrap;
}
.pills A:hover {
background-color: var(--light-blue);
text-decoration: none
}
.list-group {
display: flex;
flex-direction: column;
}
.list-group > * {
border: 1px solid var(--gray-blue);
border-radius: 10px;
padding: 2px 20px;
margin-top: 20px;
}
.list-group > A:hover {
background-color: var(--light-blue);
text-decoration: none;
}
/* PROGRESS BAR */
.progress {
display: relative;
border: 1px solid var(--border-color);
border-radius: 5px;
margin: 20px 10px;
}
.progress-bar {
width: 0%;
height: 1.5em;
background-color: var(--light-blue);
border-radius: 5px;
}
.text-center {
text-align: center;
}
A, A:visited {
text-decoration: none;
color: var(--gray-blue);
font-weight: bold;
}
A:hover {
text-decoration: underline;
}
H1 {
text-align: center;
}
TD {
padding: 5px;
}
TABLE.horizontal TH {
text-align: right;
}
TABLE.horizontal TD,
TABLE.horizontal TH {
padding: 5px;
}
.resource-player {
width: 100%;
border-radius: 10px;
max-width: 640px;
max-height: 640px;
margin: 5px;
border: 1px solid var(--border-color);
}
.dz-clickable {
text-align: center;
}
.scrollable {
max-height: 200px;
overflow: auto;
background-color: #EEE;
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 5px;
}
.admin-tools {
float: right;
padding: 10pt;
}
.disabled {
background-color: #DDD;
}
.dz-image {
width: 240px !important;
}
.dz-progress {
width: 200px !important;
margin-left: -100px !important;
margin-top: 24px !important;
}
-54
View File
@@ -1,54 +0,0 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link>
<title>{% block title %}Polyphonic{% endblock %}</title>
</head>
<body>
<div class="main">
{% block navigation %}
<nav class="navigation">
<div>
<a class="brand" href="/"><i class="fas fa-random smhide"></i> Polyphonic</a>
<span class="mdhide">Virtual Ensemble Manager</span>
</div>
<ul class="nav-buttons">
{% if request.ensemble_id %}
<li class="nav-item">
<a class="nav-link" href="{% url 'ensemble_detail' %}"><i class="fas fa-music" title="Projects"></i> <span class="smhide">My
Projects</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'register' %}"><i class="fas fa-users" title="Ensembles"></i> <span class="smhide">My
Ensembles</span></a>
</li>
{% endif %}
{% if request.is_admin %}
<li class="nav-item">
<a href="{% url 'manage' %}"><i class="fas fa-user-lock" title="Admin"></i></a>
</li>
{% endif %}
</ul>
</nav>
{% endblock %}
<div class="content">
{% block content %}
<h1>No content!</h1>
{% endblock %}
</div>
</div>
<!-- late load scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://kit.fontawesome.com/c837098e5b.js" crossorigin="anonymous"></script>
{% block scripts %}
{% endblock %}
</body>
</html>
@@ -1,14 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div>
<h3>{{ title }}</h3>
<form class="vertical" method="POST">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button>Save</button>
</div>
</form>
</div>
{% endblock %}
@@ -1,18 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div style="flex-grow: 1">
<h1>Projects for {{ ensemble.name }}</h1>
<div class="list-group narrow">
{% for project in ensemble.active_projects %}
<a class="" href="{% url 'project_detail' pk=project.id %}">
<h3>{{ project.name }}</h3>
<p><small>Due in {{ project.deadline|timeuntil }}, {{ project.submissions.count }} submissions.</small></p>
</a>
{% endfor %}
</div>
<div style="text-align: right; margin-top: 10px; color: #999;">
<small>{{ ensemble.ensemble_code }}</small>
</div>
</div>
{% endblock %}
@@ -1,31 +0,0 @@
{% extends "base.html" %}
{% block content %}
{% if request.is_admin %}
<div class="admin-tools">
{% block admin %}
{% endblock %}
</div>
{% endif %}
<h1>{{ project.name }}</h1>
{% block page %}
No content
{% endblock %}
<div class="project-links">
<div class="pills" role="tablist">
<a role="tab" href="{% url 'project_detail' pk=project.id %}">Project info</a>
{% for page in project.wiki_pages.all %}
<a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a>
{% endfor %}
<a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a>
<!--a role="tab" href="">Record a submission</a-->
{% if request.is_admin %}
<a role="tab" href="{% url 'submission_list' project=project.id %}">Submissions</a>
{% endif %}
<a role="tab" href="{% url 'submission_create' project=project.id %}">Send a file</a>
</div>
</div>
{% endblock %}
@@ -1,29 +0,0 @@
{% extends "interface/project_base.html" %}
{% load md2 %}
{% block page %}
<div class="narrow">
<h3 class="text-center">Due in {{ project.deadline|timeuntil }}!</h3>
<p>{{ project.description|markdown }}</p>
{% if project.owner %}
<p>Project email: <a href="mailto:{{ project.owner }}">{{ project.owner }}</a></p>
{% endif %}
{% with sub_count=project.submissions.count %}
<p>There have been {{ sub_count }} submission{{ sub_count|pluralize }} so far...</p>
{% if sub_count %}
<h4>Recent submissions</h4>
<table style="width: 100%">
<tbody">
{% for submission in project.submissions|slice:":5" %}
<tr>
<td>{{ submission.date|timesince }} ago</td>
<td>{{ submission.name }} ({{ submission.instrument }})</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endwith %}
</div>
{% endblock %}
@@ -1,15 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">
<h3>{{ title }}</h3>
<p>{{ instructions }}</p>
<form class="vertical" method="POST">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button>Save</button>
</div>
</form>
</div>
{% endblock %}
@@ -1,11 +0,0 @@
ALL = {{ targets|join:" " }}
-include "local.mk"
all: ${ALL}
{% for s in submissions %}
{{ s.name }}:
curl -o $@ -L {{ s.url }}
{% endfor %}
@@ -1,29 +0,0 @@
{% extends "base.html" %}
{% block content %}
{% if not request.user.is_authenticated %}
<a href="{% url 'login' %}" style="float: right"><i class="fa fa-key"></i></a>
{% endif %}
<div class="collapse">
{% if current %}
<div>
<h3>My Ensembles</h3>
<ul>
{% for ensemble in current %}
<li><a href="/?code={{ ensemble.ensemble_code}}">{{ ensemble.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
<div>
<form action="" class="vertical" method="POST">
<h3>Join an ensemble</h3>
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button class="btn btn-primary">Enter</button>
</div>
</form>
</div>
</div>
{% endblock %}
@@ -1,47 +0,0 @@
{% extends "interface/project_base.html" %}
{% load md2 %}
{% block admin %}
<a href="{% url 'resource_create' project=project.pk %}"><i class="fas fa-plus-circle"></i> Add new</a>
{% endblock %}
{% block page %}
<div class="narrow">
<h3>Resources</h3>
<div class="list-group narrow">
{% for resource in object_list %}
{% with download=resource.presigned_url %}
<div>
{% if request.is_admin %}
<div class="admin-tools">
<a href="{% url 'resource_upload' project=project.pk pk=resource.pk %}">
<i class="fas fa-upload"></i>
</a>
<a href="{% url 'resource_edit' project=project.pk pk=resource.pk %}">
<i class="fas fa-edit"></i>
</a>
</div>
{% endif %}
<h3>
{{ resource.name }}
{% if download %}
<small><a href="{{ download }}" target="_blank" rel="noopener noreferrer">
<i class="fas fa-download"></i> Download
</a></small>
{% endif %}
</h3>
<p>
<small>{{ resource.description|markdown }}</small>
{% if not resource.visible %}
<br/>(This resource is hidden from participants)
{% endif %}
</p>
{% if download and resource.media_type == 'audio' %}
<audio class="resource-player" controls src="{{ download }}"></audio>
{% endif %}
</div>
{% endwith %}
{% endfor %}
</div>
</div>
{% endblock %}
@@ -1,24 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">
<h3>Excellent, you are ready to make a submission!</h3>
<p>
Please enter some basic information so we can identify your submission and
note anything that might be relevant.<br/>
Most people will want to upload their
file directly but if you have your own cloud storage provider you can send a
public link to your submission instead.
</p>
</div>
<div>
<form class="vertical" action="" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<a href="{% url 'project_detail' project.pk %}">Cancel</a>
<button type="submit" class="btn-primary">Continue</button>
</div>
</form>
</div>
{% endblock %}
@@ -1,17 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">
<h3>Thankyou for your submission!</h3>
<table class="horizontal">
<tbody>
<tr><th>From:</th><td>{{ submission.name }}</td></tr>
<tr><th>Instrument:</th><td>{{ submission.instrument }}</td></tr>
<tr><th>Notes:</th><td>{{ submission.notes }}</td></tr>
{% if can_download %}
<tr><th>Download:</th><td><a href="{{ submission.download_url }}" target="_blank" rel="noopener noreferrer">{{ submission.download_name }}</a></td></tr>
{% endif %}
</tbody>
</table>
</div>
{% endblock %}
@@ -1,18 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="narrow">
<h3>Link to cloud storage</h3>
<p>Please paste the full link from your storage provider</p>
</div>
<div>
<form class="vertical" action="" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<a href="{{ cancel_url }}">Cancel</a>
<button type="submit" class="btn-primary">Continue</button>
</div>
</form>
</div>
{% endblock %}
@@ -1,36 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="admin-tools">
<a href="{{ signed_url }}">
<i class="fas fa-list"></i>
</a>
</div>
<table style="max-width: 800px; margin: 10pt auto;">
<thead>
<tr>
<th>Date</th><th>Time</th><th>Name</th><th>Instrument</th><th></th></tr>
</tr>
</thead>
<tbody>
{% for submission in object_list %}
<tr>
<td>{{ submission.date.date }}</td>
<td>{{ submission.date.time }}</td>
<td>{{ submission.name }}</td>
<td>{{ submission.instrument }}</td>
<td>
<a href="{% url 'submission_detail' project=project.pk pk=submission.pk %}"><i class="fas fa-info-circle" title="Info"></i></a>
{% if submission.private %}
<i style="color: #999" class="fas fa-video" title="No preview available"></i>
{% else %}
<a href="{% url 'submission_preview' project=project.pk pk=submission.pk %}"><i class="fas fa-video" title="Preview"></i></a>
{% endif %}
<a href="{{ submission.download_url }}" target="_blank" rel="noopener noreferrer"><i class="fas fa-save" title="Download"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
@@ -1,17 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="text-center">
<div style="text-align: right">
<a href="{% url 'submission_list' project=project.pk %}"><i class="fas fa-arrow-left"></i> Back</a>
</div>
{% with object.download_url as url %}
<video class="resource-player" src="{{ url }}" controls></video>
<p style="text-align: right">
<b>{{ object.name }}</b> ({{ object.instrument }})
<small>{{ object.date }}</small>
<a href="{{ url }}" target="_blank" rel="noopener noreferrer" download><i class="fas fa-save"></i> Download</a>
</p>
{% endwith %}
</div>
{% endblock %}
-11
View File
@@ -1,11 +0,0 @@
{% extends "interface/project_base.html" %}
{% block admin %}
<a href="{% url 'wiki_edit' project=project.pk pk=object.pk %}" class="admin-tool"><i class="fas fa-edit"></i></a>
{% endblock %}
{% block page %}
<div class="wiki-page">
{{ wiki_html|safe }}
</div>
{% endblock %}
@@ -1,23 +0,0 @@
{% extends "interface/project_base.html" %}
{% block page %}
<style>
TEXTAREA {
height: 200px;
}
FORM.vertical {
max-width: 90%;
}
</style>
<div>
<h3>{{ title }}</h3>
<p>{{ instructions }}</p>
<form class="vertical" method="POST">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button>Save</button>
</div>
</form>
</div>
{% endblock %}
@@ -1,17 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="narrow">
<p style="text-align: center">
Login is only required to administer a project.<br/>
If you have an ensemble code <a href="{% url 'register' %}">enter it here</a> instead.
</p>
<form method="POST" class="vertical">
{% csrf_token %}
{{ form }}
<div class="form-actions">
<button type="submit">Login</button>
</div>
</form>
</div>
{% endblock %}
-63
View File
@@ -1,63 +0,0 @@
from django.test import TestCase, Client
from interface import models
class RegisterTestCase(TestCase):
def setUp(self):
self.client = Client()
@staticmethod
def setUpTestData():
e1 = models.Ensemble.objects.create(name='The Be Sharps', code="1234", passphrase='Homer')
e1.projects.create(name='Baby on Board')
e2 = models.Ensemble.objects.create(name='Lisa and the Bleeding Gums', code="2345", passphrase="Maggie")
e2.projects.create(name='Baker St')
def test_redirect(self):
response = self.client.get('/')
self.assertRedirects(response, '/register?')
def test_redirect_project(self):
response = self.client.get('/projects/1')
self.assertRedirects(response, '/register?')
def test_redirect_with_code(self):
response = self.client.get('/?code=123-456-789')
self.assertRedirects(response, '/register?code=123-456-789')
def test_register(self):
response = self.client.post('/register', {'code': '123-456-789', })
self.assertFormError(response, 'form', 'passphrase', 'This field is required.')
response = self.client.post('/register', {'code': '123-456-789', 'passphrase': 'Foo'})
self.assertFormError(response, 'form', None, 'Incorrect code or passphrase')
response = self.client.post('/register', {'code': '12-34', 'passphrase': 'Homer'})
self.assertRedirects(response, '/')
response = self.client.get(response.url)
self.assertEqual(response.context['object'].pk, 1)
# revisting original url get redirected back to homepage
response = self.client.get('/?code=12-34')
response = self.client.get(response.url)
response = self.client.get(response.url)
self.assertEqual(response.context['object'].pk, 1)
# providing a new code
response = self.client.get('/?code=23-45')
self.assertRedirects(response, '/register?code=23-45')
response = self.client.get(response.url)
self.assertQuerysetEqual(response.context['current'], ['<Ensemble: The Be Sharps>'])
#self.assertEqual(response.context['form'].code.initial, 'foo')
response = self.client.post('/register', {'code': '23-45', 'passphrase': 'maggie'})
self.assertRedirects(response, '/')
response = self.client.get('/')
self.assertEqual(response.context['object'].pk, 2)
# can use previous link to switch back without passphrase
response = self.client.get('/?code=12-34')
response = self.client.get(response.url)
response = self.client.get(response.url)
self.assertEqual(response.context['object'].pk, 1)
-53
View File
@@ -1,53 +0,0 @@
from django.test import TestCase, Client
from interface import models
class SubmissionTestCase(TestCase):
@staticmethod
def setUpTestData():
e1 = models.Ensemble.objects.create(name='The Be Sharps', code="1234", passphrase='Homer')
e1.projects.create(name='Baby on Board')
e2 = models.Ensemble.objects.create(name='Lisa and the Bleeding Gums', code="2345", passphrase="Maggie")
e2.projects.create(name='Baker St')
def setUp(self):
self.client = Client()
def test_submission_upload(self):
response = self.client.post('/register', {'code': '12-34', 'passphrase': 'Homer'})
self.assertRedirects(response, '/')
response = self.client.post(f"/projects/1/submission", {'name': 'Ned', 'instrument': 'Harp', 'method': 'upload'})
self.assertRedirects(response, '/projects/1/submission/1/upload')
response = self.client.get(response.url)
upload = response.context['upload']
self.assertEqual(upload['url'], f"http://localhost:9000/{models.BUCKET}")
self.assertRegex(upload['fields']['key'], r'^baby-on-board\/submissions\/[0-9T\-]+_ned_harp_\$\{filename\}$')
self.assertEqual(upload['fields']['success_action_redirect'], 'http://testserver/projects/1/submission/1/complete')
self.assertEqual(models.Submission.objects.count(), 1)
self.assertRedirects(self.client.get(f"/projects/1/submission/1/cancel"), '/projects/1')
self.assertEqual(models.Submission.objects.count(), 0)
def test_submission_link(self):
response = self.client.post('/register', {'code': '12-34', 'passphrase': 'Homer'})
self.assertRedirects(response, '/')
response = self.client.post(f"/projects/1/submission", {'name': 'Ned', 'instrument': 'Harp', 'method': 'link'})
self.assertRedirects(response, '/projects/1/submission/1/link')
url = 'https://drive.google.com/a/path/to/a/video.mp4#g6e6e4a23'
response = self.client.post(f"/projects/1/submission/1/link", {'url': url})
self.assertRedirects(response, '/projects/1/submission/1')
response = self.client.get('/projects/1/submission/1')
self.assertContains(response, "Thankyou for your submission")
response = self.client.get('/projects/1')
self.assertContains(response, 'Ned')
s = models.Submission.objects.get(pk=1)
self.assertEqual(s.download_url, url)
-35
View File
@@ -1,35 +0,0 @@
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
urlpatterns = [
path('login', auth_views.LoginView.as_view(), name='login'),
path('logout', views.logout, name='logout'),
path('register', views.register, name="register"),
path('manage', views.ManageView.as_view(), name="manage"),
path('', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
path('projects/<int:pk>', views.ProjectDetailView.as_view(), name="project_detail"),
path('projects/<int:pk>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"),
path('projects/<int:project>/page/<int:pk>', views.WikiView.as_view(), name="wiki"),
path('projects/<int:project>/page/<int:pk>/edit', views.WikiEditView.as_view(), name="wiki_edit"),
path('projects/<int:project>/submission', views.SubmissionCreateView.as_view(), name="submission_create"),
path('projects/<int:project>/submission/<int:pk>', views.SubmissionDetailView.as_view(), name="submission_detail"),
path('projects/<int:project>/submission/<int:pk>/preview', views.SubmissionPreview.as_view(), name="submission_preview"),
path('projects/<int:project>/submission/<int:pk>/link', views.SubmissionLinkView.as_view(), name="submission_link"),
path('projects/<int:project>/submission/<int:pk>/upload', views.SubmissionUploadView.as_view(), name="submission_upload"),
path('projects/<int:project>/submission/<int:pk>/cancel', views.SubmissionCancelView.as_view(), name="submission_cancel"),
path('projects/<int:project>/submission/<int:pk>/complete', views.SubmissionCompleteView.as_view(), name="submission_complete"),
path('projects/<int:project>/submission/<int:pk>/download', views.SubmissionDownloadView.as_view(), name="submission_download"),
path('projects/<int:project>/submissions', views.SubmissionListView.as_view(), name="submission_list"),
path('projects/<int:project>/resources', views.ResourceListView.as_view(), name="resource_list"),
path('projects/<int:project>/resources/add', views.ResourceCreateView.as_view(), name="resource_create"),
path('projects/<int:project>/resources/<int:pk>', views.ResourceUploadView.as_view(), name="resource_upload"),
path('projects/<int:project>/resources/<int:pk>/edit', views.ResourceEditView.as_view(), name="resource_edit"),
path('projects/<int:project>/resources/<int:pk>/complete', views.ResourceCompleteView.as_view(), name="resource_complete"),
]
-427
View File
@@ -1,427 +0,0 @@
from django.shortcuts import render, get_object_or_404, redirect, resolve_url
from django.views.generic import TemplateView, View, RedirectView
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView, FormView
from django.views.generic.base import ContextMixin
from django.http import HttpResponseRedirect
from django.core.exceptions import SuspiciousOperation
from django.core.signing import Signer
from django.contrib import auth
from markdown2 import markdown
from datetime import datetime
from urllib.parse import urlparse, urlencode
import os.path
from . import models, forms
from base64 import b64decode
import logging
logger = logging.getLogger(__name__)
signer = Signer()
def signed_url(name, **kwargs):
url = resolve_url(name, **kwargs)
sig = signer.sign(url)
return sig.replace(":", "?auth=")
class EnsembleMixin(object):
admin_required = False
def dispatch(self, request, *args, **kwargs):
request.ensemble_id = request.session.get('ensemble')
request.is_admin = request.user.is_superuser
if 'auth' in request.GET:
sig = signer.sign(request.path)
if sig[len(request.path)+1:] == request.GET['auth']:
logger.info("Allowing auth key")
request.is_admin = True
return super().dispatch(request, *args, **kwargs)
else:
raise SuspiciousOperation("Bad auth code")
if not request.ensemble_id:
return redirect('register')
if not request.is_admin and request.user.is_authenticated:
try:
request.user.ensembles.get(pk=request.ensemble_id)
request.is_admin = True
except models.Ensemble.DoesNotExist:
pass
if self.admin_required and not request.is_admin:
return redirect('login')
return super().dispatch(request, *args, **kwargs)
class ProjectMixin(EnsembleMixin):
def get_project(self):
if not hasattr(self, '_project'):
if self.request.is_admin: # can access any ensemble
self._project = get_object_or_404(models.Project, pk=self.kwargs['project'])
else:
self._project = get_object_or_404(models.Project,
pk=self.kwargs['project'], ensemble=self.request.ensemble_id)
return self._project
def get_queryset(self):
return super().get_queryset().filter(project=self.get_project())
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['project'] = self.get_project()
return context
class S3UploadMixin(ProjectMixin):
accept_files = ''
def get_accept_files(self):
return self.accept_files
def get_cancel_url(self):
return self.cancel_url
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
success_url = self.request.build_absolute_uri(self.get_success_url())
key_template = self.object.key_template()
project = self.get_project()
context['upload'] = project.presigned_post(key_template,
fields={'success_action_redirect': success_url},
conditions=[["starts-with", "$success_action_redirect", ""]])
context['ajax_upload'] = project.presigned_post(key_template)
context['success_url'] = success_url
context['cancel_url'] = self.get_cancel_url()
context['accept_files'] = self.accept_files
return context
class S3CompleteView(SingleObjectMixin, RedirectView):
def complete(self, key):
self.object.key = key
self.object.save()
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if 'key' in request.GET:
self.complete(request.GET['key'])
elif 'location' in request.GET:
uri = urlparse(request.GET['location'])
bucket, key = uri.path[1:].split('/', 1)
if bucket != models.BUCKET:
key = uri.path[1:]
self.complete(key)
else:
raise KeyError("No key or location found")
return super().get(request, *args, **kwargs)
def register(request):
if 'clear' in request.GET:
request.session.clear()
request.ensemble_id = request.session.get('ensemble')
registered = request.session.setdefault('registered', {})
code = request.GET.get('code', '').replace('-', '')
# check if already joined
if code in registered:
request.session['ensemble'] = registered[code]
return redirect('ensemble_detail')
if request.user.is_superuser and code:
request.session['ensemble'] = models.Ensemble.objects.get(code=code).pk
return redirect('ensemble_detail')
if request.method == "POST":
form = forms.CodeForm(request.POST)
if form.is_valid():
data = form.cleaned_data
try:
ensemble = models.Ensemble.objects.get(code=data['code'].replace('-', ''))
if ensemble.passphrase.lower() == data['passphrase'].lower():
request.session['ensemble'] = ensemble.pk
registered[ensemble.code] = ensemble.pk
return redirect('ensemble_detail')
except models.Ensemble.DoesNotExist:
form.add_error(None, "Incorrect code or passphrase")
else:
form = forms.CodeForm(initial=request.GET)
if request.user.is_superuser:
current = models.Ensemble.objects.all()
else:
current = models.Ensemble.objects.filter(pk__in=registered.values())
return render(request, 'interface/register.html', {'form': form, 'current': current})
def on_login(sender, **kwargs):
user = kwargs['user']
request = kwargs['request']
registered = request.session.get('registered', {})
for e in user.ensembles.all():
if not e.code in registered:
registered[e.code] = e.pk
request.session['registered'] = registered
auth.signals.user_logged_in.connect(on_login)
def logout(request):
ensemble = request.session.get('ensemble')
registered = request.session.get('registered', {})
auth.logout(request)
request.session['ensemble'] = ensemble
request.session['registered'] = registered
return redirect('/')
class EnsembleDetailView(EnsembleMixin, DetailView):
def dispatch(self, request, *args, **kwargs):
# capture provided urls
if 'code' in request.GET:
return redirect('/register?code={0}'.format(request.GET['code']))
return super().dispatch(request, *args, **kwargs)
def get_object(self):
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
class ProjectDetailView(EnsembleMixin, DetailView):
def get_queryset(self):
return models.Project.objects.filter(ensemble=self.request.ensemble_id)
class ProjectMakefileView(EnsembleMixin, DetailView):
template_name = 'interface/project_submissions.mk'
content_type = 'text/plain'
def get_queryset(self):
if self.request.is_admin:
return models.Project.objects.all()
return models.Project.objects.filter(ensemble=self.request.ensemble_id)
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data['submissions'] = []
data['targets'] = []
for s in self.object.submissions:
name = s.short_name
data['targets'].append(name)
data['submissions'].append({
'url': self.request.build_absolute_uri(signed_url('submission_download', project=self.kwargs['pk'], pk=s.pk)),
'name': name,
})
return data
class WikiView(ProjectMixin, DetailView):
template_name = 'interface/wiki.html'
model = models.WikiPage
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data['wiki_html'] = markdown(self.object.markdown)
return data
class WikiCreateView(ProjectMixin, CreateView):
admin_required = True
model = models.WikiPage
fields = ['title', 'markdown']
class WikiEditView(ProjectMixin, UpdateView):
admin_required = True
model = models.WikiPage
fields = ['title', 'markdown']
class SubmissionCreateView(ProjectMixin, FormView):
#model = models.Submission
#fields = ['name', 'instrument', 'url', 'notes']
form_class = forms.SubmissionForm
template_name = "interface/submission_create.html"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.project = self.get_project()
self.object.save()
self.request.session['name'] = self.object.name
self.request.session['instrument'] = self.object.instrument
if form.cleaned_data['method'] == 'link':
return redirect('submission_link', project=self.object.project.pk, pk=self.object.pk)
return redirect('submission_upload', project=self.object.project.pk, pk=self.object.pk)
def get_initial(self):
return { k: self.request.session.get(k) for k in ('name', 'instrument') }
class SubmissionCompleteView(ProjectMixin, S3CompleteView):
model = models.Submission
def complete(self, key):
self.object.url = key
self.object.private = False
self.object.complete = True
self.object.save()
def get_redirect_url(self, **kwargs):
return resolve_url('submission_detail', **self.kwargs)
class SubmissionDownloadView(ProjectMixin, SingleObjectMixin, RedirectView):
model = models.Submission
admin_required = True
def get_redirect_url(self, **kwargs):
return self.get_object().download_url
class SubmissionDetailView(ProjectMixin, DetailView):
model = models.Submission
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['can_download'] = self.request.is_admin
return context
class SubmissionPreview(ProjectMixin, DetailView):
model = models.Submission
template_name = 'interface/submission_preview.html'
admin_required = True
class SubmissionUploadView(S3UploadMixin, DetailView):
template_name = 'interface/s3_upload.html'
model = models.Submission
accept_files = "video/*"
def get_success_url(self):
return resolve_url('submission_complete', **self.kwargs)
def get_cancel_url(self):
return resolve_url('submission_cancel', **self.kwargs)
class SubmissionLinkView(ProjectMixin, UpdateView):
model = models.Submission
template_name = 'interface/submission_link.html'
fields = ['url']
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_url'] = self.get_cancel_url()
return context
def get_success_url(self):
return resolve_url('submission_detail', **self.kwargs)
def get_cancel_url(self):
return resolve_url('submission_cancel', **self.kwargs)
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.complete = True
self.object.private = True
self.object.save()
return redirect(self.get_success_url())
class SubmissionCancelView(ProjectMixin, SingleObjectMixin, View):
model = models.Submission
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.delete()
return redirect('project_detail', pk=kwargs['project'])
class SubmissionListView(ProjectMixin, ListView):
model = models.Submission
admin_required = True
def get_queryset(self):
return super().get_queryset().filter(complete=True).order_by('-pk')
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
data['signed_url'] = self.request.build_absolute_uri(signed_url('project_makefile', pk=self.kwargs['project']))
return data
class ResourceCreateView(ProjectMixin, CreateView):
model = models.Resource
fields = ['name', 'media_type', 'description']
template_name = 'interface/project_form.html'
title = "Add a new resource"
admin_required = True
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.project = self.get_project()
self.object.save()
return redirect('resource_upload', project=self.object.project_id, pk=self.object.pk)
class ResourceUploadView(S3UploadMixin, DetailView):
model = models.Resource
template_name = 'interface/s3_upload.html'
def get_accept_files(self):
return self.object.accept()
def get_success_url(self):
return resolve_url('resource_complete', **self.kwargs)
def get_cancel_url(self):
return resolve_url('resource_list', project=self.kwargs['project'])
class ResourceCompleteView(ProjectMixin, S3CompleteView):
model = models.Resource
def get_redirect_url(self, **kwargs):
return resolve_url('resource_list', project=self.kwargs['project'])
class ResourceListView(ProjectMixin, ListView):
model = models.Resource
def get_queryset(self):
qs = super().get_queryset()
if not self.request.is_admin:
qs = qs.filter(visible=True)
return qs
class ResourceEditView(ProjectMixin, UpdateView):
admin_required = True
model = models.Resource
fields = ['name', 'description', 'visible']
template_name = 'interface/default_form.html'
def get_success_url(self):
return resolve_url('resource_list', project=self.kwargs['project'])
class ManageView(EnsembleMixin, TemplateView):
template_name = 'interface/manage.html'
admin_required = True
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['ensemble'] = models.Ensemble.objects.get(pk=self.request.ensemble_id)
context['ensemble_url'] = self.request.build_absolute_uri('/?code={0}'.format(context['ensemble'].ensemble_code()))
return context
@@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'polyphonic.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polyphonic.settings")
application = get_asgi_application()
+142
View File
@@ -0,0 +1,142 @@
"""
Django settings for polyphonic project.
Generated by 'django-admin startproject' using Django 3.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
# A place to put things
WORK_DIR = os.environ.get("WORK_DIR") or os.path.join(BASE_DIR, "data")
# Will fail to start if not defined
SECRET_KEY = os.environ.get("SECRET_KEY")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
# Application definition
POLYPHONIC_MODULES = ["polyphonic.library"]
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_markdown2",
"rest_framework",
"crispy_forms",
"crispy_bulma",
"byostorage",
"polyphonic.interface",
]
INSTALLED_APPS += POLYPHONIC_MODULES
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
CRISPY_TEMPLATE_PACK = "bulma"
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "polyphonic.config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "polyphonic.config.wsgi.application"
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(WORK_DIR, "db.sqlite3"),
}
}
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
LOGIN_REDIRECT_URL = "/"
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
# Localisation (localization?)
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Australia/Melbourne"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = "/static/"
STATIC_ROOT = "static"
# Library settings
CACHED_STORAGE_REMOTE = "byostorage.user.BYOStorage"
CACHED_STORAGE_DIR = os.path.join(WORK_DIR, "cache")
WHOOSH_INDEX = os.path.join(WORK_DIR, "index")
STORAGE_CLASSES = ["polyphonic.library.gdrive.storage.GDriveLinkStorage"]
+28
View File
@@ -0,0 +1,28 @@
from .base import * # noqa
from os import environ
DEBUG = True
SECRET_KEY = "DO NOT USE IN PRODUCTION"
# Enable debug toolbar
INSTALLED_APPS.append("debug_toolbar") # noqa
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa
INTERNAL_IPS = ["127.0.0.1"]
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": environ.get("DEBUG_LEVEL", "WARNING"),
},
},
"loggers": {
"polyphonic": {
"handlers": ["console"],
"level": environ.get("DEBUG_LEVEL", "WARNING"),
}
},
}
+4
View File
@@ -0,0 +1,4 @@
from .base import * # noqa
# Enable WhiteNoise for static files
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware") # noqa
@@ -13,10 +13,20 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('interface.urls')),
path("admin/", admin.site.urls),
path("", include("polyphonic.interface.urls")),
# path('', include('submissions.urls')),
path("", include("polyphonic.library.urls")),
]
try:
import debug_toolbar
urlpatterns.append(path("__debug__", include(debug_toolbar.urls)))
except ImportError:
pass
@@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'polyphonic.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polyphonic.config.settings.base")
application = get_wsgi_application()
+2
View File
@@ -0,0 +1,2 @@
pylint==2.6.0
django-debug-toolbar
+34
View File
@@ -0,0 +1,34 @@
from django.contrib import admin
from . import models
class EnsembleAdmin(admin.ModelAdmin):
list_display = ["name", "slug"]
class ModuleInline(admin.StackedInline):
model = models.Module
extra = 0
class ProjectAdmin(admin.ModelAdmin):
list_display = ["name", "ensemble", "event_date", "active"]
list_filter = ["ensemble", "active"]
inlines = [ModuleInline]
class ResourceAdmin(admin.ModelAdmin):
list_display = ["name", "media_type", "project"]
list_filter = ["project"]
class WikiPageAdmin(admin.ModelAdmin):
list_display = ["title", "project"]
list_filter = ["project"]
admin.site.register(models.Ensemble, EnsembleAdmin)
admin.site.register(models.Project, ProjectAdmin)
admin.site.register(models.Resource, ResourceAdmin)
admin.site.register(models.WikiPage, WikiPageAdmin)
@@ -2,4 +2,4 @@ from django.apps import AppConfig
class InterfaceConfig(AppConfig):
name = 'interface'
name = "polyphonic.interface"
+5
View File
@@ -0,0 +1,5 @@
from crispy_forms.layout import Field
class BulmaFileUpload(Field):
template = "bulma/file_upload.html"
+76
View File
@@ -0,0 +1,76 @@
from django import forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, HTML
from crispy_bulma.layout import FormGroup
from . import models, fields
class BaseForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = self.get_form_helper()
def get_form_helper(self):
helper = FormHelper(self)
# helper.add_input(Submit('submit', 'Submit', css_class='button is-link'))
# helper.layout.subm append(HTML('<a class="button is-light">Cancel</a>'))
# print(helper.layout)
helper.layout.append(
FormGroup(
Submit("submit", "Save", css_class="button is-primary"),
HTML(
'{% if view.cancel_url %}<div class="control"><a href="{{ view.cancel_url }}" class="button is-light">Cancel</a></div>{% endif %}'
),
)
)
return helper
class ProjectForm(forms.ModelForm, BaseForm):
class Meta:
model = models.Project
fields = ["name", "description", "modules", "event_date"]
# widgets = {
# 'event_date': forms.DateTimeInput(attrs={'type': 'date'})
# }
modules = forms.MultipleChoiceField(
choices=[(x, x.title()) for x in models.settings.POLYPHONIC_MODULES],
widget=forms.CheckboxSelectMultiple,
required=False,
)
class ResourceForm(forms.ModelForm, BaseForm):
class Meta:
model = models.Resource
fields = ["name", "media_type", "description", "file"]
def get_form_helper(self):
helper = super().get_form_helper()
helper[3].wrap(fields.BulmaFileUpload)
return helper
class WikiForm(forms.ModelForm, BaseForm):
class Meta:
model = models.WikiPage
fields = ["title", "markdown"]
class CodeForm(BaseForm):
code = forms.CharField(
max_length=14,
widget=forms.TextInput(
attrs={"placeholder": "xxx-xxx-xxx", "inputmode": "numeric"}
),
)
passphrase = forms.CharField(max_length=32)
class ResourceUploadForm(forms.Form):
pass
# file = S3UploadField()
@@ -0,0 +1,80 @@
# Generated by Django 3.2.7 on 2022-11-19 01:24
import byostorage.user
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import polyphonic.interface.models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('byostorage', '0006_alter_userstorage_settings_data'),
]
operations = [
migrations.CreateModel(
name='Ensemble',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Display name', max_length=100)),
('slug', models.SlugField(editable=False, help_text='Short name for the ensemble - used for folders', max_length=100, unique=True)),
('code', models.CharField(default=polyphonic.interface.models.generate_code, help_text='Ensemble registration code', max_length=9)),
('passphrase', models.CharField(help_text='Used to register ensembles', max_length=100)),
('details', models.TextField(blank=True, help_text='Description of the ensemble (markdown)')),
('admins', models.ManyToManyField(related_name='ensembles', to=settings.AUTH_USER_MODEL)),
('storage', models.ForeignKey(help_text='Default storage for this ensemble', null=True, on_delete=django.db.models.deletion.SET_NULL, to='byostorage.userstorage')),
],
),
migrations.CreateModel(
name='Project',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField(blank=True, help_text='Markdown format')),
('active', models.BooleanField(default=True)),
('event_date', models.DateTimeField(blank=True, null=True)),
('owner', models.CharField(blank=True, max_length=255)),
('ensemble', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='interface.ensemble')),
],
options={
'ordering': ['active', '-pk'],
},
),
migrations.CreateModel(
name='WikiPage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('markdown', models.TextField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wiki_pages', to='interface.project')),
],
),
migrations.CreateModel(
name='Resource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField(blank=True)),
('file', models.FileField(storage=byostorage.user.BYOStorage(), upload_to=polyphonic.interface.models.resource_key)),
('media_type', models.CharField(choices=[('audio', 'Audio'), ('video', 'Video'), ('general', 'General')], default='*', max_length=10)),
('visible', models.BooleanField(default=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resources', to='interface.project')),
],
options={
'ordering': ['-visible', '-pk'],
},
),
migrations.CreateModel(
name='Module',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.SlugField(choices=[('library', 'Library')], max_length=20)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='interface.project')),
],
),
]
@@ -0,0 +1,25 @@
# Generated by Django 3.2.7 on 2023-02-01 21:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('interface', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='project',
options={'ordering': ['active', 'event_date']},
),
migrations.RemoveField(
model_name='ensemble',
name='code',
),
migrations.RemoveField(
model_name='ensemble',
name='passphrase',
),
]
@@ -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'),
),
]
@@ -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',
),
]
+250
View File
@@ -0,0 +1,250 @@
from django.db import models
from django.utils.text import slugify
from django.utils import timezone
from django.conf import settings
from django.shortcuts import resolve_url
from byostorage.user import BYOStorage
import random
from .utils import sign_data
MEDIA_TYPES = [
("audio", "Audio"),
("video", "Video"),
("general", "General"),
]
def rough_date(d):
if not d:
return False, "sometime..."
days = (d - timezone.now()).days
in_past = days < 0
if in_past:
days = abs(days)
if days == 0:
m = int((d - timezone.now()).seconds / 60)
if m > 60:
return in_past, "{0:d} hours".format(int(m / 60))
return in_past, "{0:d} minutes!".format(int(m % 60))
if days >= 14:
return in_past, "{0:d} weeks".format(int(days / 7))
if days >= 7:
return in_past, "{0:d} weeks, {1:d} days".format(int(days / 7), int(days % 7))
return in_past, f"{days} days"
def generate_code(length=9):
return "".join([random.choice("0123456789") for _ in range(length)])
class EnsembleQuerySet(models.QuerySet):
def for_user(self, user, ensemble_keys=[], project_keys=[]):
if user.is_superuser:
return self
f = models.Q(slug__in=ensemble_keys) | models.Q(projects__in=project_keys)
if user.is_authenticated:
f |= models.Q(admins=user.pk)
return self.filter(f).distinct()
class Ensemble(models.Model):
"""A group that plays together"""
name = models.CharField(max_length=100, help_text="Display name")
slug = models.SlugField(
max_length=100,
editable=False,
unique=True,
help_text="Short name for the ensemble - used for folders",
)
admins = models.ManyToManyField("auth.User", related_name="ensembles")
details = models.TextField(
blank=True, help_text="Description of the ensemble (markdown)"
)
storage = models.ForeignKey(
"byostorage.UserStorage",
null=True,
on_delete=models.SET_NULL,
help_text="Default storage for this ensemble",
)
nonce = models.SmallIntegerField(
default=1, help_text="Increment this to reset the authentication links"
)
objects = EnsembleQuerySet.as_manager()
class Meta:
ordering = ("slug",)
def active_projects(self):
return self.projects.active().current()
def has_admin(self, user):
if not user.is_authenticated:
return False
if user.is_superuser:
return True
return user.pk in self.admins.values_list("pk", flat=True)
def save(self, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
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):
return self.name
class ProjectQuerySet(models.QuerySet):
def current(self):
return self.filter(
models.Q(event_date__gte=(timezone.now() - timezone.timedelta(7)))
| models.Q(event_date=None)
)
def active(self):
return self.filter(active=True)
def for_user(self, user, project_keys=[], ensemble_keys=[]):
if user.is_superuser:
return self
f = models.Q(pk__in=project_keys) | models.Q(ensemble__slug__in=ensemble_keys)
if user.is_authenticated:
f |= models.Q(ensemble__admins=user.pk)
return self.filter(f)
class Project(models.Model):
"""A Project linked to an ensemble"""
name = models.CharField(max_length=100)
ensemble = models.ForeignKey(
Ensemble, related_name="projects", on_delete=models.CASCADE, null=True
)
description = models.TextField(blank=True, help_text="Markdown format")
active = models.BooleanField(default=True)
event_date = models.DateTimeField(null=True, blank=True)
owner = models.CharField(max_length=255, blank=True)
nonce = models.SmallIntegerField(
default=1, help_text="Increment this to reset the authentication links"
)
objects = ProjectQuerySet.as_manager()
class Meta:
ordering = ["active", "event_date"]
@property
def days(self):
return (self.event_date - timezone.now().date()).days
@property
def has_happened(self):
if not self.event_date:
return False
return self.event_date < timezone.now()
@property
def rough_date(self):
if not self.event_date:
return "No timescale"
in_past, s = rough_date(self.event_date)
if in_past:
return f"{s} ago"
return f"In {s}"
@property
def folder(self):
project = slugify(self.name)
print(f"{self.ensemble.storage_id}:{self.ensemble.slug}/{project}")
return f"{self.ensemble.storage_id}:{self.ensemble.slug}/{project}"
@property
def active_modules(self):
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):
return self.name
class Module(models.Model):
"""Enable modules on a oriject"""
name = models.SlugField(
max_length=20, choices=[(x, x.title()) for x in settings.POLYPHONIC_MODULES]
)
project = models.ForeignKey(
Project, related_name="modules", on_delete=models.CASCADE
)
def __str__(self):
return self.name
def resource_key(resource, filename):
return f"{resource.project.folder}/resources/{filename}"
class Resource(models.Model):
"""A viewable file resource attached to a project
e.g PDF instructions, MP3 backing track
"""
project = models.ForeignKey(
Project, related_name="resources", on_delete=models.CASCADE
)
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
file = models.FileField(storage=BYOStorage(), upload_to=resource_key)
media_type = models.CharField(max_length=10, choices=MEDIA_TYPES, default="*")
visible = models.BooleanField(default=True)
class Meta:
ordering = ["-visible", "-pk"]
def accept(self):
if self.media_type == "general":
return ".*"
return f"{self.media_type}/*"
def __str__(self):
return self.name
class WikiPage(models.Model):
"""An editable wiki page for the project in markdown format"""
project = models.ForeignKey(
Project, related_name="wiki_pages", on_delete=models.CASCADE
)
title = models.CharField(max_length=255)
markdown = models.TextField()
def get_absolute_url(self):
return resolve_url("wiki", project=self.project_id, pk=self.pk)
def __str__(self):
return self.title
Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 426 KiB

@@ -0,0 +1,63 @@
@font-face {
font-family: MartinHand;
src: url('/static/fonts/Martinhand3.ttf');
}
:root {
--primary: #485fc7;
}
.fancy {
font-family: MartinHand;
color: var(--primary);
}
.has-text-shadow {
text-shadow: 1px 1px 2px #000;
}
.is-form-group {
max-width: 600px;
align-self: center;
}
.is-centered {
margin: auto;
}
.is-action {
cursor: pointer;
}
.menu-label {
color: var(--primary);
}
.button.is-primary, .button.is-primary:hover {
background-color: var(--primary);
}
A.admin-link:after {
content: "*";
}
TEXTAREA.input {
height: 400px;
}
.control INPUT[type='file'] {
border: none;
width: 80%;
text-align: center;
margin: auto 10%;
}
.project-footer {
position: fixed;
bottom: 0px;
right: 0px;
background-color: #EEE;
padding: 10px 10px;
margin-top: 30px;
border-top-left-radius: 6px;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

@@ -0,0 +1,29 @@
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = document.getElementById(el.dataset.target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
target.classList.toggle('is-hidden-touch');
});
});
// set all active links
const here = location.toString();
for (const el of document.getElementsByTagName('a')) {
if (el.href == here) {
el.classList.add('is-active');
}
}
});
+8
View File
@@ -0,0 +1,8 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="hero">
<h3 class="is-size-3">Sorry, you do not have permission to do that!</h3>
<p>{{ exception }}</p>
</div>
{% endblock %}
+8
View File
@@ -0,0 +1,8 @@
{% extends "interface/project_base.html" %}
{% block page %}
<div class="hero">
<h3 class="is-size-3">Sorry, that resource is not found.</h3>
<p>{{ exception }}</p>
</div>
{% endblock %}
+58
View File
@@ -0,0 +1,58 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="{% static 'interface/icon.png' %}" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
<link rel="stylesheet" href="{% static 'interface/css/polyphonic.css' %}"></link>
<script src="{% static 'interface/js/interface.js' %}"></script>
<script src="//unpkg.com/alpinejs" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" defer></script>
<script src="//kit.fontawesome.com/c837098e5b.js" crossorigin="anonymous" defer></script>
<title>{% block title %}Polyphonic{% endblock %}</title>
{% block media %}{% endblock %}
<style>{% block style %}{% endblock %}</style>
</head>
<body>
{% block navigation %}
<nav class="navbar" role="navigation">
<div class="navbar-brand has-text-primary">
<a class="navbar-item" href="/">
<span class="icon fancy is-size-2 is-size-4-mobile mx-4"><i class="fas fa-random"></i></span>
<span class="fancy is-size-2 is-size-4-mobile">Polyphonic</span>
</a>
<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">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarMain" class="navbar-menu">
<div class="navbar-end">
<span class="navbar-item is-size-5-touch is-size-4-tablet">{% firstof ensemble project.ensemble %}</span>
</div>
</div>
</nav>
{% endblock %}
{% block content %}
<h1>No content!</h1>
{% endblock %}
<!-- late load scripts -->
{% block scripts %}
{% endblock %}
</body>
</html>
@@ -0,0 +1,28 @@
{% load crispy_forms_field %}
<div class="field">
<div id="div_id_{{ field.name }}" class="file has-name is-fullwidth">
<label class="file-label">
{% crispy_field field 'class' 'file-input'%}
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label">
Choose a file…
</span>
</span>
<span class="file-name"></span>
</label>
</div>
</div>
<script>
const fileInput = document.querySelector('#div_id_{{ field.name }} input[type=file]');
fileInput.onchange = () => {
if (fileInput.files.length > 0) {
const fileName = document.querySelector('#div_id_{{ field.name }} .file-name');
fileName.textContent = fileInput.files[0].name;
}
}
</script>
@@ -0,0 +1,18 @@
{% extends "interface/project_base.html" %}
{% load crispy_forms_tags %}
{% block media %}
{{ form.media }}
{% endblock %}
{% block page %}
<h3 class="subtitle">{% firstof title view.title %}</h3>
<div class="columns is-centered">
<div class="column is-two-thirds">
{% if instructions %}
<p>{{ instructions }}</p>
{% endif %}
{% crispy form %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,57 @@
{% extends "interface/project_base.html" %}
{% load md2 %}
{% block admin %}
<a href="{% url 'project_create' object.slug %}" class="button is-link">
<span class="icon"><i class="fas fa-plus-circle"></i></span>
<span>Add project</span>
</a>
{% if inactive %}
<a href="?" class="button is-link">
<span class="icon"><i class="fas fa-archive"></i></span>
<span>Hide old</span>
</a>
{% else %}
<a href="?inactive" class="button is-link">
<span class="icon"><i class="fas fa-archive"></i></span>
<span>Show all</span>
</a>
{% endif %}
{% endblock %}
{% block page %}
<h3 class="title">Projects for {{ensemble.name }}</h3>
<div class="content">
{{ ensemble.details|markdown }}
</div>
<div class="block">
Contacts:
{% for admin in ensemble.admins.all %}
<a href="mailto:{{ admin.email }}" class="tag">{% firstof admin.get_full_name admin.get_username %}</a>
{% endfor %}
</div>
{% include 'interface/project_items.html' %}
{% if request.is_admin %}
<div class="">
<div class="card">
<header class="card header">
<p class="card-header-title">Admin Details</p>
</header>
<div class="card-content">
<ul>
<li><a href="{{ ensemble_link }}">Ensemble Sharing Link</a></li>
<li><a href="{% url 'project_create' ensemble.pk %}">Add a new project</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
<div>
<a href="{% url 'forget_resource' 'ensemble' ensemble.slug %}">Forget this ensemble</a>
</div>
{% endblock %}
@@ -0,0 +1,48 @@
{% extends "interface/project_base.html" %}
{% load md2 %}
{% block page %}
{% comment %}
<div class="admin-tools is-pulled-right">
<a class="button is-link" href="{% url 'register' %}">
<span class="icon"><i class="fas fa-plus-circle"></i></span>
<span>Register another</span>
</a>
</div>
{% endcomment %}
<h3 class="title">My Ensembles</h3>
<div class="columns is-multiline">
{% for ensemble in object_list %}
<div class="column is-half-tablet is-one-third-widescreen">
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img src="https://www.gravatar.com/avatar/{{ ensemble.email }}?d=mp" alt="Placeholder image">
</figure>
</div>
<div class="media-content" style="min-height: 60px">
<a href="{% url 'ensemble_detail' ensemble.slug %}">
<p class="title is-4">{{ ensemble.name }}</p>
</a>
</div>
</div>
</div>
<div class="card-footer">
{% with projects=ensemble.active_projects.count %}
<a class="card-footer-item" href="{% url 'ensemble_detail' ensemble.slug %}">{{ projects }} active project{{ projects|pluralize }}</a>
{% endwith %}
</div>
</div>
</div>
{% empty %}
<div class="hero">
You don't currently have access to any ensembles - ask your administrator for a link.
</div>
{% endfor %}
</div>
{% endblock %}
@@ -8,6 +8,9 @@
URL: <a href="{{ ensemble_url }}">{{ ensemble_url }}</a><br/>
Passphrase: {{ ensemble.passphrase }}
</p>
<ul>
<li><a href="{% url 'work_list' %}">Library</a></li>
</ul>
<p>
Sorry, not much you can do here yet.
<ul>
@@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block content %}
<div class="columns mx-2">
<div class="column is-narrow is-hidden-touch" id="projectMenu">
<div style="margin: auto 2em;">
<aside class="menu">
<p class="menu-label">My Things</p>
<ul class="menu-list">
<li><a href="{% url 'ensemble_list' %}">Ensembles</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 'collection_list' %}">Collections</a></li>
{% endif %}
</ul>
{% if project %}
<p class="menu-label">This Project</p>
<ul class="menu-list">
<li><a role="tab" href="{% url 'project_detail' project=project.id %}">Project Info</a></li>
{% if 'library' in modules %}
<li><a class="nav-link" href="{% url 'item_list' project=project.pk %}">My Music</a></li>
{% endif %}
{% for page in project.wiki_pages.all %}
<li><a class="nav-link"
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>
</ul>
{% endif %}
{% if collection %}
<p class="menu-label">Collection</p>
<ul class="menu-list">
<li><a role="tab" href="{% url 'collection_work_list' collection.pk %}">Work List</a></li>
</ul>
{% endif %}
<p class="menu-label">Admin</p>
<ul class="menu-list">
{% if request.user.is_staff %}
<li><a href="/admin" target="polyphonic_admin" rel="noopener noreferrer">Django Admin</a></li>
{% endif %}
</ul>
<ul class="menu-list">
{% if request.user.is_authenticated %}
<li><a href="{% url 'logout' %}">Logout</a></li>
{% else %}
<li><a href="{% url 'login' %}">Login</a></li>
{% endif %}
</ul>
</aside>
</div>
</div>
<div class="column">
{% if project %}
<div class="tabs is-centered is-hidden-desktop">
<ul>
<li><a href="">Info</a></li>
{% if project.wiki_pages.count %}
<li><a href="">Pages</a></li>
{% endif %}
{% if project.resources.count %}
<li><a href="{% url 'resource_list' project=project.pk %}">Resources</a></li>
{% endif %}
{% if 'library' in modules %}
<li><a href="{% url 'item_list' project=project.pk %}">My Music</a></li>
{% endif %}
{% if 'submission' in modules %}
<li><a href="{% url 'submission_create' project=project.pk %}">Send File</a></li>
{% endif %}
</ul>
</div>
{% endif %}
<div class="block">
{% if request.is_admin %}
<div class="admin-tools is-pulled-right is-hidden-mobile">
{% block admin %}
{% endblock %}
</div>
{% endif %}
{% if project %}<h3 class="title">{{ project.name }}</h3>{% endif %}
{% block page %}
No content
{% endblock %}
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,84 @@
{% extends "interface/project_base.html" %}
{% load md2 %}
{% load polyphonic %}
{% block admin %}
<a href="{% url 'wiki_create' project=project.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-file"></i></span>
<span>Add Page</span>
</a>
<a href="{% url 'project_edit' project=project.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-edit"></i></span>
<span>Edit</span>
</a>
{% endblock %}
{% block page %}
<div class="columns is-multiline">
<div class="column ">
<div class="box">
{% if project.event_date %}
<h3 class="subtitle is-centered">
<b>{{ project.event_date|date:"l jS F Y, g:i A" }}</b>
{% if project.has_happened %}
({{ project.event_date|roughtimesince }} ago)
{% else %}
(in {{ project.event_date|roughtimeuntil }})
{% endif %}
</h3>
{% endif %}
<div class="block">
{% if project.description %}
<p class="content">{{ project.description|markdown }}</p>
{% else %}
<p>No description</p>
{% endif %}
</div>
{% if project.owner %}
<div class="block">
{% if project.owner.email %}
The project owner is <a href="mailto:{{ project.owner.email }}">{{ project.owner }}</a>
{% else %}
The project owner is {{ project.owner }}.
{% endif %}
</div>
{% endif %}
</div>
</div>
{% if 'library' in modules %}
<div class="column is-one-third">
{% include 'library/project_detail.html' %}
</div>
{% endif %}
{% if 'submission' in modules %}
<div class="column">
{% include 'submissions/project_detail.html' %}
</div>
{% endif %}
</div>
{% if request.is_admin %}
<div class="box">
<h3 class="subtitle">Admin Actions</h3>
<ul>
<li><a href="{{ project_link }}">Project Link</a></li>
{% if 'library' in modules %}
<li><a href="{% url 'item_list_manage' project=project.pk %}">Manage items</a></li>
{% endif %}
</ul>
</div>
{% endif %}
<!--
<div>
<a href="{% url 'forget_resource' 'project' project.pk %}">Forget this project</a>
</div>
-->
{% endblock %}
@@ -0,0 +1,36 @@
{% 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">
{{ project.name }}
</p>
<p class="card-header-icon" style="color: black;">{{ project.rough_date }}</p>
</header>
</a>
<div class="card-content" style="height: 100px; overflow: hidden">
<div class="content">
{{ project.description | markdown }}
</div>
</div>
{% if not ensemble %}
<div class="card-footer">
<a class="card-footer-item" href="{% url 'ensemble_detail' project.ensemble.slug %}">{{ project.ensemble }}</a>
</div>
{% endif %}
</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>
@@ -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 %}
@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<section class="section">
{% if not request.user.is_authenticated %}
<a href="{% url 'login' %}" style="float: right"><i class="fa fa-key"></i></a>
{% endif %}
<div class="columns is-centered">
<div class="box is-half">
<h3 class="title">Join an ensemble</h3>
<form action="" method="POST">
{% csrf_token %}
{{ form | crispy }}
<div class="field is-grouped">
<div class="control">
<button class="button is-link">Register</button>
</div>
</div>
</form>
</div>
</div>
</section>
{% endblock %}
@@ -0,0 +1,59 @@
{% extends "interface/project_base.html" %}
{% load md2 %}
{% block admin %}
<a class="button is-link" href="{% url 'resource_create' project=project.pk %}">
<span class="icon"><i class="fas fa-plus-circle"></i></span>
<span>Add new</span>
</a>
{% endblock %}
{% block page %}
<h3 class="subtitle">Resources</h3>
<div class="columns is-multiline">
{% for resource in object_list %}
{% with download=resource.file.url %}
<div class="column is-half">
<div class="card {% if not object.visible %}disabled{% endif %}">
<div class="card-header">
<div class="card-header-title">
{% if download %}
<a href="{{ download }}">{{ resource.name }}</a>
{% else %}
{{ resource.name }}
{% endif %}
</div>
<div class="card-header-icon">
{% if request.is_admin %}
<a href="{% url 'resource_upload' project=project.pk pk=resource.pk %}" class="icon" title="Upload">
<i class="fas fa-upload"></i>
</a>
<a href="{% url 'resource_edit' project=project.pk pk=resource.pk %}" class="icon" title="Edit">
<i class="fas fa-edit"></i>
</a>
{% endif %}
</div>
</div>
<div class="card-content">
<p>
<small>{{ resource.description|markdown }}</small>
{% if not resource.visible %}
<br/>(This resource is hidden from participants)
{% endif %}
</p>
{% if download and resource.media_type == 'audio' %}
<audio class="resource-player" controls src="{{ download }}" style="width: 100%;"></audio>
{% endif %}
</div>
</div>
</div>
{% endwith %}
{% empty %}
<div class="column">
<p>There are no resources for this project</p>
</div>
{% endfor %}
</div>
{% endblock %}
@@ -0,0 +1,15 @@
{% extends "interface/project_base.html" %}
{% block admin %}
<a href="{% url 'wiki_edit' project=project.pk pk=wikipage.pk %}" class="button is-link">
<span class="icon"><i class="fas fa-edit"></i></span>
<span>Edit</span>
</a>
{% endblock %}
{% block page %}
<h3 class="subtitle">{{ wikipage.title }}</h3>
<div class="box content wiki-page">
{{ wiki_html|safe }}
</div>
{% endblock %}
@@ -0,0 +1,18 @@
{% extends "interface/project_base.html" %}
{% load crispy_forms_tags %}
{% block media %}
{{ form.media }}
{% endblock %}
{% block page %}
<h3 class="subtitle">{% firstof title view.title %}</h3>
<div class="columns is-centered">
<div class="column">
{% if instructions %}
<p>{{ instructions }}</p>
{% endif %}
{% crispy form %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<section class="section">
<div class="columns is-centered">
<div class="box is-half">
<p class="block">
Login is only required to administer a project.<br/>
</p>
<form method="POST" class="vertical">
{% csrf_token %}
{{ form | crispy }}
<div class="field is-grouped">
<div class="control">
<button class="button is-link">Login</button>
<a href="{% url 'home' %}" class="button is-light">Cancel</a>
</div>
</div>
</form>
</div>
</div>
</section>
{% endblock %}
@@ -0,0 +1,11 @@
from django import template
import os.path
register = template.Library()
def basename(value):
return os.path.basename(value)
register.filter("basename", basename)
@@ -0,0 +1,18 @@
from django import template
from django.utils import timesince
register = template.Library()
def roughtimesince(value):
return timesince.timesince(value, depth=1)
register.filter("roughtimesince", roughtimesince)
def roughtimeuntil(value):
return timesince.timeuntil(value, depth=1)
register.filter("roughtimeuntil", roughtimeuntil)
@@ -0,0 +1,11 @@
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def url_update(context, **kwargs):
params = context.request.GET.copy()
for k in kwargs:
params[k] = kwargs[k]
return "?" + params.urlencode()

Some files were not shown because too many files have changed in this diff Show More