Major changes to permission system
This commit is contained in:
parent
8a249de51c
commit
bc9f292a2e
@ -1,6 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Submit
|
from crispy_forms.layout import Submit, HTML, Div
|
||||||
|
from crispy_bulma.layout import FormGroup
|
||||||
|
|
||||||
from . import models, fields
|
from . import models, fields
|
||||||
|
|
||||||
@ -12,18 +13,25 @@ class BaseForm(forms.Form):
|
|||||||
|
|
||||||
def get_form_helper(self):
|
def get_form_helper(self):
|
||||||
helper = FormHelper(self)
|
helper = FormHelper(self)
|
||||||
helper.add_input(Submit('submit', 'Submit', css_class='button is-link'))
|
#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
|
return helper
|
||||||
|
|
||||||
|
|
||||||
class ProjectForm(forms.ModelForm, BaseForm):
|
class ProjectForm(forms.ModelForm, BaseForm):
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Project
|
model = models.Project
|
||||||
fields = ['name', 'description', 'event_date']
|
fields = ['name', 'description', 'event_date']
|
||||||
widgets = {
|
#widgets = {
|
||||||
'event_date': forms.DateTimeInput(attrs={'type': 'date'})
|
# 'event_date': forms.DateTimeInput(attrs={'type': 'date'})
|
||||||
}
|
#}
|
||||||
|
|
||||||
class ResourceForm(forms.ModelForm, BaseForm):
|
class ResourceForm(forms.ModelForm, BaseForm):
|
||||||
|
|
||||||
@ -36,6 +44,12 @@ class ResourceForm(forms.ModelForm, BaseForm):
|
|||||||
helper[3].wrap(fields.BulmaFileUpload)
|
helper[3].wrap(fields.BulmaFileUpload)
|
||||||
return helper
|
return helper
|
||||||
|
|
||||||
|
class WikiForm(forms.ModelForm, BaseForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.WikiPage
|
||||||
|
fields = ['title', 'markdown']
|
||||||
|
|
||||||
class CodeForm(BaseForm):
|
class CodeForm(BaseForm):
|
||||||
code = forms.CharField(max_length=14,
|
code = forms.CharField(max_length=14,
|
||||||
widget=forms.TextInput(attrs={'placeholder': 'xxx-xxx-xxx', 'inputmode': 'numeric'}))
|
widget=forms.TextInput(attrs={'placeholder': 'xxx-xxx-xxx', 'inputmode': 'numeric'}))
|
||||||
|
|||||||
25
app/interface/migrations/0002_auto_20230202_0804.py
Normal file
25
app/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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -8,8 +8,6 @@ from byostorage.user import BYOStorage
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
@ -27,8 +25,13 @@ def rough_date(d):
|
|||||||
in_past = days < 0
|
in_past = days < 0
|
||||||
if in_past:
|
if in_past:
|
||||||
days = abs(days)
|
days = abs(days)
|
||||||
if days ==0:
|
if days == 0:
|
||||||
return "today!"
|
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:
|
if days >= 7:
|
||||||
return in_past, "{0:d} weeks, {1:d} days".format(int(days / 7), int(days % 7))
|
return in_past, "{0:d} weeks, {1:d} days".format(int(days / 7), int(days % 7))
|
||||||
return in_past, f"{days} days"
|
return in_past, f"{days} days"
|
||||||
@ -45,23 +48,33 @@ class Ensemble(models.Model):
|
|||||||
help_text="Display name")
|
help_text="Display name")
|
||||||
slug = models.SlugField(max_length=100, editable=False, unique=True,
|
slug = models.SlugField(max_length=100, editable=False, unique=True,
|
||||||
help_text="Short name for the ensemble - used for folders")
|
help_text="Short name for the ensemble - used for folders")
|
||||||
code = models.CharField(max_length=9, default=generate_code,
|
#code = models.CharField(max_length=9, default=generate_code,
|
||||||
help_text="Ensemble registration code")
|
# help_text="Ensemble registration code")
|
||||||
passphrase = models.CharField(max_length=100,
|
#passphrase = models.CharField(max_length=100,
|
||||||
help_text="Used to register ensembles")
|
# help_text="Used to register ensembles")
|
||||||
admins = models.ManyToManyField('auth.User', related_name='ensembles')
|
admins = models.ManyToManyField('auth.User', related_name='ensembles')
|
||||||
details = models.TextField(blank=True,
|
details = models.TextField(blank=True,
|
||||||
help_text="Description of the ensemble (markdown)")
|
help_text="Description of the ensemble (markdown)")
|
||||||
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL,
|
storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL,
|
||||||
help_text="Default storage for this ensemble")
|
help_text="Default storage for this ensemble")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('slug', )
|
||||||
|
|
||||||
def active_projects(self):
|
def active_projects(self):
|
||||||
return self.projects.filter(active=True).order_by('event_date')
|
return self.projects.filter(active=True, event_date__gte=timezone.now())
|
||||||
|
|
||||||
def ensemble_code(self):
|
def ensemble_code(self):
|
||||||
code = str(self.code)
|
code = str(self.code)
|
||||||
return "{}-{}-{}".format(code[:3], code[3:6], code[6:])
|
return "{}-{}-{}".format(code[:3], code[3:6], code[6:])
|
||||||
|
|
||||||
|
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):
|
def save(self, **kwargs):
|
||||||
if not self.slug:
|
if not self.slug:
|
||||||
self.slug = slugify(self.name)
|
self.slug = slugify(self.name)
|
||||||
@ -83,7 +96,7 @@ class Project(models.Model):
|
|||||||
owner = models.CharField(max_length=255, blank=True)
|
owner = models.CharField(max_length=255, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['active', '-pk']
|
ordering = ['active', 'event_date']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def days(self):
|
def days(self):
|
||||||
@ -104,8 +117,13 @@ class Project(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def folder(self):
|
def folder(self):
|
||||||
print(f"{self.ensemble.storage_id}:{self.ensemble.slug}/{self.slug}")
|
project = slugify(self.name)
|
||||||
return f"{self.ensemble.storage_id}:{self.ensemble.slug}/{self.slug}"
|
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 __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
8
app/interface/templates/403.html
Normal file
8
app/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
app/interface/templates/404.html
Normal file
8
app/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 %}
|
||||||
@ -8,7 +8,10 @@
|
|||||||
{% block page %}
|
{% block page %}
|
||||||
<h3 class="subtitle">{% firstof title view.title %}</h3>
|
<h3 class="subtitle">{% firstof title view.title %}</h3>
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<div class="column is-form-group">
|
<div class="column is-two-thirds">
|
||||||
|
{% if instructions %}
|
||||||
|
<p>{{ instructions }}</p>
|
||||||
|
{% endif %}
|
||||||
{% crispy form %}
|
{% crispy form %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,12 +1,49 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "interface/project_base.html" %}
|
||||||
|
{% load md2 %}
|
||||||
|
|
||||||
|
{% block admin %}
|
||||||
|
<a href="{% url 'project_create' object.slug %}" class="button is-link">
|
||||||
|
<span class="icon"><i class="fas fa-plus-circle"></i></span>
|
||||||
|
<span>Add project</span>
|
||||||
|
</a>
|
||||||
|
{% if inactive %}
|
||||||
|
<a href="?" class="button is-link">
|
||||||
|
<span class="icon"><i class="fas fa-archive"></i></span>
|
||||||
|
<span>Hide old</span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="?inactive" class="button is-link">
|
||||||
|
<span class="icon"><i class="fas fa-archive"></i></span>
|
||||||
|
<span>Show all</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<h3 class="title">Projects for {{ensemble.name }}</h3>
|
||||||
|
<p>{{ ensemble.details|markdown }}</p>
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<h2>{{ ensemble.name }}</h2>
|
|
||||||
<p>{{ ensemble.details }}</p>
|
|
||||||
<h4>Administrators</h4>
|
<h4>Administrators</h4>
|
||||||
<ul>
|
<ul>
|
||||||
{% for admin in ensemble.admins.all %}
|
{% for admin in ensemble.admins.all %}
|
||||||
<li><a href="mailto:{{ admin.email }}">{% firstof admin.get_full_name admin.get_username %}</a></li>
|
<li><a href="mailto:{{ admin.email }}">{% firstof admin.get_full_name admin.get_username %}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{% include 'interface/project_items.html' %}
|
||||||
|
|
||||||
|
<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_link }}">{{ ensemble_link }}</a><br/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -2,40 +2,51 @@
|
|||||||
{% load md2 %}
|
{% load md2 %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
{% comment %}
|
||||||
<div class="admin-tools is-pulled-right">
|
<div class="admin-tools is-pulled-right">
|
||||||
<a class="button is-link" href="{% url 'register' %}">
|
<a class="button is-link" href="{% url 'register' %}">
|
||||||
<span class="icon"><i class="fas fa-plus-circle"></i></span>
|
<span class="icon"><i class="fas fa-plus-circle"></i></span>
|
||||||
<span>Register another</span>
|
<span>Register another</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
<h3 class="title">My Ensembles</h3>
|
<h3 class="title">My Ensembles</h3>
|
||||||
|
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
{% for ensemble in object_list %}
|
{% for ensemble in object_list %}
|
||||||
<div class="column is-half">
|
<div class="column is-half-tablet is-one-third-widescreen">
|
||||||
<div class="card">
|
<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">
|
<div class="card-content">
|
||||||
{% if ensemble.details %}
|
<div class="media">
|
||||||
<div class="content">
|
<div class="media-left">
|
||||||
{{ ensemble.details | markdown }}
|
<figure class="image is-48x48">
|
||||||
</div>
|
<img src="https://bulma.io/images/placeholders/96x96.png" alt="Placeholder image">
|
||||||
{% endif %}
|
</figure>
|
||||||
<div>
|
</div>
|
||||||
{% with projects=ensemble.active_projects.count %}
|
<div class="media-content">
|
||||||
<p>{{ projects }} active project{{ projects|pluralize }}</p>
|
<a href="{% url 'ensemble_detail' ensemble.slug %}">
|
||||||
{% endwith %}
|
<p class="title is-4">{{ ensemble.name }}</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if ensemble.details %}
|
||||||
|
<div class="content">
|
||||||
|
{{ ensemble.details | markdown }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% empty %}
|
||||||
|
<div class="hero">
|
||||||
|
You don't currently have access to any ensembles - ask your administrator for a link.
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,73 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -12,7 +12,7 @@
|
|||||||
<p class="menu-label">My Things</p>
|
<p class="menu-label">My Things</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a href="{% url 'ensemble_list' %}">Ensembles</a></li>
|
<li><a href="{% url 'ensemble_list' %}">Ensembles</a></li>
|
||||||
<li><a href="{% url 'ensemble_detail' %}">Projects</a></li>
|
<li><a href="{% url 'project_list' %}">Projects</a></li>
|
||||||
<li><a href="{% url 'work_list' %}">Library</a></li>
|
<li><a href="{% url 'work_list' %}">Library</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@ -76,10 +76,10 @@
|
|||||||
{% if project.resources.count %}
|
{% if project.resources.count %}
|
||||||
<li><a href="{% url 'resource_list' project=project.pk %}">Resources</a></li>
|
<li><a href="{% url 'resource_list' project=project.pk %}">Resources</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if project.enable_library %}
|
{% if 'library' in modules %}
|
||||||
<li><a href="{% url 'item_list' project=project.pk %}">My Music</a></li>
|
<li><a href="{% url 'item_list' project=project.pk %}">My Music</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if project.enable_submissions %}
|
{% if 'submission' in modules %}
|
||||||
<li><a href="{% url 'submission_create' project=project.pk %}">Send File</a></li>
|
<li><a href="{% url 'submission_create' project=project.pk %}">Send File</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -97,14 +97,8 @@
|
|||||||
{% block page %}
|
{% block page %}
|
||||||
No content
|
No content
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if ensemble %}
|
|
||||||
<div class="project-footer">
|
|
||||||
{{ ensemble.name }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -58,5 +58,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if request.is_admin %}
|
||||||
|
<div class="block">
|
||||||
|
<a href="{{ project_link }}">Project Link</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,21 +1,18 @@
|
|||||||
{% extends "interface/project_base.html" %}
|
{% extends "interface/project_base.html" %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block media %}
|
||||||
|
{{ form.media }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
<style>
|
<h3 class="subtitle">{% firstof title view.title %}</h3>
|
||||||
TEXTAREA {
|
<div class="columns is-centered">
|
||||||
height: 200px;
|
<div class="column">
|
||||||
}
|
{% if instructions %}
|
||||||
FORM.vertical {
|
<p>{{ instructions }}</p>
|
||||||
max-width: 90%;
|
{% endif %}
|
||||||
}
|
{% crispy form %}
|
||||||
</style>
|
</div>
|
||||||
<div>
|
|
||||||
<h3>{{ title }}</h3>
|
|
||||||
<p>{{ instructions }}</p>
|
|
||||||
<form class="vertical" method="POST">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form | crispy }}
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -7,7 +7,6 @@
|
|||||||
<div class="box is-half">
|
<div class="box is-half">
|
||||||
<p class="block">
|
<p class="block">
|
||||||
Login is only required to administer a project.<br/>
|
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>
|
</p>
|
||||||
<form method="POST" class="vertical">
|
<form method="POST" class="vertical">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@ -15,7 +14,7 @@
|
|||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button is-link">Login</button>
|
<button class="button is-link">Login</button>
|
||||||
<a href="{% url 'ensemble_detail' %}" class="button is-light">Cancel</a>
|
<a href="{% url 'home' %}" class="button is-light">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
237
app/interface/tests/test_access.py
Normal file
237
app/interface/tests/test_access.py
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
from django.test import TestCase, Client
|
||||||
|
|
||||||
|
from interface import models, utils
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
class AccessTestCase(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
admin = User.objects.create_user(username='admin', password='foobar', is_superuser=True)
|
||||||
|
homer = User.objects.create_user(username='homer', password='maggie')
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
|
||||||
|
b_sharps = models.Ensemble.objects.create(name='The Be Sharps')
|
||||||
|
b_sharps.admins.add(homer)
|
||||||
|
bleeding_gums = models.Ensemble.objects.create(name='Lisa and the Bleeding Gums', slug='bleeding-gums')
|
||||||
|
party_posse = models.Ensemble.objects.create(name="Party Posse", slug='party-posse')
|
||||||
|
|
||||||
|
bleeding_gums.projects.create(name='Baker St', event_date=now-timedelta(days=12))
|
||||||
|
party_posse.projects.create(name='Navy Recruitment Day', event_date=now+timedelta(days=6))
|
||||||
|
b_sharps.projects.create(name='Baby on Board', event_date=now+timedelta(days=28))
|
||||||
|
bleeding_gums.projects.create(name='Open Mic Night', event_date=now+timedelta(hours=1))
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_ensembles(self):
|
||||||
|
self.client.post('/login', {'username': 'admin', 'password': 'foobar'})
|
||||||
|
response = self.client.get('/ensembles')
|
||||||
|
self.assertObjectList(response, ['Lisa and the Bleeding Gums', 'Party Posse', 'The Be Sharps'])
|
||||||
|
self.assertContains(response, 'Django Admin')
|
||||||
|
|
||||||
|
def test_admin_ensemble_permissions(self):
|
||||||
|
self.client.post('/login', {'username': 'admin', 'password': 'foobar'})
|
||||||
|
response = self.client.get('/ensembles/party-posse')
|
||||||
|
self.assertTrue(response.context['request'].is_admin)
|
||||||
|
self.assertContains(response, "Add project")
|
||||||
|
self.assertAccess({
|
||||||
|
'/ensembles/the-be-sharps': True,
|
||||||
|
'/ensembles/bleeding-gums': True,
|
||||||
|
'/ensembles/party-posse': True,
|
||||||
|
'/ensembles/unknown': False,
|
||||||
|
'/ensembles/the-be-sharps/new-project': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_admin_projects(self):
|
||||||
|
self.client.post('/login', {'username': 'admin', 'password': 'foobar'})
|
||||||
|
response = self.client.get('/projects')
|
||||||
|
self.assertObjectList(response, ['Open Mic Night', 'Navy Recruitment Day', 'Baby on Board'])
|
||||||
|
|
||||||
|
self.assertObjectList(self.client.get('/ensembles/bleeding-gums'), ['Open Mic Night'])
|
||||||
|
self.assertObjectList(self.client.get('/ensembles/bleeding-gums?inactive'), ['Open Mic Night', 'Baker St'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_ensembles(self):
|
||||||
|
self.client.post('/login', {'username': 'homer', 'password': 'maggie'})
|
||||||
|
response = self.client.get('/ensembles')
|
||||||
|
self.assertObjectList(response, ['The Be Sharps'])
|
||||||
|
|
||||||
|
self.assertNotContains(response, 'Django Admin')
|
||||||
|
|
||||||
|
def test_user_ensemble_permissions(self):
|
||||||
|
self.client.post('/login', {'username': 'homer', 'password': 'maggie'})
|
||||||
|
response = self.client.get('/ensembles/the-be-sharps')
|
||||||
|
self.assertTrue(response.context['request'].is_admin)
|
||||||
|
self.assertContains(response, "Add project")
|
||||||
|
self.assertContains(response, 'Show all')
|
||||||
|
self.assertAccess({
|
||||||
|
'/ensembles/the-be-sharps': True,
|
||||||
|
'/ensembles/bleeding-gums': False,
|
||||||
|
'/ensembles/party-posse': False,
|
||||||
|
'/ensembles/the-be-sharps/new-project': True,
|
||||||
|
'/ensembles/party-posse/new-project': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.authorize('ensemble_detail', ensemble='bleeding-gums')
|
||||||
|
self.assertAccess({
|
||||||
|
'/ensembles/the-be-sharps': True,
|
||||||
|
'/ensembles/bleeding-gums': True,
|
||||||
|
'/ensembles/party-posse': False,
|
||||||
|
'/ensembles/the-be-sharps/new-project': True,
|
||||||
|
'/ensembles/party-posse/new-project': False,
|
||||||
|
})
|
||||||
|
response = self.client.get('/ensembles/bleeding-gums')
|
||||||
|
self.assertFalse(response.context['request'].is_admin)
|
||||||
|
self.assertNotContains(response, 'Add project')
|
||||||
|
self.assertNotContains(response, 'Show all')
|
||||||
|
|
||||||
|
def test_user_projects(self):
|
||||||
|
self.client.post('/login', {'username': 'homer', 'password': 'maggie'})
|
||||||
|
response = self.client.get('/projects')
|
||||||
|
self.assertObjectList(response, ['Baby on Board'])
|
||||||
|
response = self.client.get('/projects/3')
|
||||||
|
self.assertTrue(response.context['request'].is_admin)
|
||||||
|
|
||||||
|
|
||||||
|
self.assertAccess({
|
||||||
|
'/projects/3': True,
|
||||||
|
'/projects/3/resources': True,
|
||||||
|
'/projects/3/resources/add': True,
|
||||||
|
'/projects/4': False,
|
||||||
|
'/projects/4/resources': False,
|
||||||
|
'/projects/4/resources/add': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.client.get(utils.signed_url('project_detail', project=4))
|
||||||
|
response = self.client.get('/projects')
|
||||||
|
self.assertObjectList(response, ['Open Mic Night', 'Baby on Board'])
|
||||||
|
response = self.client.get('/projects/4')
|
||||||
|
self.assertFalse(response.context['request'].is_admin)
|
||||||
|
|
||||||
|
def test_anon_ensembles(self):
|
||||||
|
response = self.client.get('/ensembles')
|
||||||
|
self.assertObjectList(response, [])
|
||||||
|
self.assertContains(response, 'You don\'t currently have access to any ensembles')
|
||||||
|
|
||||||
|
def test_anon_authorized_ensemble(self):
|
||||||
|
response = self.client.get(utils.signed_url('ensemble_detail', ensemble='party-posse'))
|
||||||
|
self.assertContains(response, 'Party Posse')
|
||||||
|
|
||||||
|
response = self.client.get('/ensembles')
|
||||||
|
self.assertObjectList(response, ['Party Posse'])
|
||||||
|
|
||||||
|
self.assertAccess({
|
||||||
|
'/ensembles/the-be-sharps': False,
|
||||||
|
'/ensembles/party-posse': True,
|
||||||
|
'/ensembles/bleeding-gums': False,
|
||||||
|
'/ensembles/unknown': False,
|
||||||
|
})
|
||||||
|
response = self.client.get('/projects')
|
||||||
|
self.assertObjectList(response, ['Navy Recruitment Day'])
|
||||||
|
|
||||||
|
def test_anon_authorized_project(self):
|
||||||
|
self.authorize('project_detail', project=4)
|
||||||
|
self.assertObjectList(self.client.get('/projects'), ['Open Mic Night'])
|
||||||
|
self.assertObjectList(self.client.get('/ensembles'), [])
|
||||||
|
|
||||||
|
self.assertAccess({
|
||||||
|
'/projects/4': True,
|
||||||
|
'/projects/4/resources': True,
|
||||||
|
'/projects/1': False,
|
||||||
|
'/projects/1/resources': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_anon_permission_denied(self):
|
||||||
|
self.assertAccess({
|
||||||
|
'/ensembles': True,
|
||||||
|
'/ensembles/the-be-sharps': False,
|
||||||
|
'/ensembles/party-posse': False,
|
||||||
|
'/ensembles/bleeding-gums': False,
|
||||||
|
'/ensembles/unknown': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
def authorize(self, url, **kwargs):
|
||||||
|
response = self.client.get(utils.signed_url(url, **kwargs))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def assertAccess(self, urls):
|
||||||
|
for url, expected in urls.items():
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code == 200, expected, f"Expected {expected} for {url} (status: {response.status_code})")
|
||||||
|
|
||||||
|
def assertObjectList(self, response, expected, element='name'):
|
||||||
|
self.assertEqual(response.status_code, 200, "No result returned")
|
||||||
|
objects = response.context['object_list'].values_list(element, flat=True)
|
||||||
|
self.assertEqual(list(objects), expected)
|
||||||
|
|
||||||
|
"""
|
||||||
|
def test_redirect(self):
|
||||||
|
self.skipTest("No redirect")
|
||||||
|
response = self.client.get('/')
|
||||||
|
self.assertRedirects(response, '/register?')
|
||||||
|
|
||||||
|
def test_redirect_project(self):
|
||||||
|
response = self.client.get('/projects/1')
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
#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.get('/ensembles/1')
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
url = utils.signed_url('register', group='ensemble', pk=1)
|
||||||
|
|
||||||
|
response = self.client.get(url + "i")
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertRedirects(response, '/ensembles/1')
|
||||||
|
|
||||||
|
response = self.client.get('/ensembles/1')
|
||||||
|
self.assertEqual(response.context['object'].pk, 1)
|
||||||
|
|
||||||
|
response = self.client.get('/projects/1', )
|
||||||
|
|
||||||
|
def old_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,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,20 +1,24 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.contrib.auth import views as auth_views
|
from django.contrib.auth import views as auth_views
|
||||||
|
from django.views.generic.base import RedirectView
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
|
path('', RedirectView.as_view(url='projects', permanent=False), name='home'),
|
||||||
|
|
||||||
path('login', auth_views.LoginView.as_view(), name='login'),
|
path('login', auth_views.LoginView.as_view(), name='login'),
|
||||||
path('logout', views.logout, name='logout'),
|
path('logout', auth_views.LogoutView.as_view(), name='logout'),
|
||||||
path('register', views.register, name="register"),
|
#path('register/<group>/<int:pk>', views.register, name='register'),
|
||||||
|
#path('deregister/<group>/<int:pk>', views.deregister, name='deregister'),
|
||||||
|
|
||||||
path('', views.EnsembleProjectListView.as_view(), name='ensemble_detail'),
|
|
||||||
path('ensembles', views.EnsembleListView.as_view(), name="ensemble_list"),
|
path('ensembles', views.EnsembleListView.as_view(), name="ensemble_list"),
|
||||||
path('ensembles/<int:pk>', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
|
path('ensembles/<slug:ensemble>', views.EnsembleDetailView.as_view(), name='ensemble_detail'),
|
||||||
path('ensembles/<int:pk>/forget', views.EnsembleForgetView.as_view(), name='ensemble_forget'),
|
path('ensembles/<slug:ensemble>/new-project', views.ProjectCreateView.as_view(), name="project_create"),
|
||||||
|
#path('ensembles/<int:pk>/forget', views.EnsembleForgetView.as_view(), name='ensemble_forget'),
|
||||||
|
|
||||||
path('projects/create', views.ProjectCreateView.as_view(), name="project_create"),
|
path('projects', views.ProjectListView.as_view(), name="project_list"),
|
||||||
path('projects/<int:project>', views.ProjectDetailView.as_view(), name="project_detail"),
|
path('projects/<int:project>', views.ProjectDetailView.as_view(), name="project_detail"),
|
||||||
path('projects/<int:project>/edit', views.ProjectUpdateView.as_view(), name="project_edit"),
|
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>/submissions.mk', views.ProjectMakefileView.as_view(), name="project_makefile"),
|
||||||
|
|||||||
23
app/interface/utils.py
Normal file
23
app/interface/utils.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django.shortcuts import resolve_url
|
||||||
|
from django.core.signing import Signer
|
||||||
|
from django.core.exceptions import SuspiciousOperation
|
||||||
|
|
||||||
|
signer = Signer()
|
||||||
|
|
||||||
|
def signed_url(name, **kwargs):
|
||||||
|
"""
|
||||||
|
>>> signed_url('foo/bar')
|
||||||
|
"""
|
||||||
|
url = resolve_url(name, **kwargs)
|
||||||
|
sig = signer.sign(url)
|
||||||
|
return sig.replace(":", "?auth=")
|
||||||
|
|
||||||
|
def check_signed_url(url, auth):
|
||||||
|
sig = signer.sign(url)
|
||||||
|
if sig[len(url)+1:] != auth:
|
||||||
|
sig = "_HIDDEN_"
|
||||||
|
raise SuspiciousOperation("Bad auth code")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import doctest
|
||||||
|
print(doctest.testmod())
|
||||||
@ -1,215 +1,205 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
from django.shortcuts import render, get_object_or_404, redirect, resolve_url
|
from django.shortcuts import render, get_object_or_404, redirect, resolve_url
|
||||||
from django.views.generic import TemplateView, RedirectView
|
from django.views.generic import TemplateView, RedirectView
|
||||||
from django.views.generic.detail import DetailView
|
from django.views.generic.detail import DetailView
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
from django.views.generic.edit import CreateView, UpdateView
|
from django.views.generic.edit import CreateView, UpdateView
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation, PermissionDenied
|
||||||
|
from django.http import Http404, HttpResponseForbidden
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from markdown2 import markdown
|
from markdown2 import markdown
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from . import models, forms
|
from . import models, forms
|
||||||
|
from interface.utils import signed_url, check_signed_url
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
#from django.core.signing import Signer
|
class AuthorizedResourceMixin(object):
|
||||||
#signer = Signer()
|
"""
|
||||||
|
Handles two parts of the permission system, signed urls and persistent authenticated resources
|
||||||
|
"""
|
||||||
|
|
||||||
#def signed_url(name, **kwargs):
|
SESSION_KEY = 'authorized'
|
||||||
# url = resolve_url(name, **kwargs)
|
|
||||||
# sig = signer.sign(url)
|
|
||||||
# return sig.replace(":", "?auth=")
|
|
||||||
|
|
||||||
class EnsembleMixin(object):
|
|
||||||
admin_required = False
|
admin_required = False
|
||||||
|
|
||||||
|
def is_authorized(self):
|
||||||
|
"By default check if superuser or a signed request"
|
||||||
|
if self.request.is_admin:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if 'auth' in self.request.GET:
|
||||||
|
check_signed_url(self.request.path, self.request.GET['auth'])
|
||||||
|
self.on_signed_request()
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def on_signed_request(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_authorized_keys(self, resource):
|
||||||
|
'Returns a set of authorized keys for this resource'
|
||||||
|
return set(self._authorized.get(resource, []))
|
||||||
|
|
||||||
|
def add_authorized_key(self, resource, key):
|
||||||
|
'Adds a key to the authorized list for this resource'
|
||||||
|
current = self.get_authorized_keys(resource)
|
||||||
|
current.add(key)
|
||||||
|
self._authorized[resource] = list(current)
|
||||||
|
self.request.session[self.SESSION_KEY] = self._authorized
|
||||||
|
|
||||||
|
def del_authorized_key(self, resource, key):
|
||||||
|
current = self.get_authorized_keys(resource)
|
||||||
|
current.discard(key)
|
||||||
|
if current:
|
||||||
|
self._authorized[resource] = list(current)
|
||||||
|
else:
|
||||||
|
self._authorized.pop(current)
|
||||||
|
self.request.session[self.SESSION_KEY] = self._authorized
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
|
||||||
request.ensemble_id = request.session.get('ensemble')
|
self._authorized = request.session.get('authorized', {})
|
||||||
request.is_admin = request.user.is_superuser
|
request.is_admin = request.user.is_superuser
|
||||||
|
|
||||||
if request.is_admin:
|
if not self.is_authorized():
|
||||||
if request.ensemble_id is None:
|
raise Http404("Either the given resource doesn't exist or you dont have access to it.")
|
||||||
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:
|
if self.admin_required and not request.is_admin:
|
||||||
return redirect('login')
|
raise PermissionDenied("You must be an ensemble admin.")
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
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):
|
class EnsembleMixin(AuthorizedResourceMixin):
|
||||||
# context = super().get_context_data(**kwargs)
|
|
||||||
# context['ensemble'] = self.ensemble
|
|
||||||
# return context
|
|
||||||
|
|
||||||
class ProjectMixin(EnsembleMixin):
|
def is_authorized(self):
|
||||||
|
if 'forget' in self.request.GET:
|
||||||
|
self.del_authorized_key('ensemble', self.kwargs['ensemble'])
|
||||||
|
raise Http404("Access removed")
|
||||||
|
super().is_authorized()
|
||||||
|
try:
|
||||||
|
self.ensemble = self.get_ensemble()
|
||||||
|
return True
|
||||||
|
except models.Ensemble.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
def get_project(self):
|
def get_ensemble(self):
|
||||||
if not hasattr(self, '_project'):
|
ensemble = self.get_queryset().get(slug=self.kwargs['ensemble'])
|
||||||
if self.request.is_admin: # can access any ensemble
|
|
||||||
self._project = get_object_or_404(models.Project, pk=self.kwargs['project'])
|
self.request.is_admin = ensemble.has_admin(self.request.user)
|
||||||
else:
|
|
||||||
self._project = get_object_or_404(models.Project,
|
return ensemble
|
||||||
pk=self.kwargs['project'], ensemble=self.request.ensemble_id)
|
|
||||||
return self._project
|
def get_object(self):
|
||||||
|
return self.ensemble
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(project=self.get_project())
|
ensembles = models.Ensemble.objects.all()
|
||||||
|
|
||||||
|
if self.request.is_admin:
|
||||||
|
return ensembles
|
||||||
|
|
||||||
|
# limit to registered ensembles
|
||||||
|
f = Q(slug__in=self.get_authorized_keys('ensemble'))
|
||||||
|
|
||||||
|
# or ensembles where the user is admin
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
f |= Q(admins=self.request.user.pk)
|
||||||
|
|
||||||
|
return ensembles.filter(f)
|
||||||
|
|
||||||
|
class ProjectMixin(AuthorizedResourceMixin):
|
||||||
|
|
||||||
|
def is_authorized(self):
|
||||||
|
super().is_authorized()
|
||||||
|
try:
|
||||||
|
self.project = self.get_project()
|
||||||
|
return True
|
||||||
|
except models.Project.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_project(self):
|
||||||
|
project = self.get_project_queryset().get(pk=self.kwargs['project'])
|
||||||
|
self.request.is_admin = project.ensemble.has_admin(self.request.user)
|
||||||
|
return project
|
||||||
|
|
||||||
|
def get_project_queryset(self):
|
||||||
|
projects = models.Project.objects.select_related('ensemble')
|
||||||
|
if self.request.is_admin:
|
||||||
|
return projects
|
||||||
|
|
||||||
|
f = Q(pk__in=self.get_authorized_keys('project')) | Q(ensemble__slug__in=self.get_authorized_keys('ensemble'))
|
||||||
|
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
f |= Q(ensemble__admins=self.request.user.pk)
|
||||||
|
else:
|
||||||
|
f &= Q(active=True)
|
||||||
|
|
||||||
|
return projects.filter(f)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().filter(project=self.project)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['project'] = self.get_project()
|
if 'project' in self.kwargs:
|
||||||
context['modules'] = context['project'].modules.values_list('name', flat=True)
|
context['project'] = self.project
|
||||||
|
context['modules'] = self.project.active_modules
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def register(request):
|
class CrispyFormMixin(object):
|
||||||
|
|
||||||
if 'clear' in request.GET:
|
cancel_url = None
|
||||||
request.session.clear()
|
|
||||||
|
|
||||||
request.ensemble_id = request.session.get('ensemble')
|
def get_cancel_url(self):
|
||||||
registered = request.session.setdefault('registered', [])
|
return self.cancel_url
|
||||||
|
|
||||||
code = request.GET.get('code', '').replace('-', '')
|
""" ENSEMBLE VIEWS """
|
||||||
logger.debug("Registering with code %s", code)
|
|
||||||
|
|
||||||
# check if already joined
|
class EnsembleListView(EnsembleMixin, ListView):
|
||||||
if code in registered or request.user.is_superuser:
|
model = models.Ensemble
|
||||||
request.session['ensemble'] = models.Ensemble.objects.get(code=code).pk
|
|
||||||
return redirect('ensemble_detail')
|
|
||||||
|
|
||||||
if request.method == "POST":
|
def is_authorized(self):
|
||||||
form = forms.CodeForm(request.POST)
|
return True
|
||||||
|
|
||||||
if form.is_valid():
|
class EnsembleDetailView(EnsembleMixin, DetailView):
|
||||||
|
|
||||||
data = form.cleaned_data
|
def on_signed_request(self):
|
||||||
try:
|
self.add_authorized_key('ensemble', self.kwargs['ensemble'])
|
||||||
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):
|
def get_context_data(self, **kwargs):
|
||||||
data = super().get_context_data(**kwargs)
|
data = super().get_context_data(**kwargs)
|
||||||
|
data['inactive'] = 'inactive' in self.request.GET
|
||||||
|
if data['inactive']:
|
||||||
|
data['object_list'] = self.object.projects.all().order_by('-pk')
|
||||||
|
else:
|
||||||
|
data['object_list'] = self.object.active_projects()
|
||||||
if self.request.is_admin:
|
if self.request.is_admin:
|
||||||
data['ensemble_url'] = self.request.build_absolute_uri('/?code={0}'.format(self.ensemble.ensemble_code()))
|
data['ensemble_link'] = signed_url(self.request.path)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class EnsembleDetailView(DetailView):
|
""" PROJECT VIEWS """
|
||||||
model = models.Ensemble
|
|
||||||
|
|
||||||
class ProjectDetailView(ProjectMixin, DetailView):
|
class ProjectListView(ProjectMixin, ListView):
|
||||||
|
|
||||||
def get_object(self):
|
def is_authorized(self):
|
||||||
return self.get_project()
|
return True
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_project_queryset().filter(active=True, event_date__gte=timezone.now()-timezone.timedelta(7))
|
||||||
|
|
||||||
class ProjectCreateView(EnsembleMixin, CreateView):
|
class ProjectCreateView(EnsembleMixin, CreateView):
|
||||||
|
admin_required = True
|
||||||
model = models.Project
|
model = models.Project
|
||||||
template_name = "interface/default_form.html"
|
template_name = "interface/default_form.html"
|
||||||
title = "Add a new project"
|
title = "Add a new project"
|
||||||
@ -217,21 +207,41 @@ class ProjectCreateView(EnsembleMixin, CreateView):
|
|||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save(commit=False)
|
self.object = form.save(commit=False)
|
||||||
self.object.ensemble_id = self.request.ensemble_id
|
self.object.ensemble_id = self.kwargs['pk']
|
||||||
self.object.owner = self.request.user
|
self.object.owner = self.request.user
|
||||||
self.object.save()
|
self.object.save()
|
||||||
return redirect('project_detail', project=self.object.pk)
|
return redirect('project_detail', project=self.object.pk)
|
||||||
|
|
||||||
class ProjectUpdateView(EnsembleMixin, UpdateView):
|
class ProjectDetailView(ProjectMixin, DetailView):
|
||||||
model = models.Project
|
|
||||||
|
def on_signed_request(self):
|
||||||
|
self.add_authorized_key('project', self.kwargs['project'])
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
return self.project
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
data = super().get_context_data(**kwargs)
|
||||||
|
data['project_link'] = signed_url(self.request.path)
|
||||||
|
return data
|
||||||
|
|
||||||
|
class ProjectUpdateView(ProjectMixin, UpdateView):
|
||||||
|
admin_required = True
|
||||||
template_name = "interface/default_form.html"
|
template_name = "interface/default_form.html"
|
||||||
#fields = ['name', 'description', 'event_date', 'enable_library', 'enable_submissions', 'active']
|
|
||||||
pk_url_kwarg = 'project'
|
pk_url_kwarg = 'project'
|
||||||
form_class = forms.ProjectForm
|
form_class = forms.ProjectForm
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.project
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cancel_url(self):
|
||||||
|
return self.get_success_url()
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return resolve_url('project_detail', project=self.kwargs['project'])
|
return resolve_url('project_detail', project=self.kwargs['project'])
|
||||||
|
|
||||||
|
# Old Makefile from submission module
|
||||||
#class ProjectMakefileView(EnsembleMixin, DetailView):
|
#class ProjectMakefileView(EnsembleMixin, DetailView):
|
||||||
# template_name = 'interface/project_submissions.mk'
|
# template_name = 'interface/project_submissions.mk'
|
||||||
# content_type = 'text/plain'
|
# content_type = 'text/plain'
|
||||||
@ -257,6 +267,8 @@ class ProjectUpdateView(EnsembleMixin, UpdateView):
|
|||||||
#
|
#
|
||||||
# return data
|
# return data
|
||||||
|
|
||||||
|
""" WIKI VIEWS """
|
||||||
|
|
||||||
class WikiView(ProjectMixin, DetailView):
|
class WikiView(ProjectMixin, DetailView):
|
||||||
template_name = 'interface/wiki.html'
|
template_name = 'interface/wiki.html'
|
||||||
model = models.WikiPage
|
model = models.WikiPage
|
||||||
@ -269,30 +281,38 @@ class WikiView(ProjectMixin, DetailView):
|
|||||||
class WikiCreateView(ProjectMixin, CreateView):
|
class WikiCreateView(ProjectMixin, CreateView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
model = models.WikiPage
|
model = models.WikiPage
|
||||||
fields = ['title', 'markdown']
|
template_name = 'interface/default_form.html'
|
||||||
|
form_class = forms.WikiForm
|
||||||
|
|
||||||
|
def cancel_url(self):
|
||||||
|
return resolve_url('project_detail', self.kwargs['project'])
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save(commit=False)
|
self.object = form.save(commit=False)
|
||||||
self.object.project = self.get_project()
|
self.object.project = self.project
|
||||||
self.object.save()
|
self.object.save()
|
||||||
return redirect('wiki', project=self.object.project_id, pk=self.object.pk)
|
return redirect('wiki', project=self.object.project_id, pk=self.object.pk)
|
||||||
|
|
||||||
class WikiEditView(ProjectMixin, UpdateView):
|
class WikiEditView(ProjectMixin, UpdateView):
|
||||||
admin_required = True
|
admin_required = True
|
||||||
model = models.WikiPage
|
model = models.WikiPage
|
||||||
fields = ['title', 'markdown']
|
form_class = forms.WikiForm
|
||||||
|
|
||||||
|
def cancel_url(self):
|
||||||
|
return resolve_url('wiki', self.kwargs['project'], self.kwargs['pk'])
|
||||||
|
|
||||||
|
""" RESOURCE VIEWS """
|
||||||
|
|
||||||
class ResourceCreateView(ProjectMixin, CreateView):
|
class ResourceCreateView(ProjectMixin, CreateView):
|
||||||
|
admin_required = True
|
||||||
model = models.Resource
|
model = models.Resource
|
||||||
form_class = forms.ResourceForm
|
form_class = forms.ResourceForm
|
||||||
template_name = 'interface/project_form.html'
|
template_name = 'interface/default_form.html'
|
||||||
title = "Add a new resource"
|
title = "Add a new resource"
|
||||||
admin_required = True
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.object = form.save(commit=False)
|
self.object = form.save(commit=False)
|
||||||
self.object.project = self.get_project()
|
self.object.project = self.project
|
||||||
self.object.save()
|
self.object.save()
|
||||||
return redirect('resource_upload', project=self.object.project_id, pk=self.object.pk)
|
return redirect('resource_upload', project=self.object.project_id, pk=self.object.pk)
|
||||||
|
|
||||||
@ -322,13 +342,3 @@ class ResourceEditView(ProjectMixin, UpdateView):
|
|||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return resolve_url('resource_list', project=self.kwargs['project'])
|
return resolve_url('resource_list', project=self.kwargs['project'])
|
||||||
|
|
||||||
class 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
|
|
||||||
Loading…
x
Reference in New Issue
Block a user