Refactored into /app
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
class EnsembleAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'ensemble_code', '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)
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class InterfaceConfig(AppConfig):
|
||||
name = 'interface'
|
||||
@@ -0,0 +1,4 @@
|
||||
from crispy_forms.layout import Field
|
||||
|
||||
class BulmaFileUpload(Field):
|
||||
template = 'bulma/file_upload.html'
|
||||
@@ -0,0 +1,46 @@
|
||||
from django import forms
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Submit
|
||||
|
||||
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'))
|
||||
return helper
|
||||
|
||||
|
||||
class ProjectForm(forms.ModelForm, BaseForm):
|
||||
|
||||
class Meta:
|
||||
model = models.Project
|
||||
fields = ['name', 'description', 'event_date']
|
||||
widgets = {
|
||||
'event_date': forms.DateTimeInput(attrs={'type': 'date'})
|
||||
}
|
||||
|
||||
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 CodeForm(BaseForm):
|
||||
code = forms.CharField(max_length=14,
|
||||
widget=forms.TextInput(attrs={'placeholder': 'xxx-xxx-xxx', 'inputmode': 'numeric'}))
|
||||
passphrase = forms.CharField(max_length=32)
|
||||
|
||||
class ResourceUploadForm(forms.Form):
|
||||
pass
|
||||
# file = S3UploadField()
|
||||
@@ -0,0 +1,80 @@
|
||||
# Generated by Django 3.2.7 on 2022-11-19 01:24
|
||||
|
||||
import byostorage.user
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import 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=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=interface.models.resource_key)),
|
||||
('media_type', models.CharField(choices=[('audio', 'Audio'), ('video', 'Video'), ('general', 'General')], default='*', max_length=10)),
|
||||
('visible', models.BooleanField(default=True)),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resources', to='interface.project')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-visible', '-pk'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Module',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.SlugField(choices=[('library', 'Library')], max_length=20)),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='interface.project')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,157 @@
|
||||
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 datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import os.path
|
||||
|
||||
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:
|
||||
return "today!"
|
||||
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 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")
|
||||
code = models.CharField(max_length=9, default=generate_code,
|
||||
help_text="Ensemble registration code")
|
||||
passphrase = models.CharField(max_length=100,
|
||||
help_text="Used to register ensembles")
|
||||
admins = models.ManyToManyField('auth.User', related_name='ensembles')
|
||||
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")
|
||||
|
||||
def active_projects(self):
|
||||
return self.projects.filter(active=True).order_by('event_date')
|
||||
|
||||
def ensemble_code(self):
|
||||
code = str(self.code)
|
||||
return "{}-{}-{}".format(code[:3], code[3:6], code[6:])
|
||||
|
||||
def save(self, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super(Ensemble, self).save(**kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
ordering = ['active', '-pk']
|
||||
|
||||
@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):
|
||||
in_past, s = rough_date(self.event_date)
|
||||
if in_past:
|
||||
return f"{s} ago"
|
||||
return f"In {s}"
|
||||
|
||||
@property
|
||||
def folder(self):
|
||||
print(f"{self.ensemble.storage_id}:{self.ensemble.slug}/{self.slug}")
|
||||
return f"{self.ensemble.storage_id}:{self.ensemble.slug}/{self.slug}"
|
||||
|
||||
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 resource_key(resource, filename):
|
||||
return f'{resource.project.folder}/resources/{filename}'
|
||||
|
||||
class Resource(models.Model):
|
||||
''' A viewable file resource attached to a project
|
||||
|
||||
e.g PDF instructions, MP3 backing track
|
||||
'''
|
||||
project = models.ForeignKey(Project, related_name='resources', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(blank=True)
|
||||
file = models.FileField(storage=BYOStorage(), upload_to=resource_key)
|
||||
media_type = models.CharField(max_length=10, choices=MEDIA_TYPES, default='*')
|
||||
visible = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-visible', '-pk']
|
||||
|
||||
def accept(self):
|
||||
if self.media_type == 'general':
|
||||
return ".*"
|
||||
return f"{self.media_type}/*"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class WikiPage(models.Model):
|
||||
''' An editable wiki page for the project in markdown format
|
||||
|
||||
'''
|
||||
project = models.ForeignKey(Project, related_name='wiki_pages', on_delete=models.CASCADE)
|
||||
title = models.CharField(max_length=255)
|
||||
markdown = models.TextField()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return resolve_url('wiki', project=self.project_id, pk=self.pk)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 426 KiB |
@@ -0,0 +1,63 @@
|
||||
@font-face {
|
||||
font-family: MartinHand;
|
||||
src: url('/static/fonts/Martinhand3.ttf');
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #485fc7;
|
||||
}
|
||||
|
||||
.fancy {
|
||||
font-family: MartinHand;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.has-text-shadow {
|
||||
text-shadow: 1px 1px 2px #000;
|
||||
}
|
||||
|
||||
.is-form-group {
|
||||
max-width: 600px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.is-centered {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.is-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.button.is-primary, .button.is-primary:hover {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
A.admin-link:after {
|
||||
content: "*";
|
||||
}
|
||||
|
||||
TEXTAREA.input {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.control INPUT[type='file'] {
|
||||
border: none;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
margin: auto 10%;
|
||||
}
|
||||
|
||||
.project-footer {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
background-color: #EEE;
|
||||
padding: 10px 10px;
|
||||
margin-top: 30px;
|
||||
border-top-left-radius: 6px;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% 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>
|
||||
<title>{% block title %}Polyphonic{% endblock %}</title>
|
||||
{% block media %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% block navigation %}
|
||||
<nav class="navbar" role="navigation">
|
||||
<div class="navbar-brand has-text-primary">
|
||||
<a class="navbar-item" href="/">
|
||||
<span class="icon fancy is-size-2 mx-4"><i class="fas fa-random"></i></span>
|
||||
<span class="fancy is-size-2">Polyphonic</span>
|
||||
</a>
|
||||
<span class="navbar-item is-hidden-mobile fancy is-size-5">Music 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-4">{% firstof ensemble project.ensemble %}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>No content!</h1>
|
||||
{% endblock %}
|
||||
|
||||
<!-- 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>
|
||||
@@ -0,0 +1,28 @@
|
||||
{% load crispy_forms_field %}
|
||||
|
||||
<div class="field">
|
||||
<div id="div_id_{{ field.name }}" class="file has-name is-fullwidth">
|
||||
<label class="file-label">
|
||||
{% crispy_field field 'class' 'file-input'%}
|
||||
<span class="file-cta">
|
||||
<span class="file-icon">
|
||||
<i class="fas fa-upload"></i>
|
||||
</span>
|
||||
<span class="file-label">
|
||||
Choose a file…
|
||||
</span>
|
||||
</span>
|
||||
<span class="file-name"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const fileInput = document.querySelector('#div_id_{{ field.name }} input[type=file]');
|
||||
fileInput.onchange = () => {
|
||||
if (fileInput.files.length > 0) {
|
||||
const fileName = document.querySelector('#div_id_{{ field.name }} .file-name');
|
||||
fileName.textContent = fileInput.files[0].name;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
{% 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-form-group">
|
||||
{% crispy form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ ensemble.name }}</h2>
|
||||
<p>{{ ensemble.details }}</p>
|
||||
<h4>Administrators</h4>
|
||||
<ul>
|
||||
{% for admin in ensemble.admins.all %}
|
||||
<li><a href="mailto:{{ admin.email }}">{% firstof admin.get_full_name admin.get_username %}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
{% load md2 %}
|
||||
|
||||
{% block page %}
|
||||
<div class="admin-tools is-pulled-right">
|
||||
<a class="button is-link" href="{% url 'register' %}">
|
||||
<span class="icon"><i class="fas fa-plus-circle"></i></span>
|
||||
<span>Register another</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h3 class="title">My Ensembles</h3>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
{% for ensemble in object_list %}
|
||||
<div class="column is-half">
|
||||
<div class="card">
|
||||
<header class="card-header{% if ensemble.pk == ensemble_id %} has-background-link-light{% endif %}">
|
||||
<a class="card-header-title" href="{% url 'register' %}?code={{ ensemble.code }}">
|
||||
{{ ensemble.name }}
|
||||
</a>
|
||||
<a class="card-header-icon" href="{% url 'ensemble_forget' pk=ensemble.id %}">
|
||||
<span class="delete"></span>
|
||||
</a>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
{% if ensemble.details %}
|
||||
<div class="content">
|
||||
{{ ensemble.details | markdown }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
{% with projects=ensemble.active_projects.count %}
|
||||
<p>{{ projects }} active project{{ projects|pluralize }}</p>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,73 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
{% load md2 %}
|
||||
|
||||
{% block admin %}
|
||||
<a href="{% url 'project_create' %}" class="button is-link">
|
||||
<span class="icon"><i class="fas fa-plus-circle"></i></span>
|
||||
<span>Add new</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block page %}
|
||||
<h3 class="title">Projects for {{ ensemble.name }}</h3>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
{% for project in ensemble.active_projects %}
|
||||
<div class="column is-half">
|
||||
<div class="card">
|
||||
|
||||
<a class="" href="{% url 'project_detail' project=project.id %}">
|
||||
<header class="card-header">
|
||||
<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">
|
||||
<div class="content">
|
||||
{{ project.description | markdown }}
|
||||
</div>
|
||||
<p><small>
|
||||
{% if project.deadline %}In {{ project.deadline|timeuntil }}<br/>{% endif %}
|
||||
{% if project.works.count %}
|
||||
<a href="{% url 'item_list' project=project.pk %}">
|
||||
{{ project.works.count }} works
|
||||
</a>
|
||||
<br/>
|
||||
{% endif %}
|
||||
{% if project.submissions.count %}{{ project.submissions.count }} submissions<br/>{% endif %}
|
||||
</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="hero">
|
||||
<div class="hero-body">
|
||||
<p class="title">No projects currently planned</p>
|
||||
<p class="subtitle">Go put your feet up!</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% 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">
|
||||
|
||||
<p>
|
||||
Joining instructions for participants<br/><br/>
|
||||
URL: <a href="{{ ensemble_url }}">{{ ensemble_url }}</a><br/>
|
||||
Code: {{ ensemble.ensemble_code }}<br/>
|
||||
Passphrase: {{ ensemble.passphrase }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="narrow">
|
||||
<h3>Manage {{ ensemble.name }}</h3>
|
||||
<p>
|
||||
Joining instructions for participants<br/><br/>
|
||||
URL: <a href="{{ ensemble_url }}">{{ ensemble_url }}</a><br/>
|
||||
Passphrase: {{ ensemble.passphrase }}
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="{% url 'work_list' %}">Library</a></li>
|
||||
</ul>
|
||||
<p>
|
||||
Sorry, not much you can do here yet.
|
||||
<ul>
|
||||
<li>Should be able to create new projects.</li>
|
||||
{% if request.user.is_superuser %}
|
||||
<li><a href="/admin">Django Admin</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<br/>
|
||||
Logged in as {{ request.user }} [<a href="{% url 'logout' %}">Logout</a>]
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,110 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
|
||||
<div class="columns">
|
||||
<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 'ensemble_detail' %}">Projects</a></li>
|
||||
<li><a href="{% url 'work_list' %}">Library</a></li>
|
||||
</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>
|
||||
{% for page in project.wiki_pages.all %}
|
||||
<li><a class="nav-link {% if page.id == wiki_id %}active{% endif %}"
|
||||
href="{% url 'wiki' project=project.id pk=page.id %}">{{ page.title }}</a></li>
|
||||
{% endfor %}
|
||||
<li><a role="tab" href="{% url 'resource_list' project=project.pk %}">Resources</a></li>
|
||||
|
||||
{% if 'library' in modules %}
|
||||
<li><a href="{% url 'item_list' project=project.id %}">My Music</a></li>
|
||||
{% endif %}
|
||||
|
||||
{% if 'submission' in modules %}
|
||||
<!--a role="tab" href="">Record a submission</a-->
|
||||
{% if request.is_admin %}
|
||||
<li><a role="tab" class="admin-link" href="{% url 'submission_list' project=project.id %}">Submissions</a></li>
|
||||
{% endif %}
|
||||
|
||||
<li><a role="tab" href="{% url 'submission_create' project=project.id %}">Send a File</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<p class="menu-label">Admin</p>
|
||||
{% if request.is_admin %}
|
||||
<ul class="menu-list">
|
||||
<li><a class="admin-link" href="{% url 'collection_list' %}">Collections</a></li>
|
||||
{% if request.user.is_superuser %}
|
||||
<li><a class="admin-link" href="/admin" target="polyphonic_admin" rel="noopener noreferrer">Django Admin</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<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 project.enable_library %}
|
||||
<li><a href="{% url 'item_list' project=project.pk %}">My Music</a></li>
|
||||
{% endif %}
|
||||
{% if project.enable_submissions %}
|
||||
<li><a href="{% url 'submission_create' project=project.pk %}">Send File</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<section class="section">
|
||||
{% if request.is_admin %}
|
||||
<div class="admin-tools is-pulled-right">
|
||||
{% block admin %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if project %}<h3 class="title">{{ project.name }}</h3>{% endif %}
|
||||
{% block page %}
|
||||
No content
|
||||
{% endblock %}
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if ensemble %}
|
||||
<div class="project-footer">
|
||||
{{ ensemble.name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,62 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
{% load md2 %}
|
||||
{% load polyphonic %}
|
||||
|
||||
{% block admin %}
|
||||
<a href="{% url 'wiki_create' project=project.pk %}" class="button is-link">
|
||||
<span class="icon"><i class="fas fa-file"></i></span>
|
||||
<span>Add Page</span>
|
||||
</a>
|
||||
<a href="{% url 'project_edit' project=project.pk %}" class="button is-link">
|
||||
<span class="icon"><i class="fas fa-edit"></i></span>
|
||||
<span>Edit</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<div class="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 'library' in modules %}
|
||||
<div class="block">
|
||||
{% include 'library/project_detail.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if 'submission' in modules %}
|
||||
<div class="block">
|
||||
{% include 'submissions/project_detail.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block media %}
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<h3 class="subtitle">{% firstof title view.title %}</h3>
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-form-group">
|
||||
{% if instructions %}
|
||||
<p>{{ instructions }}</p>
|
||||
{% endif %}
|
||||
{% crispy form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
{% if not request.user.is_authenticated %}
|
||||
<a href="{% url 'login' %}" style="float: right"><i class="fa fa-key"></i></a>
|
||||
{% endif %}
|
||||
<div class="columns is-centered">
|
||||
<div class="box is-half">
|
||||
<h3 class="title">Join an ensemble</h3>
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form | crispy }}
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-link">Register</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,58 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
{% load md2 %}
|
||||
|
||||
{% block admin %}
|
||||
<a class="button is-link" href="{% url 'resource_create' project=project.pk %}">
|
||||
<span class="icon"><i class="fas fa-plus-circle"></i></span>
|
||||
<span>Add new</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<div class="columns is-multiline">
|
||||
{% for resource in object_list %}
|
||||
{% with download=resource.file.url %}
|
||||
<div class="column is-half">
|
||||
<div class="card {% if not object.visible %}disabled{% endif %}">
|
||||
<div class="card-header">
|
||||
|
||||
<div class="card-header-title">
|
||||
{% if download %}
|
||||
<a href="{{ download }}">{{ resource.name }}</a>
|
||||
{% else %}
|
||||
{{ resource.name }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-header-icon">
|
||||
{% if request.is_admin %}
|
||||
<a href="{% url 'resource_upload' project=project.pk pk=resource.pk %}" class="icon" title="Upload">
|
||||
<i class="fas fa-upload"></i>
|
||||
</a>
|
||||
<a href="{% url 'resource_edit' project=project.pk pk=resource.pk %}" class="icon" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
<small>{{ resource.description|markdown }}</small>
|
||||
{% if not resource.visible %}
|
||||
<br/>(This resource is hidden from participants)
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if download and resource.media_type == 'audio' %}
|
||||
<audio class="resource-player" controls src="{{ download }}" style="width: 100%;"></audio>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<div class="column">
|
||||
<p>There are resources for this project</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,90 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="narrow">
|
||||
<h3 id="status">File upload</h3>
|
||||
<form method="POST" action="{{ upload.url }}" enctype="multipart/form-data" id="item-upload" class="dropzone">
|
||||
<div class="fallback">
|
||||
{% for field, value in upload.fields.items %}
|
||||
<input type="hidden" name="{{ field }}" value="{{ value }}" />
|
||||
{% endfor %}
|
||||
|
||||
<input name="file" type="file" accept="{{ accept_files }}/*" id="fallback-file"/>
|
||||
<input type="submit" value="Send" id="fallback-upload"/>
|
||||
</div>
|
||||
</form>
|
||||
<div class="form-actions text-right">
|
||||
<a class=""
|
||||
href="{{ cancel_url }}">Cancel</a>
|
||||
|
||||
<button type="submit" id="upload" disabled>Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<link rel="stylesheet" href="{% static 'dropzone/dropzone.css' %}"/>
|
||||
<script src="{% static 'dropzone/dropzone.js' %}"></script>
|
||||
{{ ajax_upload|json_script:"dropzone-data" }}
|
||||
<script type="text/javascript">
|
||||
|
||||
const dzData = JSON.parse(document.getElementById('dropzone-data').textContent);
|
||||
console.log(dzData);
|
||||
|
||||
let status = $('#status');
|
||||
let uploadBtn = $('#upload');
|
||||
|
||||
$('#fallback-upload').on('click', function() {
|
||||
status.html('Uploading - please wait...');
|
||||
}).attr('disabled', true);
|
||||
|
||||
$('#fallback-file').on('change', function() {
|
||||
status.html('Ready to upload');
|
||||
$('#fallback-upload').attr('disabled', false);
|
||||
});
|
||||
|
||||
let fallback = {{ request.GET.fallback|yesno:"true,false,false" }};
|
||||
|
||||
Dropzone.options.itemUpload = {
|
||||
params: dzData['fields'],
|
||||
forceFallback: fallback,
|
||||
autoProcessQueue: true,
|
||||
createImageThumbnails: false,
|
||||
//acceptedFiles: acceptFiles,
|
||||
timeout: 36000000,
|
||||
maxFiles: 1,
|
||||
addRemoveLinks: true,
|
||||
maxFilesize: 500,
|
||||
dictDefaultMessage: "Click to select a file to upload (max 500Mb)",
|
||||
dictFallbackMessage: "Your browser is old!, using alternative method",
|
||||
init: function() {
|
||||
let dz = this;
|
||||
uploadBtn.on('click', function() {
|
||||
dz.processQueue();
|
||||
});
|
||||
this.on('addedfile', function(f) {
|
||||
status.html('Ready to upload...');
|
||||
uploadBtn.attr('disabled', false);
|
||||
});
|
||||
this.on('sending', function(f) {
|
||||
status.html('Uploading - please wait...');
|
||||
uploadBtn.attr('disabled', true);
|
||||
});
|
||||
this.on('success', function(f) {
|
||||
status.html("Upload successfull");
|
||||
//if (! confirm("Upload complete - redirect?")) return;
|
||||
let s3_location = f.xhr.getResponseHeader("Location");
|
||||
location.href = "{{ success_url }}?location=" + s3_location;
|
||||
});
|
||||
this.on('error', function(file, msg) {
|
||||
status.html('There was an issue - please see troubleshooting');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
|
||||
{% block admin %}
|
||||
<a href="{% url 'wiki_edit' project=project.pk pk=wikipage.pk %}" class="button is-link">
|
||||
<span class="icon"><i class="fas fa-edit"></i></span>
|
||||
<span>Edit</span>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
<h3 class="subtitle">{{ wikipage.title }}</h3>
|
||||
<div class="box content wiki-page">
|
||||
{{ wiki_html|safe }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "interface/project_base.html" %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% 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 | crispy }}
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% 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/>
|
||||
If you have an ensemble code <a href="{% url 'register' %}">enter it here</a> instead.
|
||||
</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 'ensemble_detail' %}" class="button is-light">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,9 @@
|
||||
from django import template
|
||||
import os.path
|
||||
|
||||
register = template.Library()
|
||||
|
||||
def basename(value):
|
||||
return os.path.basename(value)
|
||||
|
||||
register.filter('basename', basename)
|
||||
@@ -0,0 +1,14 @@
|
||||
from django import template
|
||||
from django.utils import timesince
|
||||
|
||||
register = template.Library()
|
||||
|
||||
def roughtimesince(value):
|
||||
return timesince.timesince(value, depth=1)
|
||||
|
||||
register.filter('roughtimesince', roughtimesince)
|
||||
|
||||
def roughtimeuntil(value):
|
||||
return timesince.timeuntil(value, depth=1)
|
||||
|
||||
register.filter('roughtimeuntil', roughtimeuntil)
|
||||
@@ -0,0 +1,10 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def url_update(context, **kwargs):
|
||||
params = context.request.GET.copy()
|
||||
for k in kwargs:
|
||||
params[k] = kwargs[k]
|
||||
return "?" + params.urlencode()
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.test import TestCase
|
||||
|
||||
class IntegrationTestCase(TestCase):
|
||||
|
||||
def test_runs(self):
|
||||
self.assertTrue(True)
|
||||
@@ -0,0 +1,63 @@
|
||||
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)
|
||||
@@ -0,0 +1,30 @@
|
||||
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('', views.EnsembleProjectListView.as_view(), name='ensemble_detail'),
|
||||
path('ensembles', views.EnsembleListView.as_view(), name="ensemble_list"),
|
||||
path('ensembles/<int:pk>', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
|
||||
path('ensembles/<int:pk>/forget', views.EnsembleForgetView.as_view(), name='ensemble_forget'),
|
||||
|
||||
path('projects/create', views.ProjectCreateView.as_view(), name="project_create"),
|
||||
path('projects/<int:project>', views.ProjectDetailView.as_view(), name="project_detail"),
|
||||
path('projects/<int:project>/edit', views.ProjectUpdateView.as_view(), name="project_edit"),
|
||||
#path('projects/<int:project>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"),
|
||||
|
||||
path('projects/<int:project>/page/create', views.WikiCreateView.as_view(), name="wiki_create"),
|
||||
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>/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>/upload', views.ResourceUploadView.as_view(), name="resource_upload"),
|
||||
path('projects/<int:project>/resources/<int:pk>/edit', views.ResourceEditView.as_view(), name="resource_edit"),
|
||||
]
|
||||
@@ -0,0 +1,334 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect, resolve_url
|
||||
from django.views.generic import TemplateView, RedirectView
|
||||
from django.views.generic.detail import DetailView
|
||||
from django.views.generic.list import ListView
|
||||
from django.views.generic.edit import CreateView, UpdateView
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.contrib import auth
|
||||
|
||||
from markdown2 import markdown
|
||||
|
||||
from . import models, forms
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#from django.core.signing import Signer
|
||||
#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 request.is_admin:
|
||||
if request.ensemble_id is None:
|
||||
return redirect('ensemble_list')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
# 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 request.user.is_authenticated:
|
||||
try:
|
||||
request.user.ensembles.get(pk=request.ensemble_id)
|
||||
request.is_admin = True
|
||||
except models.Ensemble.DoesNotExist:
|
||||
pass
|
||||
|
||||
if not request.ensemble_id:
|
||||
return redirect('register')
|
||||
|
||||
if self.admin_required and not request.is_admin:
|
||||
return redirect('login')
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def ensemble(self):
|
||||
return models.Ensemble.objects.get(pk=self.request.ensemble_id)
|
||||
|
||||
#def get_context_data(self, **kwargs):
|
||||
# context = super().get_context_data(**kwargs)
|
||||
# context['ensemble'] = self.ensemble
|
||||
# return context
|
||||
|
||||
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()
|
||||
context['modules'] = context['project'].modules.values_list('name', flat=True)
|
||||
return context
|
||||
|
||||
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('-', '')
|
||||
logger.debug("Registering with code %s", code)
|
||||
|
||||
# check if already joined
|
||||
if code in registered or request.user.is_superuser:
|
||||
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.append(ensemble.code)
|
||||
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)
|
||||
|
||||
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.append(e.code)
|
||||
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 EnsembleForgetView(EnsembleMixin, RedirectView):
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
|
||||
registered = self.request.session.setdefault('registered', [])
|
||||
|
||||
ensemble = models.Ensemble.objects.get(pk=self.kwargs['pk'])
|
||||
try:
|
||||
registered.remove(ensemble.code)
|
||||
self.request.session['registered'] = registered
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if self.request.ensemble_id == ensemble.pk:
|
||||
del(self.request.session['ensemble'])
|
||||
|
||||
return resolve_url('ensemble_detail')
|
||||
|
||||
class EnsembleListView(ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
registered = self.request.session.get('registered', [])
|
||||
if self.request.user.is_superuser:
|
||||
current = models.Ensemble.objects.all()
|
||||
else:
|
||||
current = models.Ensemble.objects.filter(code__in=registered)
|
||||
|
||||
return current
|
||||
|
||||
#def get_context_data(self, **kwargs):
|
||||
# context = super().get_context_data(**kwargs)
|
||||
# context['ensemble_id'] = self.request.session.get('ensemble')
|
||||
# return context
|
||||
|
||||
class EnsembleProjectListView(EnsembleMixin, DetailView):
|
||||
template_name = 'interface/ensemble_project_list.html'
|
||||
|
||||
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)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
if self.request.is_admin:
|
||||
data['ensemble_url'] = self.request.build_absolute_uri('/?code={0}'.format(self.ensemble.ensemble_code()))
|
||||
return data
|
||||
|
||||
class EnsembleDetailView(DetailView):
|
||||
model = models.Ensemble
|
||||
|
||||
class ProjectDetailView(ProjectMixin, DetailView):
|
||||
|
||||
def get_object(self):
|
||||
return self.get_project()
|
||||
|
||||
class ProjectCreateView(EnsembleMixin, CreateView):
|
||||
model = models.Project
|
||||
template_name = "interface/default_form.html"
|
||||
title = "Add a new project"
|
||||
form_class = forms.ProjectForm
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = form.save(commit=False)
|
||||
self.object.ensemble_id = self.request.ensemble_id
|
||||
self.object.owner = self.request.user
|
||||
self.object.save()
|
||||
return redirect('project_detail', project=self.object.pk)
|
||||
|
||||
class ProjectUpdateView(EnsembleMixin, UpdateView):
|
||||
model = models.Project
|
||||
template_name = "interface/default_form.html"
|
||||
#fields = ['name', 'description', 'event_date', 'enable_library', 'enable_submissions', 'active']
|
||||
pk_url_kwarg = 'project'
|
||||
form_class = forms.ProjectForm
|
||||
|
||||
def get_success_url(self):
|
||||
return resolve_url('project_detail', project=self.kwargs['project'])
|
||||
|
||||
#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']
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = form.save(commit=False)
|
||||
self.object.project = self.get_project()
|
||||
self.object.save()
|
||||
return redirect('wiki', project=self.object.project_id, pk=self.object.pk)
|
||||
|
||||
class WikiEditView(ProjectMixin, UpdateView):
|
||||
admin_required = True
|
||||
model = models.WikiPage
|
||||
fields = ['title', 'markdown']
|
||||
|
||||
|
||||
class ResourceCreateView(ProjectMixin, CreateView):
|
||||
model = models.Resource
|
||||
form_class = forms.ResourceForm
|
||||
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(ProjectMixin, UpdateView):
|
||||
admin_required = True
|
||||
model = models.Resource
|
||||
fields = ['file']
|
||||
template_name = 'interface/default_form.html'
|
||||
|
||||
def get_success_url(self):
|
||||
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
|
||||
Reference in New Issue
Block a user