Compare commits

...

13 Commits

9 changed files with 130 additions and 59 deletions
pretalx_musicrate
__init__.pyforms.py
locale/de_DE/LC_MESSAGES
management/commands
migrations
models.py
templates/pretalx_musicrate
urls.pyviews.py

View File

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

View File

@ -62,12 +62,9 @@ class AssigneeForm(forms.ModelForm):
except Assignee.DoesNotExist:
self.instance = Assignee(submission=submission)
super().__init__(*args, instance=self.instance, **kwargs)
self.fields["user"].queryset = User.objects.none().union(
*(
t.members.all()
for t in submission.event.teams.filter(can_change_submissions=True)
)
)
self.fields["user"].queryset = User.objects.filter(
teams__in=submission.event.teams, teams__can_change_submissions=True
).distinct()
class Meta:
model = Assignee

View File

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: pretalx-musicrate 0.9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-02 19:23+0100\n"
"PO-Revision-Date: 2024-02-14 22:30+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,7 +16,7 @@ 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/signals.py:59
msgid "pretalx-musicrate"
@ -194,31 +194,35 @@ msgstr "pretalx-musicrate-Einstellungen"
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:48
#: pretalx_musicrate/views.py:66
msgid "Invalid token"
msgstr "Ungültiges Token"
#: pretalx_musicrate/views.py:123
#: pretalx_musicrate/views.py:133
msgid "The pretalx-musicrate settings were updated."
msgstr "Die pretalx-musicrate-Einstellungen wurden gespeichert."
#: pretalx_musicrate/views.py:282 pretalx_musicrate/views.py:469
#: pretalx_musicrate/views.py:289 pretalx_musicrate/views.py:482
msgid "Saved!"
msgstr "Gespeichert!"
#: pretalx_musicrate/views.py:386
#: 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

@ -20,6 +20,9 @@ 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):
@ -31,9 +34,15 @@ class Command(BaseCommand):
with scope(event=event):
submissions = {}
for juror in event.jurors.prefetch_related("ratings__submission").order_by(
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")
)

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):

View File

@ -11,4 +11,8 @@
<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

@ -11,6 +11,7 @@ from .views import (
PresenterView,
QRCodeView,
RatingView,
ScoreExportView,
)
urlpatterns = [
@ -33,6 +34,7 @@ urlpatterns = [
EnhancedSubmissionList.as_view(),
name="enhanced_list",
),
path("scores/", ScoreExportView.as_view(), name="scores"),
path("<code>/", AssigneeView.as_view(), name="assignee"),
]
),

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
@ -16,7 +16,7 @@ from django.views.generic.detail import SingleObjectMixin
from django_context_decorator import context
from pretalx.common.views.mixins import EventPermissionRequired
from pretalx.orga.views.submission import BaseSubmissionList, SubmissionList
from pretalx.submission.models import Submission
from pretalx.submission.models import Submission, SubmissionStates
from .forms import (
AssigneeForm,
@ -31,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
@ -57,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(
@ -83,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(
@ -140,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
@ -157,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
@ -249,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
@ -284,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)
@ -353,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
@ -372,8 +384,8 @@ 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
@ -405,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")
@ -501,3 +513,27 @@ class EnhancedSubmissionList(SubmissionList):
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