Compare commits
53 Commits
Author | SHA1 | Date |
---|---|---|
|
d319559272 | |
|
1b264054fc | |
|
c5b39e1890 | |
|
97a1dbf33a | |
|
27e0b1e2c5 | |
|
ec3acac3fc | |
|
d5e3a35056 | |
|
9accf9714e | |
|
2d7efeaf09 | |
|
50d189d8ed | |
|
f8f95db5c3 | |
|
dcce8e5407 | |
|
660650c8da | |
|
18af2a75e6 | |
|
01f5434065 | |
|
e7df05566f | |
|
cabf4538d2 | |
|
81f1d44700 | |
|
1c8047a006 | |
|
6823e11ff0 | |
|
a693370a7d | |
|
5b508bf61b | |
|
d4a1a4738f | |
|
91acd4e855 | |
|
83409ef2c8 | |
|
353213b1c8 | |
|
a67cc96935 | |
|
fdf9110875 | |
|
c7d67d97f6 | |
|
8b1b807ef3 | |
|
e3a82cb705 | |
|
03b5a6ebf4 | |
|
5df8a8b29b | |
|
ca3ca52ab2 | |
|
d709f020f4 | |
|
f0caef1cca | |
|
f3b5473cd3 | |
|
122c120eae | |
|
23821c523d | |
|
1a3382614a | |
|
b99264647d | |
|
48216a8862 | |
|
d2ca7db7aa | |
|
57317b51d2 | |
|
3091fb0783 | |
|
14df5d5b68 | |
|
a0ccd2ff8e | |
|
049062d5bb | |
|
2bb22a0e78 | |
|
31c3c820fd | |
|
b6d35d152c | |
|
41f5fa840b | |
|
fac4f8ba56 |
|
@ -17,7 +17,7 @@ steps:
|
||||||
export PATH="$$DRONE_WORKSPACE/.venv/bin:$$PATH"
|
export PATH="$$DRONE_WORKSPACE/.venv/bin:$$PATH"
|
||||||
- echo $$PATH
|
- echo $$PATH
|
||||||
- which pip
|
- which pip
|
||||||
- pip install black build flake8 isort twine
|
- pip install black build Django flake8 isort twine
|
||||||
|
|
||||||
- name: check style
|
- name: check style
|
||||||
image: python:3.11-alpine
|
image: python:3.11-alpine
|
||||||
|
@ -31,6 +31,10 @@ steps:
|
||||||
image: python:3.11-alpine
|
image: python:3.11-alpine
|
||||||
commands:
|
commands:
|
||||||
- *path
|
- *path
|
||||||
|
- apk --update-cache add gettext
|
||||||
|
- cd pretalx_musicrate
|
||||||
|
- django-admin compilemessages
|
||||||
|
- cd ..
|
||||||
- python -m build
|
- python -m build
|
||||||
|
|
||||||
- name: publish
|
- name: publish
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*.{html,js}]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright 2023 Luca
|
Copyright 2023-2024 Luca Schmid
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -40,7 +40,7 @@ black .
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright 2023 Luca Schmid
|
Copyright 2023-2024 Luca Schmid
|
||||||
|
|
||||||
Released under the terms of the Apache License 2.0
|
Released under the terms of the Apache License 2.0
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.5.0"
|
__version__ = "2025.6.1"
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
|
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
|
||||||
from i18nfield.forms import I18nModelForm
|
from i18nfield.forms import I18nModelForm
|
||||||
|
from pretalx.person.models import User
|
||||||
|
from pretalx.submission.forms import SubmissionFilterForm
|
||||||
|
|
||||||
from .models import MusicrateSettings, Rating
|
from .models import Assignee, MusicrateSettings, Rating
|
||||||
|
|
||||||
|
|
||||||
class MusicrateSettingsForm(I18nModelForm):
|
class MusicrateSettingsForm(I18nModelForm):
|
||||||
|
@ -34,12 +37,6 @@ class MusicrateSettingsForm(I18nModelForm):
|
||||||
"link_questions",
|
"link_questions",
|
||||||
"advance_threshold",
|
"advance_threshold",
|
||||||
)
|
)
|
||||||
widgets = {
|
|
||||||
"submission_types": forms.SelectMultiple(attrs={"class": "select2"}),
|
|
||||||
"genre_question": forms.Select(attrs={"class": "select2"}),
|
|
||||||
"origin_question": forms.Select(attrs={"class": "select2"}),
|
|
||||||
"link_questions": forms.SelectMultiple(attrs={"class": "select2"}),
|
|
||||||
}
|
|
||||||
field_classes = {
|
field_classes = {
|
||||||
"submission_types": SafeModelMultipleChoiceField,
|
"submission_types": SafeModelMultipleChoiceField,
|
||||||
"genre_question": SafeModelChoiceField,
|
"genre_question": SafeModelChoiceField,
|
||||||
|
@ -56,3 +53,31 @@ class RatingForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rating
|
model = Rating
|
||||||
fields = ("rating",)
|
fields = ("rating",)
|
||||||
|
|
||||||
|
|
||||||
|
class AssigneeForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, submission=None, **kwargs):
|
||||||
|
try:
|
||||||
|
self.instance = Assignee.objects.get(submission=submission)
|
||||||
|
except Assignee.DoesNotExist:
|
||||||
|
self.instance = Assignee(submission=submission)
|
||||||
|
super().__init__(*args, instance=self.instance, **kwargs)
|
||||||
|
self.fields["user"].queryset = User.objects.filter(
|
||||||
|
teams__in=submission.event.teams, teams__can_change_submissions=True
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Assignee
|
||||||
|
fields = ("user",)
|
||||||
|
widgets = {"user": forms.Select(attrs={"class": "select2"})}
|
||||||
|
|
||||||
|
|
||||||
|
class EnhancedSubmissionFilterForm(SubmissionFilterForm):
|
||||||
|
require_all_tags = forms.BooleanField(required=False, label=_("require all"))
|
||||||
|
|
||||||
|
def filter_queryset(self, qs):
|
||||||
|
qs = super().filter_queryset(qs)
|
||||||
|
if self.cleaned_data.get("require_all_tags"):
|
||||||
|
for tag in self.cleaned_data.get("tags"):
|
||||||
|
qs = qs.filter(tags__in=[tag])
|
||||||
|
return qs
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# pretalx plugin for rating music.
|
# pretalx plugin for rating music.
|
||||||
# Copyright (C) 2023
|
# Copyright (C) 2023-2024
|
||||||
# This file is distributed under the same license as the pretalx_musicrate package.
|
# This file is distributed under the same license as the pretalx_musicrate package.
|
||||||
# Luca Schmid <Luca@hackerspace-bamberg.de>, 2023.
|
# Luca Schmid <Luca@hackerspace-bamberg.de>, 2023-2024.
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: pretalx-musicrate 0.0.1\n"
|
"Project-Id-Version: pretalx-musicrate 0.9.0\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2023-12-16 03:23+0100\n"
|
"POT-Creation-Date: 2024-12-09 22:22+0100\n"
|
||||||
"PO-Revision-Date: 2023-12-16 03:24+0100\n"
|
"PO-Revision-Date: 2024-12-09 22:23+0100\n"
|
||||||
"Last-Translator: Luca <Luca@hackerspace-bamberg.de>\n"
|
"Last-Translator: Luca <Luca@hackerspace-bamberg.de>\n"
|
||||||
"Language-Team: Luca <Luca@hackerspace-bamberg.de>\n"
|
"Language-Team: Luca <Luca@hackerspace-bamberg.de>\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
|
@ -16,9 +16,9 @@ msgstr ""
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
"X-Generator: Poedit 3.4.1\n"
|
"X-Generator: Poedit 3.4.2\n"
|
||||||
|
|
||||||
#: pretalx_musicrate/apps.py:12
|
#: pretalx_musicrate/apps.py:12 pretalx_musicrate/signals.py:59
|
||||||
msgid "pretalx-musicrate"
|
msgid "pretalx-musicrate"
|
||||||
msgstr "pretalx-musicrate"
|
msgstr "pretalx-musicrate"
|
||||||
|
|
||||||
|
@ -26,6 +26,10 @@ msgstr "pretalx-musicrate"
|
||||||
msgid "pretalx plugin for rating music"
|
msgid "pretalx plugin for rating music"
|
||||||
msgstr "pretalx-Plugin zur Bewertung von Musik"
|
msgstr "pretalx-Plugin zur Bewertung von Musik"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/forms.py:79
|
||||||
|
msgid "require all"
|
||||||
|
msgstr "alle voraussetzen"
|
||||||
|
|
||||||
#: pretalx_musicrate/models.py:23
|
#: pretalx_musicrate/models.py:23
|
||||||
msgid ""
|
msgid ""
|
||||||
"You can limit pretalx-musicrate to some session types. Leave this field "
|
"You can limit pretalx-musicrate to some session types. Leave this field "
|
||||||
|
@ -62,10 +66,103 @@ msgstr "Weiter-Schwellenwert"
|
||||||
msgid "Link Questions"
|
msgid "Link Questions"
|
||||||
msgstr "Fragen zu Links"
|
msgstr "Fragen zu Links"
|
||||||
|
|
||||||
#: pretalx_musicrate/signals.py:16
|
#: pretalx_musicrate/signals.py:21
|
||||||
|
msgid "The name of the submission's assignee"
|
||||||
|
msgstr "Der Name der für die Einreichung zuständigen Person"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/signals.py:35
|
||||||
|
msgid "Sessions, but better"
|
||||||
|
msgstr "Vorträge, aber besser"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/signals.py:45
|
||||||
msgid "Collective Rating"
|
msgid "Collective Rating"
|
||||||
msgstr "Anhörtag"
|
msgstr "Anhörtag"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/assignee.html:5
|
||||||
|
#, python-format
|
||||||
|
msgid "Assignee for %(quotation_open)s%(title)s%(quotation_close)s"
|
||||||
|
msgstr "Zuständige*r für %(quotation_open)s%(title)s%(quotation_close)s"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:43
|
||||||
|
msgid "proposal"
|
||||||
|
msgid_plural "proposals"
|
||||||
|
msgstr[0] "Einreichung"
|
||||||
|
msgstr[1] "Einreichungen"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:72
|
||||||
|
msgid "Search"
|
||||||
|
msgstr "Suche"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:77
|
||||||
|
#, python-format
|
||||||
|
msgid "List filtered by answers to question \"%(question)s\"."
|
||||||
|
msgstr "Liste gefiltert nach Antworten zur Frage \"%(question)s\"."
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:82
|
||||||
|
msgid "Remove filter"
|
||||||
|
msgstr "Filter entfernen"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:93
|
||||||
|
msgid "Rating"
|
||||||
|
msgstr "Bewertung"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:94
|
||||||
|
msgid "Sort by rating (0-10)"
|
||||||
|
msgstr "Nach Bewertung sortieren (0-10)"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:95
|
||||||
|
msgid "Sort by rating (10-0)"
|
||||||
|
msgstr "Nach Bewertung sortieren (10-0)"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:98
|
||||||
|
msgid "Title"
|
||||||
|
msgstr "Titel"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:99
|
||||||
|
msgid "Sort by title (a-z)"
|
||||||
|
msgstr "Nach Titel sortieren (a-z)"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:100
|
||||||
|
msgid "Sort by title (z-a)"
|
||||||
|
msgstr "Nach Titel sortieren (z-a)"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:104
|
||||||
|
msgid "Type"
|
||||||
|
msgstr "Typ"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:108
|
||||||
|
msgid "State"
|
||||||
|
msgstr "Status"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:109
|
||||||
|
msgid "Sort by state (a-z)"
|
||||||
|
msgstr "Nach Status sortieren (a-z)"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:110
|
||||||
|
msgid "Sort by state (z-a)"
|
||||||
|
msgstr "Nach Status sortieren (z-a)"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:113
|
||||||
|
msgid "Assignee"
|
||||||
|
msgstr "Zuständige*r"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:114
|
||||||
|
msgid "Sort by assignee (a-z)"
|
||||||
|
msgstr "Nach Zuständiger*m sortieren (a-z)"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:115
|
||||||
|
msgid "Sort by assignee (z-a)"
|
||||||
|
msgstr "Nach Zuständiger*m sortieren (z-a)"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:154
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:162
|
||||||
|
msgid "edit"
|
||||||
|
msgstr "bearbeiten"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/enhanced_list.html:167
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr "Löschen"
|
||||||
|
|
||||||
#: pretalx_musicrate/templates/pretalx_musicrate/join.html:5
|
#: pretalx_musicrate/templates/pretalx_musicrate/join.html:5
|
||||||
msgid "Join collective rating"
|
msgid "Join collective rating"
|
||||||
msgstr "Beim Anhörtag mitmachen"
|
msgstr "Beim Anhörtag mitmachen"
|
||||||
|
@ -89,35 +186,43 @@ msgstr "Mitmach-QR-Code anzeigen"
|
||||||
msgid "Go to collective rating"
|
msgid "Go to collective rating"
|
||||||
msgstr "Zum Anhörtag"
|
msgstr "Zum Anhörtag"
|
||||||
|
|
||||||
#: pretalx_musicrate/templates/pretalx_musicrate/settings.html:8
|
#: pretalx_musicrate/templates/pretalx_musicrate/settings.html:7
|
||||||
msgid "pretalx-musicrate settings"
|
msgid "pretalx-musicrate settings"
|
||||||
msgstr "pretalx-musicrate-Einstellungen"
|
msgstr "pretalx-musicrate-Einstellungen"
|
||||||
|
|
||||||
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:29
|
#: pretalx_musicrate/templates/pretalx_musicrate/settings.html:12
|
||||||
|
msgid "Export ratings"
|
||||||
|
msgstr "Bewertungen exportieren"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/settings.html:16
|
||||||
|
msgid "Export scores"
|
||||||
|
msgstr "Auswertung exportieren"
|
||||||
|
|
||||||
|
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:34
|
||||||
msgid "(not specified)"
|
msgid "(not specified)"
|
||||||
msgstr "(nicht angegeben)"
|
msgstr "(nicht angegeben)"
|
||||||
|
|
||||||
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:42
|
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:47
|
||||||
msgid "Previous"
|
msgid "Previous"
|
||||||
msgstr "Zurück"
|
msgstr "Zurück"
|
||||||
|
|
||||||
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:50
|
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:55
|
||||||
msgid "Next"
|
msgid "Next"
|
||||||
msgstr "Weiter"
|
msgstr "Weiter"
|
||||||
|
|
||||||
#: pretalx_musicrate/views.py:39
|
#: pretalx_musicrate/views.py:66
|
||||||
msgid "Invalid token"
|
msgid "Invalid token"
|
||||||
msgstr "Ungültiges Token"
|
msgstr "Ungültiges Token"
|
||||||
|
|
||||||
#: pretalx_musicrate/views.py:114
|
#: pretalx_musicrate/views.py:133
|
||||||
msgid "The pretalx-musicrate settings were updated."
|
msgid "The pretalx-musicrate settings were updated."
|
||||||
msgstr "Die pretalx-musicrate-Einstellungen wurden gespeichert."
|
msgstr "Die pretalx-musicrate-Einstellungen wurden gespeichert."
|
||||||
|
|
||||||
#: pretalx_musicrate/views.py:273
|
#: pretalx_musicrate/views.py:289 pretalx_musicrate/views.py:482
|
||||||
msgid "Saved!"
|
msgid "Saved!"
|
||||||
msgstr "Gespeichert!"
|
msgstr "Gespeichert!"
|
||||||
|
|
||||||
#: pretalx_musicrate/views.py:375
|
#: pretalx_musicrate/views.py:398
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(num_ratings)d of %(num_jurors)d has rated this submission"
|
msgid "%(num_ratings)d of %(num_jurors)d has rated this submission"
|
||||||
msgid_plural "%(num_ratings)d of %(num_jurors)d have rated this submission"
|
msgid_plural "%(num_ratings)d of %(num_jurors)d have rated this submission"
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import math
|
||||||
|
import statistics
|
||||||
|
from operator import attrgetter, itemgetter
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django_scopes import scope
|
||||||
|
from pretalx.event.models import Event
|
||||||
|
from pretalx.submission.models import Submission
|
||||||
|
|
||||||
|
from ...models import Rating, Score
|
||||||
|
|
||||||
|
RATINGS_MIN = 30
|
||||||
|
SCALE = list(map(float, map(itemgetter(0), Rating.RATING_CHOICES)))
|
||||||
|
|
||||||
|
MEAN = statistics.mean(SCALE)
|
||||||
|
STD = math.sqrt(statistics.mean([(i - MEAN) ** 2 for i in SCALE]))
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Compute submission scores from ratings"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"-a", "--all", action="store_true", help="include frozen jurors"
|
||||||
|
)
|
||||||
|
parser.add_argument("event")
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
event = Event.objects.get(slug=kwargs["event"])
|
||||||
|
except Event.DoesNotExist:
|
||||||
|
raise CommandError(f"no event found with slug '{kwargs['event']}'")
|
||||||
|
|
||||||
|
with scope(event=event):
|
||||||
|
submissions = {}
|
||||||
|
|
||||||
|
jurors = event.jurors.prefetch_related("ratings__submission").order_by(
|
||||||
|
"token"
|
||||||
|
)
|
||||||
|
if not kwargs["all"]:
|
||||||
|
jurors = jurors.filter(frozen=False)
|
||||||
|
|
||||||
|
self.stderr.write(f"computing scores from {jurors.count()} jurors")
|
||||||
|
|
||||||
|
for juror in jurors:
|
||||||
|
ratings = list(
|
||||||
|
juror.ratings.exclude(rating="").order_by("submission__created")
|
||||||
|
)
|
||||||
|
values = list(map(int, map(attrgetter("rating"), ratings)))
|
||||||
|
ratings = dict(
|
||||||
|
zip(
|
||||||
|
map(attrgetter("code"), map(attrgetter("submission"), ratings)),
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(values) < RATINGS_MIN:
|
||||||
|
mean = MEAN
|
||||||
|
std = STD
|
||||||
|
else:
|
||||||
|
mean = sum(values) / len(values)
|
||||||
|
std = math.sqrt(
|
||||||
|
sum([(i - mean) ** 2 for i in values]) / len(values)
|
||||||
|
)
|
||||||
|
|
||||||
|
for code in ratings:
|
||||||
|
if code not in submissions:
|
||||||
|
submissions[code] = []
|
||||||
|
submissions[code].append((ratings[code] - mean) / std * STD + MEAN)
|
||||||
|
|
||||||
|
for submission in Submission.objects.select_related("score").filter(
|
||||||
|
code__in=submissions.keys()
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
score = submission.score
|
||||||
|
except Submission.score.RelatedObjectDoesNotExist:
|
||||||
|
score = Score(submission=submission)
|
||||||
|
|
||||||
|
ratings = submissions[submission.code]
|
||||||
|
score.value = sum(ratings) / len(ratings)
|
||||||
|
score.save()
|
|
@ -1,7 +1,7 @@
|
||||||
# Generated by Django 4.2.8 on 2023-12-15 18:27
|
# Generated by Django 4.2.8 on 2023-12-15 18:27
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import pretalx.common.mixins.models
|
import pretalx.common.models.mixins
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
import pretalx_musicrate.models
|
import pretalx_musicrate.models
|
||||||
|
@ -51,8 +51,8 @@ class Migration(migrations.Migration):
|
||||||
"abstract": False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
bases=(
|
bases=(
|
||||||
pretalx.common.mixins.models.LogMixin,
|
pretalx.common.models.mixins.LogMixin,
|
||||||
pretalx.common.mixins.models.FileCleanupMixin,
|
pretalx.common.models.mixins.FileCleanupMixin,
|
||||||
models.Model,
|
models.Model,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Generated by Django 4.2.8 on 2024-02-12 21:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("submission", "0072_alter_reviewscore_label"),
|
||||||
|
("pretalx_musicrate", "0007_remove_juror_created_remove_juror_updated"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Assignee",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"submission",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="submission.submission",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="assigned_submissions",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 4.2.8 on 2024-02-14 00:36
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("submission", "0072_alter_reviewscore_label"),
|
||||||
|
("pretalx_musicrate", "0008_assignee"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Score",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("value", models.FloatField()),
|
||||||
|
(
|
||||||
|
"submission",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="submission.submission",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.1.5 on 2025-02-23 13:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretalx_musicrate", "0009_score"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="juror",
|
||||||
|
name="frozen",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -78,6 +78,7 @@ class Juror(models.Model):
|
||||||
related_name="jurors",
|
related_name="jurors",
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
frozen = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
class Rating(models.Model):
|
class Rating(models.Model):
|
||||||
|
@ -105,3 +106,15 @@ class Rating(models.Model):
|
||||||
fields=("submission", "juror"), name="unique_rating"
|
fields=("submission", "juror"), name="unique_rating"
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Assignee(models.Model):
|
||||||
|
submission = models.OneToOneField("submission.Submission", on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"person.User", on_delete=models.CASCADE, related_name="assigned_submissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Score(models.Model):
|
||||||
|
submission = models.OneToOneField("submission.Submission", on_delete=models.CASCADE)
|
||||||
|
value = models.FloatField()
|
||||||
|
|
|
@ -1,23 +1,52 @@
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from pretalx.mail.placeholders import SimpleFunctionalMailTextPlaceholder
|
||||||
|
from pretalx.mail.signals import register_mail_placeholders
|
||||||
from pretalx.orga.signals import nav_event, nav_event_settings
|
from pretalx.orga.signals import nav_event, nav_event_settings
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(register_mail_placeholders)
|
||||||
|
def pretalx_musicrate_placeholders(sender, **kwargs):
|
||||||
|
return [
|
||||||
|
SimpleFunctionalMailTextPlaceholder(
|
||||||
|
"assignee",
|
||||||
|
["submission"],
|
||||||
|
lambda submission: (
|
||||||
|
submission.assignee.user.name
|
||||||
|
if hasattr(submission, "assignee")
|
||||||
|
else "Günther"
|
||||||
|
),
|
||||||
|
"Günther",
|
||||||
|
_("The name of the submission's assignee"),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@receiver(nav_event)
|
@receiver(nav_event)
|
||||||
def pretalx_musicrate_qrcode(sender, request, **kwargs):
|
def pretalx_musicrate_nav_event(sender, request, **kwargs):
|
||||||
if not request.user.has_perm("orga.view_submissions", request.event):
|
if not request.user.has_perm("orga.view_submissions", request.event):
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"active": request.resolver_match.url_name
|
"active": request.resolver_match.view_name
|
||||||
|
== "plugins:pretalx_musicrate:enhanced_list",
|
||||||
|
"icon": "sticky-note-o",
|
||||||
|
"label": _("Sessions, but better"),
|
||||||
|
"url": reverse(
|
||||||
|
"plugins:pretalx_musicrate:enhanced_list",
|
||||||
|
kwargs={"event": request.event.slug},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"active": request.resolver_match.view_name
|
||||||
== "plugins:pretalx_musicrate:qrcode",
|
== "plugins:pretalx_musicrate:qrcode",
|
||||||
"icon": "star",
|
"icon": "star",
|
||||||
"label": _("Collective Rating"),
|
"label": _("Collective Rating"),
|
||||||
"url": reverse(
|
"url": reverse(
|
||||||
"plugins:pretalx_musicrate:qrcode", kwargs={"event": request.event.slug}
|
"plugins:pretalx_musicrate:qrcode", kwargs={"event": request.event.slug}
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,12 +56,12 @@ def pretalx_musicrate_settings(sender, request, **kwargs):
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"label": "pretalx-musicrate",
|
"label": _("pretalx-musicrate"),
|
||||||
"url": reverse(
|
"url": reverse(
|
||||||
"plugins:pretalx_musicrate:settings",
|
"plugins:pretalx_musicrate:settings.musicrate",
|
||||||
kwargs={"event": request.event.slug},
|
kwargs={"event": request.event.slug},
|
||||||
),
|
),
|
||||||
"active": request.resolver_match.url_name
|
"active": request.resolver_match.view_name
|
||||||
== "plugins:pretalx_musicrate:settings",
|
== "plugins:pretalx_musicrate:settings.musicrate",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -33,12 +33,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-container > .rating > input[type=radio]:checked ~ .rating-point {
|
.rating-container > .rating > input[type=radio]:checked ~ .rating-point {
|
||||||
background-image: url(point-fill.svg);
|
background-image: url(static('pretalx_musicrate/point-fill.svg'));
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-container > .rating > .rating-point {
|
.rating-container > .rating > .rating-point {
|
||||||
--max-size: calc((100vw - 2em - 2 * 0.1em * var(--num-choices)) / var(--num-choices));
|
--max-size: calc((100vw - 2em - 2 * 0.1em * var(--num-choices)) / var(--num-choices));
|
||||||
background: url(point-stroke.svg) no-repeat center/100%;
|
background: url(static('pretalx_musicrate/point-stroke.svg')) no-repeat center/100%;
|
||||||
height: 3em;
|
height: 3em;
|
||||||
margin: 0 0.1em;
|
margin: 0 0.1em;
|
||||||
max-height: var(--max-size);
|
max-height: var(--max-size);
|
|
@ -0,0 +1,14 @@
|
||||||
|
onReady(() => {
|
||||||
|
const updateRequireAllVisibility = () => {
|
||||||
|
if (document.querySelector("#id_tags").value) {
|
||||||
|
document.querySelector("#requireAll").classList.remove("d-none")
|
||||||
|
} else {
|
||||||
|
document.querySelector("#requireAll").classList.add("d-none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector("#id_tags")
|
||||||
|
.addEventListener("change", updateRequireAllVisibility)
|
||||||
|
updateRequireAllVisibility()
|
||||||
|
})
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "orga/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% blocktranslate with title=submission.title %}Assignee for {{ quotation_open }}{{ title }}{{ quotation_close }}{% endblocktranslate %}</h2>
|
||||||
|
{% include "orga/includes/base_form.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
{% extends "orga/base.html" %}
|
||||||
|
{% load compress %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load rules %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{% compress js %}
|
||||||
|
<script defer src="{% static "orga/js/submission_filter.js" %}"></script>
|
||||||
|
{% endcompress %}
|
||||||
|
{% compress js %}
|
||||||
|
<script defer src="{% static "pretalx_musicrate/submission_filter.js" %}"></script>
|
||||||
|
{% endcompress %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
{% compress css %}
|
||||||
|
<style>
|
||||||
|
.search-form {
|
||||||
|
#requireAll .form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#requireAll label {
|
||||||
|
display: inline-block;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#requireAll .form-group-inline {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endcompress %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% has_perm 'orga.change_submission_state' request.user request.event as can_change_submission %}
|
||||||
|
{% has_perm 'orga.view_speakers' request.user request.event as can_view_speakers %}
|
||||||
|
<h2>
|
||||||
|
{{ page_obj.paginator.count }}
|
||||||
|
{% blocktranslate trimmed count count=page_obj.paginator.count %}
|
||||||
|
proposal
|
||||||
|
{% plural %}
|
||||||
|
proposals
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="submit-group search-submit-group">
|
||||||
|
<form class="search-form">
|
||||||
|
{{ filter_form.q.as_field_group }}
|
||||||
|
{% if show_submission_types and filter_form.submission_type %}{{ filter_form.submission_type.as_field_group }}{% endif %}
|
||||||
|
<div class="d-flex flex-column form-group">
|
||||||
|
{{ filter_form.state.as_field_group }}
|
||||||
|
<div id="pending" class="ml-1 d-none">{{ filter_form.pending_state__isnull.as_field_group }}</div>
|
||||||
|
</div>
|
||||||
|
{% if filter_form.track %}{{ filter_form.track.as_field_group }}{% endif %}
|
||||||
|
{% if filter_form.tags %}
|
||||||
|
<div class="d-flex flex-column form-group">
|
||||||
|
{{ filter_form.tags.as_field_group }}
|
||||||
|
<div id="requireAll" class="ml-1 d-none">{{ filter_form.require_all_tags.as_field_group }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if filter_form.content_locale %}{{ filter_form.content_locale.as_field_group }}{% endif %}
|
||||||
|
{# These fields are hidden, but included to keep question search intact #}
|
||||||
|
{% if request.GET.question %} <input type="hidden" name="question" value="{{ request.GET.question }}"> {% endif %}
|
||||||
|
{% if request.GET.answer__options %} <input type="hidden" name="answer__options" value="{{ request.GET.answer__options }}"> {% endif %}
|
||||||
|
{% if request.GET.answer %} <input type="hidden" name="answer" value="{{ request.GET.answer }}"> {% endif %}
|
||||||
|
{% if request.GET.unanswered %} <input type="hidden" name="unanswered" value="{{ request.GET.unanswered }}"> {% endif %}
|
||||||
|
|
||||||
|
<button class="btn btn-success" type="submit">{% translate "Search" %}</button>
|
||||||
|
</form>
|
||||||
|
{% if filter_form.is_valid and filter_form.cleaned_data.question %}
|
||||||
|
<p class="text-muted ml-2">
|
||||||
|
<span class="fa fa-filter"></span>
|
||||||
|
{% blocktranslate trimmed with question=filter_form.cleaned_data.question.question %}
|
||||||
|
List filtered by answers to question "{{ question }}".
|
||||||
|
{% endblocktranslate %}
|
||||||
|
<a href="{% querystring question="" answer="" answer__options="" %}" class="text-muted">
|
||||||
|
<span class="fa fa-times"></span>
|
||||||
|
{% translate "Remove filter" %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover table-flip">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{% translate "Rating" %}
|
||||||
|
<a href="{% querystring sort="score__value" %}"><i class="fa fa-caret-down" title="{% translate "Sort by rating (0-10)" %}"></i></a>
|
||||||
|
<a href="{% querystring sort="-score__value" %}"><i class="fa fa-caret-up" title="{% translate "Sort by rating (10-0)" %}"></i></a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% translate "Title" %}
|
||||||
|
<a href="?{% querystring sort="title" %}"><i class="fa fa-caret-down" title="{% translate "Sort by title (a-z)" %}"></i></a>
|
||||||
|
<a href="?{% querystring sort="-title" %}"><i class="fa fa-caret-up" title="{% translate "Sort by title (z-a)" %}"></i></a>
|
||||||
|
</th>
|
||||||
|
{% if show_submission_types %}
|
||||||
|
<th>
|
||||||
|
{% translate "Type" %}
|
||||||
|
</th>
|
||||||
|
{% endif %}
|
||||||
|
<th>
|
||||||
|
{% translate "State" %}
|
||||||
|
<a href="?{% querystring sort="state" %}"><i class="fa fa-caret-down" title="{% translate "Sort by state (a-z)" %}"></i></a>
|
||||||
|
<a href="?{% querystring sort="-state" %}"><i class="fa fa-caret-up" title="{% translate "Sort by state (z-a)" %}"></i></a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% translate "Assignee" %}
|
||||||
|
<a href="?{% querystring sort="assignee" %}"><i class="fa fa-caret-down" title="{% translate "Sort by assignee (a-z)" %}"></i></a>
|
||||||
|
<a href="?{% querystring sort="-assignee" %}"><i class="fa fa-caret-up" title="{% translate "Sort by assignee (z-a)" %}"></i></a>
|
||||||
|
</th>
|
||||||
|
{% if can_change_submission %}
|
||||||
|
<th></th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for submission in submissions %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if submission.score %}
|
||||||
|
{{ submission.score.value }}
|
||||||
|
{% else %}
|
||||||
|
–
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ submission.orga_urls.base }}">
|
||||||
|
{% if can_view_speakers %}{{ submission.title }}{% else %}{{ submission.anonymised.title|default:submission.title }}{% endif %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% if show_submission_types %}
|
||||||
|
<td>
|
||||||
|
{{ submission.submission_type.name }}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>
|
||||||
|
{% include "orga/submission/state_dropdown.html" with submission=submission %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if submission.assignee %}
|
||||||
|
{{ submission.assignee.user.name }}
|
||||||
|
{% else %}
|
||||||
|
–
|
||||||
|
{% endif %}
|
||||||
|
{% if can_change_submission %}
|
||||||
|
<a class="btn btn-sm btn-link"
|
||||||
|
href="{% url "plugins:pretalx_musicrate:assignee" event=request.event.slug code=submission.code %}"
|
||||||
|
title="{% translate "edit" %}">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% if can_change_submission %}
|
||||||
|
<td class="action-column">
|
||||||
|
<a href="{{ submission.orga_urls.edit }}"
|
||||||
|
title="{% translate "edit" %}"
|
||||||
|
class="btn btn-sm btn-info">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ submission.orga_urls.delete }}?from=list"
|
||||||
|
title="{% translate "Delete" %}"
|
||||||
|
class="btn btn-sm btn-danger">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "orga/includes/pagination.html" %}
|
||||||
|
{% endblock %}
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
{% block submission_header %}
|
{% block submission_header %}
|
||||||
{% compress css %}
|
{% compress css %}
|
||||||
<link rel="stylesheet" href="{% static "pretalx_musicrate/rating.css" %}">
|
<link rel="stylesheet" type="text/x-scss" href="{% static "pretalx_musicrate/rating.scss" %}">
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script defer src="{% static "pretalx_musicrate/rating.js" %}"></script>
|
<script defer src="{% static "pretalx_musicrate/rating.js" %}"></script>
|
||||||
|
|
|
@ -1,27 +1,18 @@
|
||||||
{% extends "orga/base.html" %}
|
{% extends "orga/base.html" %}
|
||||||
{% load bootstrap4 %}
|
|
||||||
{% load compress %}
|
{% load compress %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{% translate "pretalx-musicrate settings" %}</h2>
|
<h2>{% translate "pretalx-musicrate settings" %}</h2>
|
||||||
<form method="post">
|
{% include "orga/includes/base_form.html" %}
|
||||||
{% csrf_token %}
|
<hr>
|
||||||
{% bootstrap_form_errors form %}
|
<a class="btn btn-success btn-lg btn-block" href="{% url "plugins:pretalx_musicrate:export" event=request.event.slug %}">
|
||||||
{% bootstrap_field form.submission_types layout='event' %}
|
<i class="fa fa-download"></i>
|
||||||
{% bootstrap_field form.genre_question layout='event' %}
|
{% translate "Export ratings" %}
|
||||||
{% bootstrap_field form.origin_question layout='event' %}
|
</a>
|
||||||
{% bootstrap_field form.link_questions layout='event' %}
|
<a class="btn btn-success btn-lg btn-block" href="{% url "plugins:pretalx_musicrate:scores" event=request.event.slug %}">
|
||||||
{% bootstrap_field form.advance_threshold layout='event' %}
|
<i class="fa fa-download"></i>
|
||||||
<div class="submit-group panel">
|
{% translate "Export scores" %}
|
||||||
<span></span>
|
</a>
|
||||||
<span>
|
|
||||||
<button type="submit" class="btn btn-success btn-lg">
|
|
||||||
<i class="fa fa-check"></i>
|
|
||||||
{% translate "Save" %}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
{% extends "cfp/event/base.html" %}
|
{% extends "cfp/event/base.html" %}
|
||||||
{% load compress %}
|
{% load compress %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}{{ submission.title }} ::{% endblock %}
|
{% block title %}{{ submission.title }} ::{% endblock %}
|
||||||
|
|
||||||
{% block cfp_header %}
|
{% block stylesheets %}
|
||||||
{% compress css %}
|
{% compress css %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "common/css/_forms.css" %}" />
|
||||||
<style>
|
<style>
|
||||||
.musicrate-pagination {
|
.musicrate-pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -21,6 +23,9 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block cfp_header %}
|
||||||
{% block submission_header %}
|
{% block submission_header %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -29,7 +34,7 @@
|
||||||
{% translate "(not specified)" as not_specified %}
|
{% translate "(not specified)" as not_specified %}
|
||||||
<h1>{{ submission.title }}</h1>
|
<h1>{{ submission.title }}</h1>
|
||||||
<p>
|
<p>
|
||||||
{{ genre|default:not_specified }} · {{ origin|default:not_specified }}
|
{{ submission.submission_type.name }} · {{ genre|default:not_specified }} · {{ origin|default:not_specified }}
|
||||||
{% if submission.internal_notes %}
|
{% if submission.internal_notes %}
|
||||||
<br>
|
<br>
|
||||||
{{ submission.internal_notes }}
|
{{ submission.internal_notes }}
|
||||||
|
|
|
@ -1,22 +1,49 @@
|
||||||
from django.urls import include, path
|
from django.urls import include, path, re_path
|
||||||
|
from pretalx.event.models.event import SLUG_REGEX
|
||||||
|
|
||||||
from .views import (
|
from .views import (
|
||||||
|
AssigneeView,
|
||||||
|
EnhancedSubmissionList,
|
||||||
|
ExportView,
|
||||||
JoinView,
|
JoinView,
|
||||||
MayAdvanceView,
|
MayAdvanceView,
|
||||||
MusicrateSettingsView,
|
MusicrateSettingsView,
|
||||||
PresenterView,
|
PresenterView,
|
||||||
QRCodeView,
|
QRCodeView,
|
||||||
RatingView,
|
RatingView,
|
||||||
|
ScoreExportView,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
re_path(
|
||||||
"orga/event/<slug:event>/settings/p/pretalx_musicrate/",
|
rf"^orga/event/(?P<event>{SLUG_REGEX})/",
|
||||||
MusicrateSettingsView.as_view(),
|
include(
|
||||||
name="settings",
|
[
|
||||||
|
path(
|
||||||
|
"settings/p/pretalx_musicrate/",
|
||||||
|
MusicrateSettingsView.as_view(),
|
||||||
|
name="settings.musicrate",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"p/pretalx_musicrate/",
|
||||||
|
include(
|
||||||
|
[
|
||||||
|
path("export/", ExportView.as_view(), name="export"),
|
||||||
|
path(
|
||||||
|
"list/",
|
||||||
|
EnhancedSubmissionList.as_view(),
|
||||||
|
name="enhanced_list",
|
||||||
|
),
|
||||||
|
path("scores/", ScoreExportView.as_view(), name="scores"),
|
||||||
|
path("<code>/", AssigneeView.as_view(), name="assignee"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
),
|
),
|
||||||
path(
|
re_path(
|
||||||
"<slug:event>/p/pretalx_musicrate/",
|
rf"^(?P<event>{SLUG_REGEX})/p/pretalx_musicrate/",
|
||||||
include(
|
include(
|
||||||
[
|
[
|
||||||
path("", QRCodeView.as_view(), name="qrcode"),
|
path("", QRCodeView.as_view(), name="qrcode"),
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import csv
|
||||||
import re
|
import re
|
||||||
from hmac import compare_digest
|
from hmac import compare_digest
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import JsonResponse
|
from django.db.models import Case, F, FilteredRelation, Q, Value, When
|
||||||
|
from django.http import HttpResponse, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
@ -12,9 +14,16 @@ from django.utils.translation import gettext_lazy as _, ngettext
|
||||||
from django.views.generic import FormView, TemplateView, View
|
from django.views.generic import FormView, TemplateView, View
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from django_context_decorator import context
|
from django_context_decorator import context
|
||||||
from pretalx.common.mixins.views import EventPermissionRequired
|
from pretalx.common.views.mixins import EventPermissionRequired
|
||||||
|
from pretalx.orga.views.submission import BaseSubmissionList, SubmissionList
|
||||||
|
from pretalx.submission.models import Submission, SubmissionStates
|
||||||
|
|
||||||
from .forms import MusicrateSettingsForm, RatingForm
|
from .forms import (
|
||||||
|
AssigneeForm,
|
||||||
|
EnhancedSubmissionFilterForm,
|
||||||
|
MusicrateSettingsForm,
|
||||||
|
RatingForm,
|
||||||
|
)
|
||||||
from .models import Juror, Rating
|
from .models import Juror, Rating
|
||||||
|
|
||||||
youtube_re = re.compile(
|
youtube_re = re.compile(
|
||||||
|
@ -22,12 +31,30 @@ youtube_re = re.compile(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_submission(settings, submissions, submission=None):
|
||||||
|
if submission is not None and submission.state != SubmissionStates.SUBMITTED:
|
||||||
|
submission = None
|
||||||
|
|
||||||
|
submission = submission or settings.last_submission
|
||||||
|
if submission is not None and submission.state != SubmissionStates.SUBMITTED:
|
||||||
|
submission = None
|
||||||
|
|
||||||
|
submissions = submissions.filter(
|
||||||
|
submission_type__in=settings.submission_types.all(),
|
||||||
|
state=SubmissionStates.SUBMITTED,
|
||||||
|
)
|
||||||
|
if submission is not None and not submissions.filter(pk=submission.pk).exists():
|
||||||
|
submission = None
|
||||||
|
|
||||||
|
return submission or submissions.order_by("created").first()
|
||||||
|
|
||||||
|
|
||||||
class JoinView(TemplateView):
|
class JoinView(TemplateView):
|
||||||
template_name = "pretalx_musicrate/join.html"
|
template_name = "pretalx_musicrate/join.html"
|
||||||
|
|
||||||
def validate_token(self, token):
|
def validate_token(self, token):
|
||||||
try:
|
try:
|
||||||
self.juror = Juror.objects.get(token=token)
|
self.juror = Juror.objects.get(token=token, frozen=False)
|
||||||
return True
|
return True
|
||||||
except Juror.DoesNotExist:
|
except Juror.DoesNotExist:
|
||||||
self.juror = None
|
self.juror = None
|
||||||
|
@ -48,14 +75,10 @@ class JoinView(TemplateView):
|
||||||
def get(self, request, *args, token, **kwargs):
|
def get(self, request, *args, token, **kwargs):
|
||||||
token_valid = self.validate_token(token)
|
token_valid = self.validate_token(token)
|
||||||
if self.juror is not None:
|
if self.juror is not None:
|
||||||
submission = (
|
submission = get_last_submission(
|
||||||
self.juror.last_submission
|
self.request.event.pretalx_musicrate_settings,
|
||||||
or self.request.event.pretalx_musicrate_settings.last_submission
|
self.request.event.submissions,
|
||||||
or self.request.event.submissions.filter(
|
self.juror.last_submission,
|
||||||
submission_type__in=self.request.event.pretalx_musicrate_settings.submission_types.all()
|
|
||||||
)
|
|
||||||
.order_by("created")
|
|
||||||
.first()
|
|
||||||
)
|
)
|
||||||
if submission is not None:
|
if submission is not None:
|
||||||
return redirect(
|
return redirect(
|
||||||
|
@ -74,14 +97,10 @@ class JoinView(TemplateView):
|
||||||
event=self.request.event,
|
event=self.request.event,
|
||||||
last_submission=self.request.event.pretalx_musicrate_settings.last_submission,
|
last_submission=self.request.event.pretalx_musicrate_settings.last_submission,
|
||||||
)
|
)
|
||||||
submission = (
|
submission = get_last_submission(
|
||||||
self.juror.last_submission
|
self.request.event.pretalx_musicrate_settings,
|
||||||
or self.request.event.pretalx_musicrate_settings.last_submission
|
self.request.event.submissions,
|
||||||
or self.request.event.submissions.filter(
|
self.juror.last_submission,
|
||||||
submission_type__in=self.request.event.pretalx_musicrate_settings.submission_types.all()
|
|
||||||
)
|
|
||||||
.order_by("created")
|
|
||||||
.first()
|
|
||||||
)
|
)
|
||||||
if submission is not None:
|
if submission is not None:
|
||||||
return redirect(
|
return redirect(
|
||||||
|
@ -131,13 +150,9 @@ class QRCodeView(EventPermissionRequired, TemplateView):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
context["last_submission"] = (
|
context["last_submission"] = get_last_submission(
|
||||||
self.request.event.pretalx_musicrate_settings.last_submission
|
self.request.event.pretalx_musicrate_settings,
|
||||||
or self.request.event.submissions.filter(
|
self.request.event.submissions,
|
||||||
submission_type__in=self.request.event.pretalx_musicrate_settings.submission_types.all()
|
|
||||||
)
|
|
||||||
.order_by("created")
|
|
||||||
.first()
|
|
||||||
)
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -148,7 +163,8 @@ class SubmissionMixin(SingleObjectMixin):
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.request.event.submissions.prefetch_related("answers").filter(
|
return self.request.event.submissions.prefetch_related("answers").filter(
|
||||||
submission_type__in=self.request.event.pretalx_musicrate_settings.submission_types.all()
|
submission_type__in=self.request.event.pretalx_musicrate_settings.submission_types.all(),
|
||||||
|
state=SubmissionStates.SUBMITTED,
|
||||||
)
|
)
|
||||||
|
|
||||||
@context
|
@context
|
||||||
|
@ -240,7 +256,7 @@ class RatingView(FormView, SubmissionMixin):
|
||||||
@cached_property
|
@cached_property
|
||||||
def juror(self):
|
def juror(self):
|
||||||
return get_object_or_404(
|
return get_object_or_404(
|
||||||
Juror, token=self.request.resolver_match.kwargs["token"]
|
Juror, token=self.request.resolver_match.kwargs["token"], frozen=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@context
|
@context
|
||||||
|
@ -275,17 +291,22 @@ class RatingView(FormView, SubmissionMixin):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
|
||||||
|
last_submission = get_last_submission(
|
||||||
|
self.request.event.pretalx_musicrate_settings,
|
||||||
|
self.request.event.submissions,
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
(settings := self.request.event.pretalx_musicrate_settings).last_submission
|
last_submission is not None
|
||||||
is not None
|
and self.object.created > last_submission.created
|
||||||
and self.object.created > settings.last_submission.created
|
|
||||||
):
|
):
|
||||||
return redirect(
|
return redirect(
|
||||||
self.request.resolver_match.view_name,
|
self.request.resolver_match.view_name,
|
||||||
event=self.request.event.slug,
|
event=self.request.event.slug,
|
||||||
token=self.juror.token,
|
token=self.juror.token,
|
||||||
code=settings.last_submission.code,
|
code=last_submission.code,
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -321,13 +342,15 @@ class PresenterView(EventPermissionRequired, SubmissionMixin, TemplateView):
|
||||||
links.append(
|
links.append(
|
||||||
(
|
(
|
||||||
"youtube",
|
"youtube",
|
||||||
url.removeprefix("http")
|
(
|
||||||
.removeprefix("s")
|
url.removeprefix("http")
|
||||||
.removeprefix("://")
|
.removeprefix("s")
|
||||||
.removeprefix("youtu.be/")[:11]
|
.removeprefix("://")
|
||||||
if m[1] == "youtu.be/"
|
.removeprefix("youtu.be/")[:11]
|
||||||
else "".join(
|
if m[1] == "youtu.be/"
|
||||||
parse_qs(urlparse(url).query).get("v", "dQw4w9WgXcQ")
|
else "".join(
|
||||||
|
parse_qs(urlparse(url).query).get("v", "dQw4w9WgXcQ")
|
||||||
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -342,16 +365,16 @@ class PresenterView(EventPermissionRequired, SubmissionMixin, TemplateView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if (
|
|
||||||
(settings := self.request.event.pretalx_musicrate_settings).last_submission
|
settings = self.request.event.pretalx_musicrate_settings
|
||||||
is None
|
last_submission = get_last_submission(settings, self.request.event.submissions)
|
||||||
or self.object.created > settings.last_submission.created
|
if last_submission is None or self.object.created > last_submission.created:
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
settings.last_submission = self.object
|
settings.last_submission = self.object
|
||||||
settings.save()
|
settings.save()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
response = super().get(request, *args, **kwargs)
|
response = super().get(request, *args, **kwargs)
|
||||||
response._csp_update = {"frame-src": "https://www.youtube-nocookie.com"}
|
response._csp_update = {"frame-src": "https://www.youtube-nocookie.com"}
|
||||||
return response
|
return response
|
||||||
|
@ -361,12 +384,12 @@ class MayAdvanceView(EventPermissionRequired, SubmissionMixin, View):
|
||||||
permission_required = "orga.view_submissions"
|
permission_required = "orga.view_submissions"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
num_ratings = self.submission.ratings.count()
|
num_ratings = self.submission.ratings.filter(juror__frozen=False).count()
|
||||||
num_jurors = self.request.event.jurors.count()
|
num_jurors = self.request.event.jurors.filter(frozen=False).count()
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
"mayAdvance": num_ratings
|
"mayAdvance": num_ratings
|
||||||
>= int(
|
>= round(
|
||||||
num_jurors
|
num_jurors
|
||||||
* self.request.event.pretalx_musicrate_settings.advance_threshold
|
* self.request.event.pretalx_musicrate_settings.advance_threshold
|
||||||
)
|
)
|
||||||
|
@ -379,3 +402,138 @@ class MayAdvanceView(EventPermissionRequired, SubmissionMixin, View):
|
||||||
% {"num_jurors": num_jurors, "num_ratings": num_ratings},
|
% {"num_jurors": num_jurors, "num_ratings": num_ratings},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportView(EventPermissionRequired, View):
|
||||||
|
permission_required = "orga.view_submissions"
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
response = HttpResponse(
|
||||||
|
content_type="text/csv",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f'attachment; filename="{request.event.slug}.csv"'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
writer = csv.writer(response)
|
||||||
|
genre_question = request.event.pretalx_musicrate_settings.genre_question
|
||||||
|
origin_question = request.event.pretalx_musicrate_settings.origin_question
|
||||||
|
jurors = request.event.jurors.filter(frozen=False).order_by("token")
|
||||||
|
for submission in (
|
||||||
|
request.event.submissions.prefetch_related("answers")
|
||||||
|
.select_related("submission_type")
|
||||||
|
.filter(
|
||||||
|
submission_type__in=request.event.pretalx_musicrate_settings.submission_types.all()
|
||||||
|
)
|
||||||
|
.only("title", "submission_type__name")
|
||||||
|
.order_by("created")
|
||||||
|
):
|
||||||
|
submission_info = [submission.title, submission.submission_type.name]
|
||||||
|
if genre_question is not None:
|
||||||
|
submission_info.append(
|
||||||
|
submission.answers.filter(question=genre_question)
|
||||||
|
.values_list("answer", flat=True)
|
||||||
|
.first()
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
if origin_question is not None:
|
||||||
|
submission_info.append(
|
||||||
|
submission.answers.filter(question=origin_question)
|
||||||
|
.values_list("answer", flat=True)
|
||||||
|
.first()
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
*submission_info,
|
||||||
|
*jurors.annotate(
|
||||||
|
filtered_ratings=FilteredRelation(
|
||||||
|
"ratings", condition=Q(ratings__submission=submission)
|
||||||
|
),
|
||||||
|
rating=Case(
|
||||||
|
When(filtered_ratings__rating=None, then=Value("")),
|
||||||
|
When(filtered_ratings__rating="", then=Value("E")),
|
||||||
|
default="filtered_ratings__rating",
|
||||||
|
),
|
||||||
|
).values_list("rating", flat=True),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class AssigneeView(EventPermissionRequired, FormView, SingleObjectMixin):
|
||||||
|
form_class = AssigneeForm
|
||||||
|
model = Submission
|
||||||
|
permission_required = "orga.change_submissions"
|
||||||
|
slug_field = "code"
|
||||||
|
slug_url_kwarg = "code"
|
||||||
|
template_name = "pretalx_musicrate/assignee.html"
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs["submission"] = self.object
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.request.path
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.save()
|
||||||
|
messages.success(self.request, _("Saved!"))
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
return super().get(*args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
return super().post(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EnhancedSubmissionList(SubmissionList):
|
||||||
|
sortable_fields = ("code", "score__value", "title", "state", "assignee")
|
||||||
|
template_name = "pretalx_musicrate/enhanced_list.html"
|
||||||
|
|
||||||
|
def get_filter_form(self):
|
||||||
|
return EnhancedSubmissionFilterForm(
|
||||||
|
data=self.request.GET,
|
||||||
|
event=self.request.event,
|
||||||
|
usable_states=self.usable_states,
|
||||||
|
limit_tracks=self.limit_tracks,
|
||||||
|
search_fields=self.get_default_filters(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_base_queryset(self, for_review=False):
|
||||||
|
qs = (
|
||||||
|
super(BaseSubmissionList, self)
|
||||||
|
.get_queryset(for_review=for_review)
|
||||||
|
.prefetch_related("assignee")
|
||||||
|
.order_by("-id")
|
||||||
|
)
|
||||||
|
if not self.filter_form.is_valid():
|
||||||
|
return qs
|
||||||
|
return self.filter_form.filter_queryset(qs)
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreExportView(EventPermissionRequired, View):
|
||||||
|
permission_required = "orga.view_submissions"
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
response = HttpResponse(
|
||||||
|
content_type="text/csv",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f'attachment; filename="{request.event.slug}.csv"'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
csv.writer(response).writerows(
|
||||||
|
request.event.submissions.select_related("score", "submission_type")
|
||||||
|
.filter(
|
||||||
|
submission_type__in=request.event.pretalx_musicrate_settings.submission_types.all()
|
||||||
|
)
|
||||||
|
.exclude(state__in=(SubmissionStates.CANCELED, SubmissionStates.WITHDRAWN))
|
||||||
|
.order_by(F("score__value").desc(nulls_last=True))
|
||||||
|
.values_list("score__value", "title")
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
|
@ -6,7 +6,6 @@ use_parentheses=True
|
||||||
line_length=88
|
line_length=88
|
||||||
known_first_party=pretalx_musicrate
|
known_first_party=pretalx_musicrate
|
||||||
known_third_party=pretalx
|
known_third_party=pretalx
|
||||||
skip=setup.py,.venv
|
|
||||||
combine_as_imports=True
|
combine_as_imports=True
|
||||||
default_section = THIRDPARTY
|
default_section = THIRDPARTY
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue