Compare commits

...

37 Commits

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
21 changed files with 696 additions and 107 deletions

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.11.1"
__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.9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-18 01:34+0100\n"
"PO-Revision-Date: 2023-12-18 01:36+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,43 +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/settings.html:22
msgid "Save"
msgstr "Speichern"
#: pretalx_musicrate/templates/pretalx_musicrate/settings.html:30
#: pretalx_musicrate/templates/pretalx_musicrate/settings.html:12
msgid "Export ratings"
msgstr "Bewertungen exportieren"
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:29
#: 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:41
#: pretalx_musicrate/views.py:66
msgid "Invalid token"
msgstr "Ungültiges Token"
#: pretalx_musicrate/views.py:116
#: pretalx_musicrate/views.py:133
msgid "The pretalx-musicrate settings were updated."
msgstr "Die pretalx-musicrate-Einstellungen wurden gespeichert."
#: pretalx_musicrate/views.py:275
#: pretalx_musicrate/views.py:289 pretalx_musicrate/views.py:482
msgid "Saved!"
msgstr "Gespeichert!"
#: pretalx_musicrate/views.py:377
#: 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

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

@ -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):
@ -112,3 +113,8 @@ class Assignee(models.Model):
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

@ -13,8 +13,8 @@ def pretalx_musicrate_placeholders(sender, **kwargs):
"assignee",
["submission"],
lambda submission: (
assignee.user.name
if (assignee := submission.assignee) is not None
submission.assignee.user.name
if hasattr(submission, "assignee")
else "Günther"
),
"Günther",
@ -24,10 +24,20 @@ def pretalx_musicrate_placeholders(sender, **kwargs):
@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.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",
@ -36,7 +46,7 @@ def pretalx_musicrate_qrcode(sender, request, **kwargs):
"url": reverse(
"plugins:pretalx_musicrate:qrcode", kwargs={"event": request.event.slug}
),
}
},
]
@ -46,7 +56,7 @@ def pretalx_musicrate_settings(sender, request, **kwargs):
return []
return [
{
"label": "pretalx-musicrate",
"label": _("pretalx-musicrate"),
"url": reverse(
"plugins:pretalx_musicrate:settings.musicrate",
kwargs={"event": request.event.slug},

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

@ -1,32 +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' %}
<div class="submit-group panel">
<span></span>
<span>
<button type="submit" class="btn btn-success btn-lg">
<i class="fa fa-check"></i>
{% translate "Save" %}
</button>
</span>
</div>
</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 %}

View File

@ -1,6 +1,9 @@
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,
@ -8,20 +11,39 @@ from .views import (
PresenterView,
QRCodeView,
RatingView,
ScoreExportView,
)
urlpatterns = [
path(
"orga/event/<slug:event>/settings/p/pretalx_musicrate/",
re_path(
rf"^orga/event/(?P<event>{SLUG_REGEX})/",
include(
[
path("", MusicrateSettingsView.as_view(), name="settings.musicrate"),
path("export/", ExportView.as_view(), name="export"),
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(
"<slug:event>/p/pretalx_musicrate/",
re_path(
rf"^(?P<event>{SLUG_REGEX})/p/pretalx_musicrate/",
include(
[
path("", QRCodeView.as_view(), name="qrcode"),

View File

@ -4,7 +4,7 @@ from hmac import compare_digest
from urllib.parse import parse_qs, urlparse
from django.contrib import messages
from django.db.models import Case, FilteredRelation, Q, Value, When
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
@ -14,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(
@ -24,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
@ -50,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(
@ -76,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(
@ -133,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
@ -150,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
@ -242,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
@ -277,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)
@ -346,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
@ -365,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
)
@ -398,7 +417,7 @@ class ExportView(EventPermissionRequired, View):
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.order_by("token")
jurors = request.event.jurors.filter(frozen=False).order_by("token")
for submission in (
request.event.submissions.prefetch_related("answers")
.select_related("submission_type")
@ -439,3 +458,82 @@ class ExportView(EventPermissionRequired, View):
]
)
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