Compare commits
119 Commits
lockdown_r
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e69cdeca4 | |||
| e797876c16 | |||
| 1301d19c08 | |||
| ca3effcad1 | |||
| 5468f6d3e7 | |||
| e46d8145a7 | |||
| 3444cdbc59 | |||
| 6e77474d15 | |||
| 72bad08a61 | |||
| b217fbea5e | |||
| cec64ecd2f | |||
| d30005d5b6 | |||
| ef981e06e8 | |||
| 4cef5800bc | |||
| 78441dc142 | |||
| c96a2ad80b | |||
| 7e0c77f260 | |||
| ff17114514 | |||
| 533d6f09e8 | |||
| eeb35ce4f6 | |||
| 947626c2af | |||
| c1f0e48f80 | |||
| ab7d32d46e | |||
| 5116247ae8 | |||
| 93c4926dfd | |||
| 27d1b03c3c | |||
| 4e102c07ac | |||
| b1ea75cec0 | |||
| 7d041e1fd0 | |||
| 504c2ee56b | |||
| cfd6d45189 | |||
| ca62ed693a | |||
| 5e0e165037 | |||
| 4164d56dea | |||
| 75dced77b8 | |||
| b3675e28af | |||
| b86c867bd2 | |||
| 9248692e5d | |||
| 406609262d | |||
| ffb31cc004 | |||
| b1ef2b9dac | |||
| 5c56d40bf8 | |||
| a1341d1edc | |||
| 4d964291b2 | |||
| ee5305ba6c | |||
| 147c84550c | |||
|
|
596445061f | ||
|
|
02858a76c0 | ||
|
|
1bcec919cf | ||
|
|
78789c02ed | ||
|
|
04bf1ab1f3 | ||
|
|
fb700b15d6 | ||
|
|
0bd2596ebc | ||
|
|
1f0a336ed4 | ||
|
|
220163bca2 | ||
|
|
d1328ae1b1 | ||
|
|
69f747d5b5 | ||
|
|
2a6554471d | ||
|
|
6edc612ed6 | ||
|
|
f441940e8c | ||
|
|
b1eaf9c7bc | ||
|
|
a892b0bc41 | ||
|
|
f1757be96e | ||
|
|
95ee92cef6 | ||
|
|
b11cfcd6ea | ||
|
|
6e1b26440f | ||
|
|
4268b66b27 | ||
|
|
f22961ad44 | ||
|
|
caeee16657 | ||
|
|
4344cb978b | ||
|
|
a3d8cf0a21 | ||
|
|
70aa7ae3a3 | ||
|
|
f840ee3d8b | ||
|
|
7d4f959146 | ||
|
|
1cb00ebc0b | ||
|
|
928173976b | ||
|
|
d7892d355d | ||
|
|
dbbfa79f10 | ||
|
|
f49ff0fd0e | ||
|
|
6ec5808275 | ||
|
|
948e9deb54 | ||
|
|
75de40f2bd | ||
|
|
295999eaef | ||
|
|
f9fa9d5e05 | ||
|
|
c720cb773e | ||
|
|
da15eebb0a | ||
|
|
e74043ae67 | ||
|
|
5a201cef85 | ||
|
|
5fb5138383 | ||
|
|
039b440a57 | ||
|
|
7d394e2f34 | ||
|
|
13f466228b | ||
|
|
b85440d25c | ||
|
|
bc9f292a2e | ||
|
|
8a249de51c | ||
|
|
7e47eec4ae | ||
|
|
59deeffefe | ||
|
|
53ec846f98 | ||
|
|
94bba3769a | ||
|
|
2726a8fe04 | ||
|
|
4731d18131 | ||
|
|
c639020ac9 | ||
|
|
7f6875f3c4 | ||
|
|
025e1344f0 | ||
|
|
bbc74a77f9 | ||
|
|
dfe4a925c7 | ||
|
|
18e5893cc2 | ||
|
|
988161b599 | ||
|
|
f7aaa98000 | ||
|
|
8f18b9ab9d | ||
|
|
598ee5ad7e | ||
|
|
7aedc6bd07 | ||
|
|
f50fe11ced | ||
|
|
12beebd486 | ||
|
|
9094cae63f | ||
|
|
1581f56b74 | ||
|
|
35555c3321 | ||
|
|
526ae4f59b | ||
|
|
98fe7b77ce |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
26
.gitignore
vendored
26
.gitignore
vendored
@ -1,9 +1,23 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
db.sqlite3
|
*.sqlite3
|
||||||
|
*.swp
|
||||||
|
credentials.json
|
||||||
credentials
|
credentials
|
||||||
polyphonic/settings.py
|
local_settings.py
|
||||||
env
|
local.mk
|
||||||
test.*
|
.coverage
|
||||||
static
|
.lint
|
||||||
teststore
|
.deploy
|
||||||
|
Session.vim
|
||||||
|
poetry.lock
|
||||||
|
/env
|
||||||
|
/data
|
||||||
|
/old
|
||||||
|
/static
|
||||||
|
/teststore
|
||||||
|
/cache
|
||||||
|
/local_storage
|
||||||
|
/media
|
||||||
|
/index
|
||||||
|
/dist
|
||||||
|
|||||||
25
Dockerfile
Normal file
25
Dockerfile
Normal 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"]
|
||||||
40
Makefile
40
Makefile
@ -1,15 +1,42 @@
|
|||||||
PYTHON=env/bin/python
|
PYTHON=env/bin/python
|
||||||
DROPZONE=5.7.0
|
DROPZONE=5.7.0
|
||||||
|
|
||||||
|
VERSION=0.8.4
|
||||||
|
|
||||||
|
export DJANGO_SETTINGS_MODULE=polyphonic.config.settings.dev
|
||||||
|
|
||||||
|
-include local.mk
|
||||||
|
|
||||||
|
test: .coverage
|
||||||
|
|
||||||
|
check: .lint
|
||||||
|
|
||||||
|
pre-commit: check test
|
||||||
|
|
||||||
|
.coverage: polyphonic
|
||||||
|
poetry run coverage run --include "polyphonic/*" --omit "*/migrations/*" polyphonic/manage.py test polyphonic
|
||||||
|
poetry run coverage html
|
||||||
|
poetry run coverage report
|
||||||
|
|
||||||
|
.lint: polyphonic
|
||||||
|
poetry run ruff check polyphonic
|
||||||
|
poetry run ruff format --check polyphonic
|
||||||
|
touch $@
|
||||||
|
|
||||||
|
|
||||||
|
build: dist/polyphonic-${VERSION}-py3-none-any.whl
|
||||||
|
|
||||||
|
dist/polyphonic-${VERSION}-py3-none-any.whl: polyphonic
|
||||||
|
poetry build
|
||||||
|
|
||||||
dev-setup:
|
dev-setup:
|
||||||
env/bin/pip install -r requirements.txt
|
poetry install --with=dev
|
||||||
env/bin/pip install -r dev-requirements.txt
|
poetry run manage migrate
|
||||||
${PYTHON} manage.py migrate
|
poetry run manage createsuperuser --username admin --email admin@localhost
|
||||||
${PYTHON} manage.py createsuperuser --username admin --email admin@localhost
|
|
||||||
|
|
||||||
upgrade:
|
upgrade:
|
||||||
${PYTHON} manage.py migrate
|
poetry run manage migrate
|
||||||
${PYTHON} manage.py collectstatic
|
poetry run manage collectstatic
|
||||||
${MAKE} libraries
|
${MAKE} libraries
|
||||||
|
|
||||||
libraries: static/dropzone static/fonts/Quicksand_Book.otf
|
libraries: static/dropzone static/fonts/Quicksand_Book.otf
|
||||||
@ -35,4 +62,3 @@ start_s3_storage:
|
|||||||
stop_s3_storage:
|
stop_s3_storage:
|
||||||
kill `cat teststore/pid` | true
|
kill `cat teststore/pid` | true
|
||||||
rm teststore/pid
|
rm teststore/pid
|
||||||
|
|
||||||
|
|||||||
19
README.md
19
README.md
@ -1,13 +1,28 @@
|
|||||||
## Polyphonic
|
## Polyphonic
|
||||||
|
|
||||||
A simple web app for managing video uploads to an S3 bucket for virtual ensembles.
|
A simple web app managing scores, large files and submissions in a manor tailored to musical ensembles.
|
||||||
|
|
||||||
|
No registration required for ensemble participants - just a one time code and passphrase.
|
||||||
|
|
||||||
|
### Library App
|
||||||
|
|
||||||
|
Store all your scores on your own cloud account (Amazon S3, Google Files etc).
|
||||||
|
Tag up the scores so you can generate custom part sets and assign them to
|
||||||
|
projects so people can easily print just their parts.
|
||||||
|
|
||||||
|
### Submissions App
|
||||||
|
|
||||||
|
Accept video/audio submissions direct to your cloud storage. Was developed and
|
||||||
|
used during 2020 lockdown period for virtual choirs/orchestras but could have more uses.
|
||||||
|
|
||||||
### S3 Setup
|
### S3 Setup
|
||||||
|
|
||||||
#### Bucket setup [virtual-orchestra]
|
#### Bucket setup [virtual-orchestra]
|
||||||
|
|
||||||
Default block public access
|
Default block public access
|
||||||
|
|
||||||
Permissions -> CORS
|
Permissions -> CORS
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||||
@ -28,6 +43,7 @@ Permissions -> CORS
|
|||||||
|
|
||||||
User
|
User
|
||||||
Create with programatic access (copy keys) and an inline policy for the bucket.
|
Create with programatic access (copy keys) and an inline policy for the bucket.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Version": "2012-10-17",
|
"Version": "2012-10-17",
|
||||||
@ -52,3 +68,4 @@ Create with programatic access (copy keys) and an inline policy for the bucket.
|
|||||||
```
|
```
|
||||||
|
|
||||||
3.
|
3.
|
||||||
|
|
||||||
|
|||||||
19
TODO.md
Normal file
19
TODO.md
Normal 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 +0,0 @@
|
|||||||
pylint==2.6.0
|
|
||||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal 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
|
||||||
@ -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)
|
|
||||||
@ -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']
|
|
||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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)
|
|
||||||
@ -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)
|
|
||||||
@ -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"),
|
|
||||||
]
|
|
||||||
@ -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
|
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()
|
application = get_asgi_application()
|
||||||
142
polyphonic/config/settings/base.py
Normal file
142
polyphonic/config/settings/base.py
Normal 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
polyphonic/config/settings/dev.py
Normal file
28
polyphonic/config/settings/dev.py
Normal 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
polyphonic/config/settings/docker.py
Normal file
4
polyphonic/config/settings/docker.py
Normal 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
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path('', include('interface.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
|
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()
|
application = get_wsgi_application()
|
||||||
2
polyphonic/dev-requirements.txt
Normal file
2
polyphonic/dev-requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pylint==2.6.0
|
||||||
|
django-debug-toolbar
|
||||||
34
polyphonic/interface/admin.py
Normal file
34
polyphonic/interface/admin.py
Normal 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):
|
class InterfaceConfig(AppConfig):
|
||||||
name = 'interface'
|
name = "polyphonic.interface"
|
||||||
5
polyphonic/interface/fields.py
Normal file
5
polyphonic/interface/fields.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from crispy_forms.layout import Field
|
||||||
|
|
||||||
|
|
||||||
|
class BulmaFileUpload(Field):
|
||||||
|
template = "bulma/file_upload.html"
|
||||||
76
polyphonic/interface/forms.py
Normal file
76
polyphonic/interface/forms.py
Normal 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()
|
||||||
80
polyphonic/interface/migrations/0001_initial.py
Normal file
80
polyphonic/interface/migrations/0001_initial.py
Normal file
@ -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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
25
polyphonic/interface/migrations/0002_auto_20230202_0804.py
Normal file
25
polyphonic/interface/migrations/0002_auto_20230202_0804.py
Normal file
@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
27
polyphonic/interface/migrations/0003_auto_20230209_0910.py
Normal file
27
polyphonic/interface/migrations/0003_auto_20230209_0910.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2023-02-08 22:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('interface', '0002_auto_20230202_0804'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='ensemble',
|
||||||
|
options={'ordering': ('slug',)},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ensemble',
|
||||||
|
name='auth',
|
||||||
|
field=models.SmallIntegerField(default=1, help_text='Increment this to reset the authentication links'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='project',
|
||||||
|
name='auth',
|
||||||
|
field=models.SmallIntegerField(default=1, help_text='Increment this to reset the authentication links'),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
polyphonic/interface/migrations/0004_auto_20230210_0938.py
Normal file
23
polyphonic/interface/migrations/0004_auto_20230210_0938.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2023-02-09 22:38
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('interface', '0003_auto_20230209_0910'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='ensemble',
|
||||||
|
old_name='auth',
|
||||||
|
new_name='nonce',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='project',
|
||||||
|
old_name='auth',
|
||||||
|
new_name='nonce',
|
||||||
|
),
|
||||||
|
]
|
||||||
0
polyphonic/interface/migrations/__init__.py
Normal file
0
polyphonic/interface/migrations/__init__.py
Normal file
250
polyphonic/interface/models.py
Normal file
250
polyphonic/interface/models.py
Normal 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
|
||||||
BIN
polyphonic/interface/static/fonts/Martinhand3.ttf
Normal file
BIN
polyphonic/interface/static/fonts/Martinhand3.ttf
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 426 KiB After Width: | Height: | Size: 426 KiB |
63
polyphonic/interface/static/interface/css/polyphonic.css
Normal file
63
polyphonic/interface/static/interface/css/polyphonic.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
BIN
polyphonic/interface/static/interface/icon.png
Normal file
BIN
polyphonic/interface/static/interface/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 258 B |
29
polyphonic/interface/static/interface/js/interface.js
Normal file
29
polyphonic/interface/static/interface/js/interface.js
Normal file
@ -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
polyphonic/interface/templates/403.html
Normal file
8
polyphonic/interface/templates/403.html
Normal 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
polyphonic/interface/templates/404.html
Normal file
8
polyphonic/interface/templates/404.html
Normal 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 %}
|
||||||
59
polyphonic/interface/templates/base.html
Normal file
59
polyphonic/interface/templates/base.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{% 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 -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
|
||||||
|
<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 mx-4"><span class="material-symbols-outlined is-size-1 is-size-3-mobile">groups</span></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>
|
||||||
28
polyphonic/interface/templates/bulma/file_upload.html
Normal file
28
polyphonic/interface/templates/bulma/file_upload.html
Normal file
@ -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">
|
||||||
|
<span class="material-symbols-outlined">file_upload</span>
|
||||||
|
</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>
|
||||||
18
polyphonic/interface/templates/interface/default_form.html
Normal file
18
polyphonic/interface/templates/interface/default_form.html
Normal file
@ -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,58 @@
|
|||||||
|
{% extends "interface/project_base.html" %}
|
||||||
|
{% load md2 %}
|
||||||
|
{% load polyphonic %}
|
||||||
|
|
||||||
|
{% block admin %}
|
||||||
|
<a href="{% url 'project_create' object.slug %}" class="button is-link">
|
||||||
|
{{ "add_notes"|icon }}
|
||||||
|
<span>Add project</span>
|
||||||
|
</a>
|
||||||
|
{% if inactive %}
|
||||||
|
<a href="?" class="button is-link">
|
||||||
|
{{ "preview_off"|icon }}
|
||||||
|
<span>Hide old</span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="?inactive" class="button is-link">
|
||||||
|
{{ "preview"|icon }}
|
||||||
|
<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 %}
|
||||||
51
polyphonic/interface/templates/interface/ensemble_list.html
Normal file
51
polyphonic/interface/templates/interface/ensemble_list.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{% 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' %}">
|
||||||
|
{% icon "add_file" %}
|
||||||
|
<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: 100px">
|
||||||
|
<a href="{% url 'ensemble_detail' ensemble.slug %}">
|
||||||
|
<p class="title is-4">{{ ensemble.name }}</p>
|
||||||
|
</a>
|
||||||
|
<div class="mt-3">
|
||||||
|
{{ ensemble.details|markdown }}
|
||||||
|
</div>
|
||||||
|
</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/>
|
URL: <a href="{{ ensemble_url }}">{{ ensemble_url }}</a><br/>
|
||||||
Passphrase: {{ ensemble.passphrase }}
|
Passphrase: {{ ensemble.passphrase }}
|
||||||
</p>
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{% url 'work_list' %}">Library</a></li>
|
||||||
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
Sorry, not much you can do here yet.
|
Sorry, not much you can do here yet.
|
||||||
<ul>
|
<ul>
|
||||||
99
polyphonic/interface/templates/interface/project_base.html
Normal file
99
polyphonic/interface/templates/interface/project_base.html
Normal file
@ -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 %}
|
||||||
84
polyphonic/interface/templates/interface/project_detail.html
Normal file
84
polyphonic/interface/templates/interface/project_detail.html
Normal file
@ -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">
|
||||||
|
{{ "add_notes"|icon }}
|
||||||
|
<span>Add Page</span>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'project_edit' project=project.pk %}" class="button is-link">
|
||||||
|
{{ "edit"|icon }}
|
||||||
|
<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 %}
|
||||||
36
polyphonic/interface/templates/interface/project_items.html
Normal file
36
polyphonic/interface/templates/interface/project_items.html
Normal file
@ -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 %}
|
||||||
24
polyphonic/interface/templates/interface/register.html
Normal file
24
polyphonic/interface/templates/interface/register.html
Normal file
@ -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 %}
|
||||||
60
polyphonic/interface/templates/interface/resource_list.html
Normal file
60
polyphonic/interface/templates/interface/resource_list.html
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{% extends "interface/project_base.html" %}
|
||||||
|
{% load md2 %}
|
||||||
|
{% load polyphonic %}
|
||||||
|
|
||||||
|
{% block admin %}
|
||||||
|
<a class="button is-link" href="{% url 'resource_create' project=project.pk %}">
|
||||||
|
{% icon "add_notes" %}
|
||||||
|
<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 %}" title="Upload">
|
||||||
|
{% icon "upload_file" %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'resource_edit' project=project.pk pk=resource.pk %}" title="Edit">
|
||||||
|
{% icon "edit" %}
|
||||||
|
</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 %}
|
||||||
16
polyphonic/interface/templates/interface/wiki.html
Normal file
16
polyphonic/interface/templates/interface/wiki.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "interface/project_base.html" %}
|
||||||
|
{% load polyphonic %}
|
||||||
|
|
||||||
|
{% block admin %}
|
||||||
|
<a href="{% url 'wiki_edit' project=project.pk pk=wikipage.pk %}" class="button is-link">
|
||||||
|
{{ "edit"|icon }}
|
||||||
|
<span>Edit</span>
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<h3 class="subtitle">{{ wikipage.title }}</h3>
|
||||||
|
<div class="box content wiki-page">
|
||||||
|
{{ wiki_html|safe }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
18
polyphonic/interface/templates/interface/wikipage_form.html
Normal file
18
polyphonic/interface/templates/interface/wikipage_form.html
Normal file
@ -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 %}
|
||||||
24
polyphonic/interface/templates/registration/login.html
Normal file
24
polyphonic/interface/templates/registration/login.html
Normal file
@ -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 %}
|
||||||
11
polyphonic/interface/templatetags/path_filters.py
Normal file
11
polyphonic/interface/templatetags/path_filters.py
Normal file
@ -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)
|
||||||
36
polyphonic/interface/templatetags/polyphonic.py
Normal file
36
polyphonic/interface/templatetags/polyphonic.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from django import template
|
||||||
|
from django.utils import timesince
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter("icon", is_safe=True)
|
||||||
|
def material_icon(value):
|
||||||
|
return f'<span class="icon"><span class="material-symbols-outlined">{value}</span></span>'
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def icon(name, element="span", classes=[]):
|
||||||
|
classes = ["icon"] + classes
|
||||||
|
return format_html(
|
||||||
|
'<{} class="{}"><span class="material-symbols-outlined">{}</span></{}>',
|
||||||
|
element,
|
||||||
|
" ".join(classes),
|
||||||
|
name,
|
||||||
|
element,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
11
polyphonic/interface/templatetags/url_tools.py
Normal file
11
polyphonic/interface/templatetags/url_tools.py
Normal file
@ -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
Loading…
x
Reference in New Issue
Block a user