Compare commits

...

56 Commits
v0.3.0 ... main

Author SHA1 Message Date
Luca d319559272 fix(computescores): unexpected keyword argument
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2025-02-23 17:57:25 +01:00
Luca 1b264054fc chore: bump version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2025-02-23 17:46:19 +01:00
Luca c5b39e1890 feat(computescores): add option to include frozen jurors 2025-02-23 17:45:55 +01:00
Luca 97a1dbf33a chore: bump version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2025-02-23 14:41:17 +01:00
Luca 27e0b1e2c5 feat: add flag to Juror to disable them 2025-02-23 14:40:52 +01:00
Luca ec3acac3fc fix: assignee assignment
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2025-01-30 18:47:07 +01:00
Luca d5e3a35056 chore: bump version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-12-09 22:33:19 +01:00
Luca 9accf9714e feat: add score export 2024-12-09 22:32:50 +01:00
Luca 2d7efeaf09 chore: bump version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-12-08 15:49:16 +01:00
Luca 50d189d8ed feat: 'fix' last submission when submission types change 2024-12-08 15:48:24 +01:00
Luca f8f95db5c3 chore: bump version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-12-07 20:40:54 +01:00
Luca dcce8e5407 fix: find new last submission if current one is not 'submitted'
continuous-integration/drone/push Build is passing Details
2024-12-07 20:36:15 +01:00
Luca 660650c8da feat: include only 'submitted' submissions in rating 2024-12-07 19:34:29 +01:00
Luca 18af2a75e6 chore: bump version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-11-02 19:59:01 +01:00
Luca 01f5434065 fix: round half up instead of rounding down
continuous-integration/drone/push Build is passing Details
2024-11-02 19:57:39 +01:00
Luca e7df05566f chore: update year 2024-11-02 19:53:14 +01:00
Luca cabf4538d2 fix: missing style definition for .submit-group 2024-11-02 19:48:44 +01:00
Luca 81f1d44700 chore: update localization
continuous-integration/drone/push Build is passing Details
2024-11-02 19:34:39 +01:00
Luca 1c8047a006 refactor: remove dependencies on Bootstrap and jQuery 2024-11-02 19:21:47 +01:00
Luca 6823e11ff0 refactor: match urls using pretalx' custom SLUG_REGEX 2024-11-02 19:20:35 +01:00
Luca a693370a7d chore: remove legacy css classes on widgets 2024-11-02 19:15:31 +01:00
Luca 5b508bf61b fix: mixins import path 2024-11-02 19:14:29 +01:00
Luca d4a1a4738f chore: start development cycle 2024-11-02 19:13:30 +01:00
Luca 91acd4e855 style: add .editorconfig 2024-11-02 19:12:14 +01:00
Luca 83409ef2c8 chore: update for pretalx 2024.1.0
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-16 19:11:14 +01:00
Luca 353213b1c8 fix(computescores): ensure rating processing order is the same as in export
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-16 12:01:26 +01:00
Luca a67cc96935 chore: bump version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-14 22:35:07 +01:00
Luca fdf9110875 feat: update localization for de_DE 2024-02-14 22:34:39 +01:00
Luca c7d67d97f6 fix: ordering of submissions by score
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-14 19:40:19 +01:00
Luca 8b1b807ef3 feat: compute scores from ratings via management command
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-14 18:58:16 +01:00
Luca e3a82cb705 fix: RelatedObjectDoesNotExist raised when no assignee exists
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-14 17:16:02 +01:00
Luca 03b5a6ebf4 chore: bump version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-14 02:16:03 +01:00
Luca 5df8a8b29b feat: add Score model
continuous-integration/drone/push Build is passing Details
2024-02-14 01:45:33 +01:00
Luca ca3ca52ab2 feat: implement enhanced submission list
continuous-integration/drone/push Build is passing Details
2024-02-14 00:54:28 +01:00
Luca d709f020f4 fix: urlconf order 2024-02-14 00:49:19 +01:00
Luca f0caef1cca chore: bump version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-13 18:40:03 +01:00
Luca f3b5473cd3 feat: add assignee form 2024-02-13 18:39:25 +01:00
Luca 122c120eae fix: add missing migration
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-13 16:33:40 +01:00
Luca 23821c523d fix: version number
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2024-02-13 15:43:08 +01:00
Luca 1a3382614a style: fix code style
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is failing Details
2024-02-13 15:38:04 +01:00
Luca b99264647d feat: add {assignee} placeholder
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is failing Details
2024-02-13 15:32:23 +01:00
Luca 48216a8862 feat: add submission assignee model 2024-02-13 15:30:34 +01:00
Luca d2ca7db7aa feat(export): sort submissions and optimize query
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-12-18 02:23:44 +01:00
Luca 57317b51d2 feat: export ratings
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-12-18 01:38:35 +01:00
Luca 3091fb0783 fix: keep dropdown menu expanded 2023-12-18 01:15:50 +01:00
Luca 14df5d5b68 fix: highlighting of active nav elements 2023-12-18 01:11:53 +01:00
Luca a0ccd2ff8e style(isort): use default skip settings 2023-12-18 00:51:41 +01:00
Luca 049062d5bb feat: show submission type
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-12-17 16:28:15 +01:00
Luca 2bb22a0e78 fix: background images in production
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-12-17 03:54:04 +01:00
Luca 31c3c820fd ci: only compile messages in pretalx_musicrate/
continuous-integration/drone/push Build is passing Details
2023-12-17 00:01:10 +01:00
Luca b6d35d152c ci: install gettext
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-12-16 23:56:28 +01:00
Luca 41f5fa840b ci: fix build order
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is failing Details
2023-12-16 23:52:01 +01:00
Luca fac4f8ba56 ci: build and include language files
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is failing Details
2023-12-16 23:50:26 +01:00
Luca ad86b5dfd3 fix: missing partial template
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-12-16 23:19:49 +01:00
Luca 98d9607a70 chore: bump version
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2023-12-16 23:05:13 +01:00
Luca 14df6ecec4 fix: dependency on unreleased migrations
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is failing Details
2023-12-16 22:55:20 +01:00
29 changed files with 861 additions and 115 deletions

View File

@ -17,7 +17,7 @@ steps:
export PATH="$$DRONE_WORKSPACE/.venv/bin:$$PATH"
- echo $$PATH
- which pip
- pip install black build flake8 isort twine
- pip install black build Django flake8 isort twine
- name: check style
image: python:3.11-alpine
@ -31,6 +31,10 @@ steps:
image: python:3.11-alpine
commands:
- *path
- apk --update-cache add gettext
- cd pretalx_musicrate
- django-admin compilemessages
- cd ..
- python -m build
- name: publish

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
root = true
[*.{html,js}]
indent_size = 4
indent_style = space

View File

@ -1,4 +1,4 @@
Copyright 2023 Luca
Copyright 2023-2024 Luca Schmid
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -40,7 +40,7 @@ black .
## License
Copyright 2023 Luca Schmid
Copyright 2023-2024 Luca Schmid
Released under the terms of the Apache License 2.0

View File

@ -1 +1 @@
__version__ = "0.3.0"
__version__ = "2025.6.1"

View File

@ -1,8 +1,11 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceField
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):
@ -34,12 +37,6 @@ class MusicrateSettingsForm(I18nModelForm):
"link_questions",
"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 = {
"submission_types": SafeModelMultipleChoiceField,
"genre_question": SafeModelChoiceField,
@ -56,3 +53,31 @@ class RatingForm(forms.ModelForm):
class Meta:
model = 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

View File

@ -1,14 +1,14 @@
# 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.
# Luca Schmid <Luca@hackerspace-bamberg.de>, 2023.
# Luca Schmid <Luca@hackerspace-bamberg.de>, 2023-2024.
#
msgid ""
msgstr ""
"Project-Id-Version: pretalx-musicrate 0.0.1\n"
"Project-Id-Version: pretalx-musicrate 0.9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-16 03:23+0100\n"
"PO-Revision-Date: 2023-12-16 03:24+0100\n"
"POT-Creation-Date: 2024-12-09 22:22+0100\n"
"PO-Revision-Date: 2024-12-09 22:23+0100\n"
"Last-Translator: Luca <Luca@hackerspace-bamberg.de>\n"
"Language-Team: Luca <Luca@hackerspace-bamberg.de>\n"
"Language: de_DE\n"
@ -16,9 +16,9 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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"
msgstr "pretalx-musicrate"
@ -26,6 +26,10 @@ msgstr "pretalx-musicrate"
msgid "pretalx plugin for rating music"
msgstr "pretalx-Plugin zur Bewertung von Musik"
#: pretalx_musicrate/forms.py:79
msgid "require all"
msgstr "alle voraussetzen"
#: pretalx_musicrate/models.py:23
msgid ""
"You can limit pretalx-musicrate to some session types. Leave this field "
@ -62,10 +66,103 @@ msgstr "Weiter-Schwellenwert"
msgid "Link Questions"
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"
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
msgid "Join collective rating"
msgstr "Beim Anhörtag mitmachen"
@ -89,35 +186,43 @@ msgstr "Mitmach-QR-Code anzeigen"
msgid "Go to collective rating"
msgstr "Zum Anhörtag"
#: pretalx_musicrate/templates/pretalx_musicrate/settings.html:8
#: pretalx_musicrate/templates/pretalx_musicrate/settings.html:7
msgid "pretalx-musicrate settings"
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)"
msgstr "(nicht angegeben)"
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:42
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:47
msgid "Previous"
msgstr "Zurück"
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:50
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:55
msgid "Next"
msgstr "Weiter"
#: pretalx_musicrate/views.py:39
#: pretalx_musicrate/views.py:66
msgid "Invalid token"
msgstr "Ungültiges Token"
#: pretalx_musicrate/views.py:114
#: pretalx_musicrate/views.py:133
msgid "The pretalx-musicrate settings were updated."
msgstr "Die pretalx-musicrate-Einstellungen wurden gespeichert."
#: pretalx_musicrate/views.py:273
#: pretalx_musicrate/views.py:289 pretalx_musicrate/views.py:482
msgid "Saved!"
msgstr "Gespeichert!"
#: pretalx_musicrate/views.py:375
#: pretalx_musicrate/views.py:398
#, python-format
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"

View File

View File

@ -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()

View File

@ -8,8 +8,8 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
("submission", "0074_created_updated_everywhere"),
("event", "0035_created_updated_everywhere"),
("submission", "0072_alter_reviewscore_label"),
("event", "0033_chinese_locale_codes"),
]
operations = [

View File

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("submission", "0074_created_updated_everywhere"),
("submission", "0072_alter_reviewscore_label"),
("pretalx_musicrate", "0001_initial"),
]

View File

@ -1,7 +1,7 @@
# Generated by Django 4.2.8 on 2023-12-15 18:27
import django.db.models.deletion
import pretalx.common.mixins.models
import pretalx.common.models.mixins
from django.db import migrations, models
import pretalx_musicrate.models
@ -9,8 +9,8 @@ import pretalx_musicrate.models
class Migration(migrations.Migration):
dependencies = [
("submission", "0074_created_updated_everywhere"),
("event", "0035_created_updated_everywhere"),
("submission", "0072_alter_reviewscore_label"),
("event", "0033_chinese_locale_codes"),
("pretalx_musicrate", "0004_musicratesettings_advance_threshold"),
]
@ -51,8 +51,8 @@ class Migration(migrations.Migration):
"abstract": False,
},
bases=(
pretalx.common.mixins.models.LogMixin,
pretalx.common.mixins.models.FileCleanupMixin,
pretalx.common.models.mixins.LogMixin,
pretalx.common.models.mixins.FileCleanupMixin,
models.Model,
),
),

View File

@ -5,7 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("submission", "0074_created_updated_everywhere"),
("submission", "0072_alter_reviewscore_label"),
(
"pretalx_musicrate",
"0005_juror_musicratesettings_last_submission_rating_and_more",

View File

@ -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,
),
),
],
),
]

View File

@ -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",
),
),
],
),
]

View File

@ -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),
),
]

View File

@ -78,6 +78,7 @@ class Juror(models.Model):
related_name="jurors",
null=True,
)
frozen = models.BooleanField(default=False)
class Rating(models.Model):
@ -105,3 +106,15 @@ class Rating(models.Model):
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()

View File

@ -1,23 +1,52 @@
from django.dispatch import receiver
from django.urls import reverse
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
@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)
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):
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",
"icon": "star",
"label": _("Collective Rating"),
"url": reverse(
"plugins:pretalx_musicrate:qrcode", kwargs={"event": request.event.slug}
),
}
},
]
@ -27,12 +56,12 @@ def pretalx_musicrate_settings(sender, request, **kwargs):
return []
return [
{
"label": "pretalx-musicrate",
"label": _("pretalx-musicrate"),
"url": reverse(
"plugins:pretalx_musicrate:settings",
"plugins:pretalx_musicrate:settings.musicrate",
kwargs={"event": request.event.slug},
),
"active": request.resolver_match.url_name
== "plugins:pretalx_musicrate:settings",
"active": request.resolver_match.view_name
== "plugins:pretalx_musicrate:settings.musicrate",
}
]

View File

@ -33,12 +33,12 @@
}
.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 {
--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;
margin: 0 0.1em;
max-height: var(--max-size);

View File

@ -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()
})

View File

@ -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 %}

View File

@ -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 %}
&ndash;
{% 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 %}
&ndash;
{% 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 %}

View File

@ -5,7 +5,7 @@
{% block submission_header %}
{% 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 %}
{% compress js %}
<script defer src="{% static "pretalx_musicrate/rating.js" %}"></script>

View File

@ -1,19 +1,18 @@
{% extends "orga/base.html" %}
{% load bootstrap4 %}
{% load compress %}
{% load i18n %}
{% load static %}
{% block content %}
<h2>{% translate "pretalx-musicrate settings" %}</h2>
<form method="post">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.submission_types layout='event' %}
{% bootstrap_field form.genre_question layout='event' %}
{% bootstrap_field form.origin_question layout='event' %}
{% bootstrap_field form.link_questions layout='event' %}
{% bootstrap_field form.advance_threshold layout='event' %}
{% include "orga/includes/submit_row.html" %}
</form>
{% include "orga/includes/base_form.html" %}
<hr>
<a class="btn btn-success btn-lg btn-block" href="{% url "plugins:pretalx_musicrate:export" event=request.event.slug %}">
<i class="fa fa-download"></i>
{% translate "Export ratings" %}
</a>
<a class="btn btn-success btn-lg btn-block" href="{% url "plugins:pretalx_musicrate:scores" event=request.event.slug %}">
<i class="fa fa-download"></i>
{% translate "Export scores" %}
</a>
{% endblock %}

View File

@ -1,11 +1,13 @@
{% extends "cfp/event/base.html" %}
{% load compress %}
{% load i18n %}
{% load static %}
{% block title %}{{ submission.title }} ::{% endblock %}
{% block cfp_header %}
{% block stylesheets %}
{% compress css %}
<link rel="stylesheet" type="text/css" href="{% static "common/css/_forms.css" %}" />
<style>
.musicrate-pagination {
display: flex;
@ -21,6 +23,9 @@
}
</style>
{% endcompress %}
{% endblock %}
{% block cfp_header %}
{% block submission_header %}
{% endblock %}
{% endblock %}
@ -29,7 +34,7 @@
{% translate "(not specified)" as not_specified %}
<h1>{{ submission.title }}</h1>
<p>
{{ genre|default:not_specified }} &middot; {{ origin|default:not_specified }}
{{ submission.submission_type.name }} &middot; {{ genre|default:not_specified }} &middot; {{ origin|default:not_specified }}
{% if submission.internal_notes %}
<br>
{{ submission.internal_notes }}

View File

@ -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 (
AssigneeView,
EnhancedSubmissionList,
ExportView,
JoinView,
MayAdvanceView,
MusicrateSettingsView,
PresenterView,
QRCodeView,
RatingView,
ScoreExportView,
)
urlpatterns = [
re_path(
rf"^orga/event/(?P<event>{SLUG_REGEX})/",
include(
[
path(
"orga/event/<slug:event>/settings/p/pretalx_musicrate/",
"settings/p/pretalx_musicrate/",
MusicrateSettingsView.as_view(),
name="settings",
name="settings.musicrate",
),
path(
"<slug:event>/p/pretalx_musicrate/",
"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"),
]
),
),
]
),
),
re_path(
rf"^(?P<event>{SLUG_REGEX})/p/pretalx_musicrate/",
include(
[
path("", QRCodeView.as_view(), name="qrcode"),

View File

@ -1,9 +1,11 @@
import csv
import re
from hmac import compare_digest
from urllib.parse import parse_qs, urlparse
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.urls import reverse
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.detail import SingleObjectMixin
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
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):
template_name = "pretalx_musicrate/join.html"
def validate_token(self, token):
try:
self.juror = Juror.objects.get(token=token)
self.juror = Juror.objects.get(token=token, frozen=False)
return True
except Juror.DoesNotExist:
self.juror = None
@ -48,14 +75,10 @@ class JoinView(TemplateView):
def get(self, request, *args, token, **kwargs):
token_valid = self.validate_token(token)
if self.juror is not None:
submission = (
self.juror.last_submission
or self.request.event.pretalx_musicrate_settings.last_submission
or self.request.event.submissions.filter(
submission_type__in=self.request.event.pretalx_musicrate_settings.submission_types.all()
)
.order_by("created")
.first()
submission = get_last_submission(
self.request.event.pretalx_musicrate_settings,
self.request.event.submissions,
self.juror.last_submission,
)
if submission is not None:
return redirect(
@ -74,14 +97,10 @@ class JoinView(TemplateView):
event=self.request.event,
last_submission=self.request.event.pretalx_musicrate_settings.last_submission,
)
submission = (
self.juror.last_submission
or self.request.event.pretalx_musicrate_settings.last_submission
or self.request.event.submissions.filter(
submission_type__in=self.request.event.pretalx_musicrate_settings.submission_types.all()
)
.order_by("created")
.first()
submission = get_last_submission(
self.request.event.pretalx_musicrate_settings,
self.request.event.submissions,
self.juror.last_submission,
)
if submission is not None:
return redirect(
@ -131,13 +150,9 @@ class QRCodeView(EventPermissionRequired, TemplateView):
},
)
)
context["last_submission"] = (
self.request.event.pretalx_musicrate_settings.last_submission
or self.request.event.submissions.filter(
submission_type__in=self.request.event.pretalx_musicrate_settings.submission_types.all()
)
.order_by("created")
.first()
context["last_submission"] = get_last_submission(
self.request.event.pretalx_musicrate_settings,
self.request.event.submissions,
)
return context
@ -148,7 +163,8 @@ class SubmissionMixin(SingleObjectMixin):
def get_queryset(self):
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
@ -240,7 +256,7 @@ class RatingView(FormView, SubmissionMixin):
@cached_property
def juror(self):
return get_object_or_404(
Juror, token=self.request.resolver_match.kwargs["token"]
Juror, token=self.request.resolver_match.kwargs["token"], frozen=False
)
@context
@ -275,17 +291,22 @@ class RatingView(FormView, SubmissionMixin):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
last_submission = get_last_submission(
self.request.event.pretalx_musicrate_settings,
self.request.event.submissions,
)
if (
(settings := self.request.event.pretalx_musicrate_settings).last_submission
is not None
and self.object.created > settings.last_submission.created
last_submission is not None
and self.object.created > last_submission.created
):
return redirect(
self.request.resolver_match.view_name,
event=self.request.event.slug,
token=self.juror.token,
code=settings.last_submission.code,
code=last_submission.code,
)
return super().get(request, *args, **kwargs)
@ -321,6 +342,7 @@ class PresenterView(EventPermissionRequired, SubmissionMixin, TemplateView):
links.append(
(
"youtube",
(
url.removeprefix("http")
.removeprefix("s")
.removeprefix("://")
@ -328,6 +350,7 @@ class PresenterView(EventPermissionRequired, SubmissionMixin, TemplateView):
if m[1] == "youtu.be/"
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):
self.object = self.get_object()
if (
(settings := self.request.event.pretalx_musicrate_settings).last_submission
is None
or self.object.created > settings.last_submission.created
):
settings = self.request.event.pretalx_musicrate_settings
last_submission = get_last_submission(settings, self.request.event.submissions)
if last_submission is None or self.object.created > last_submission.created:
try:
settings.last_submission = self.object
settings.save()
except Exception:
pass
response = super().get(request, *args, **kwargs)
response._csp_update = {"frame-src": "https://www.youtube-nocookie.com"}
return response
@ -361,12 +384,12 @@ class MayAdvanceView(EventPermissionRequired, SubmissionMixin, View):
permission_required = "orga.view_submissions"
def get(self, request, *args, **kwargs):
num_ratings = self.submission.ratings.count()
num_jurors = self.request.event.jurors.count()
num_ratings = self.submission.ratings.filter(juror__frozen=False).count()
num_jurors = self.request.event.jurors.filter(frozen=False).count()
return JsonResponse(
{
"mayAdvance": num_ratings
>= int(
>= round(
num_jurors
* 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},
}
)
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

View File

@ -6,7 +6,6 @@ use_parentheses=True
line_length=88
known_first_party=pretalx_musicrate
known_third_party=pretalx
skip=setup.py,.venv
combine_as_imports=True
default_section = THIRDPARTY