Compare commits

..

No commits in common. "main" and "v0.2.0" have entirely different histories.
main ... v0.2.0

30 changed files with 122 additions and 862 deletions

View File

@ -17,7 +17,7 @@ steps:
export PATH="$$DRONE_WORKSPACE/.venv/bin:$$PATH"
- echo $$PATH
- which pip
- pip install black build Django flake8 isort twine
- pip install black build flake8 isort twine
- name: check style
image: python:3.11-alpine
@ -31,10 +31,6 @@ 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

View File

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

View File

@ -1,4 +1,4 @@
Copyright 2023-2024 Luca Schmid
Copyright 2023 Luca
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-2024 Luca Schmid
Copyright 2023 Luca Schmid
Released under the terms of the Apache License 2.0

View File

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

View File

@ -1,11 +1,8 @@
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 Assignee, MusicrateSettings, Rating
from .models import MusicrateSettings, Rating
class MusicrateSettingsForm(I18nModelForm):
@ -37,6 +34,12 @@ 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,
@ -53,31 +56,3 @@ 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-2024
# Copyright (C) 2023
# This file is distributed under the same license as the pretalx_musicrate package.
# Luca Schmid <Luca@hackerspace-bamberg.de>, 2023-2024.
# Luca Schmid <Luca@hackerspace-bamberg.de>, 2023.
#
msgid ""
msgstr ""
"Project-Id-Version: pretalx-musicrate 0.9.0\n"
"Project-Id-Version: pretalx-musicrate 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-09 22:22+0100\n"
"PO-Revision-Date: 2024-12-09 22:23+0100\n"
"POT-Creation-Date: 2023-12-16 03:23+0100\n"
"PO-Revision-Date: 2023-12-16 03:24+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.2\n"
"X-Generator: Poedit 3.4.1\n"
#: pretalx_musicrate/apps.py:12 pretalx_musicrate/signals.py:59
#: pretalx_musicrate/apps.py:12
msgid "pretalx-musicrate"
msgstr "pretalx-musicrate"
@ -26,10 +26,6 @@ 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 "
@ -66,103 +62,10 @@ msgstr "Weiter-Schwellenwert"
msgid "Link Questions"
msgstr "Fragen zu Links"
#: 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
#: pretalx_musicrate/signals.py:16
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"
@ -186,43 +89,35 @@ msgstr "Mitmach-QR-Code anzeigen"
msgid "Go to collective rating"
msgstr "Zum Anhörtag"
#: pretalx_musicrate/templates/pretalx_musicrate/settings.html:7
#: pretalx_musicrate/templates/pretalx_musicrate/settings.html:8
msgid "pretalx-musicrate settings"
msgstr "pretalx-musicrate-Einstellungen"
#: 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
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:29
msgid "(not specified)"
msgstr "(nicht angegeben)"
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:47
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:42
msgid "Previous"
msgstr "Zurück"
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:55
#: pretalx_musicrate/templates/pretalx_musicrate/submission_base.html:50
msgid "Next"
msgstr "Weiter"
#: pretalx_musicrate/views.py:66
#: pretalx_musicrate/views.py:39
msgid "Invalid token"
msgstr "Ungültiges Token"
#: pretalx_musicrate/views.py:133
#: pretalx_musicrate/views.py:114
msgid "The pretalx-musicrate settings were updated."
msgstr "Die pretalx-musicrate-Einstellungen wurden gespeichert."
#: pretalx_musicrate/views.py:289 pretalx_musicrate/views.py:482
#: pretalx_musicrate/views.py:273
msgid "Saved!"
msgstr "Gespeichert!"
#: pretalx_musicrate/views.py:398
#: pretalx_musicrate/views.py:375
#, 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

@ -1,81 +0,0 @@
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", "0072_alter_reviewscore_label"),
("event", "0033_chinese_locale_codes"),
("submission", "0074_created_updated_everywhere"),
("event", "0035_created_updated_everywhere"),
]
operations = [

View File

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("submission", "0072_alter_reviewscore_label"),
("submission", "0074_created_updated_everywhere"),
("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.models.mixins
import pretalx.common.mixins.models
from django.db import migrations, models
import pretalx_musicrate.models
@ -9,8 +9,8 @@ import pretalx_musicrate.models
class Migration(migrations.Migration):
dependencies = [
("submission", "0072_alter_reviewscore_label"),
("event", "0033_chinese_locale_codes"),
("submission", "0074_created_updated_everywhere"),
("event", "0035_created_updated_everywhere"),
("pretalx_musicrate", "0004_musicratesettings_advance_threshold"),
]
@ -51,8 +51,8 @@ class Migration(migrations.Migration):
"abstract": False,
},
bases=(
pretalx.common.models.mixins.LogMixin,
pretalx.common.models.mixins.FileCleanupMixin,
pretalx.common.mixins.models.LogMixin,
pretalx.common.mixins.models.FileCleanupMixin,
models.Model,
),
),

View File

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

View File

@ -1,42 +0,0 @@
# 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

@ -1,34 +0,0 @@
# 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

@ -1,18 +0,0 @@
# 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,7 +78,6 @@ class Juror(models.Model):
related_name="jurors",
null=True,
)
frozen = models.BooleanField(default=False)
class Rating(models.Model):
@ -106,15 +105,3 @@ 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,52 +1,23 @@
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_nav_event(sender, request, **kwargs):
def pretalx_musicrate_qrcode(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
"active": request.resolver_match.url_name
== "plugins:pretalx_musicrate:qrcode",
"icon": "star",
"label": _("Collective Rating"),
"url": reverse(
"plugins:pretalx_musicrate:qrcode", kwargs={"event": request.event.slug}
),
},
}
]
@ -56,12 +27,12 @@ def pretalx_musicrate_settings(sender, request, **kwargs):
return []
return [
{
"label": _("pretalx-musicrate"),
"label": "pretalx-musicrate",
"url": reverse(
"plugins:pretalx_musicrate:settings.musicrate",
"plugins:pretalx_musicrate:settings",
kwargs={"event": request.event.slug},
),
"active": request.resolver_match.view_name
== "plugins:pretalx_musicrate:settings.musicrate",
"active": request.resolver_match.url_name
== "plugins:pretalx_musicrate:settings",
}
]

View File

@ -33,12 +33,12 @@
}
.rating-container > .rating > input[type=radio]:checked ~ .rating-point {
background-image: url(static('pretalx_musicrate/point-fill.svg'));
background-image: url(point-fill.svg);
}
.rating-container > .rating > .rating-point {
--max-size: calc((100vw - 2em - 2 * 0.1em * var(--num-choices)) / var(--num-choices));
background: url(static('pretalx_musicrate/point-stroke.svg')) no-repeat center/100%;
background: url(point-stroke.svg) no-repeat center/100%;
height: 3em;
margin: 0 0.1em;
max-height: var(--max-size);

View File

@ -1,14 +0,0 @@
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

@ -1,8 +0,0 @@
{% 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

@ -1,180 +0,0 @@
{% 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" type="text/x-scss" href="{% static "pretalx_musicrate/rating.scss" %}">
<link rel="stylesheet" href="{% static "pretalx_musicrate/rating.css" %}">
{% endcompress %}
{% compress js %}
<script defer src="{% static "pretalx_musicrate/rating.js" %}"></script>

View File

@ -1,18 +1,19 @@
{% extends "orga/base.html" %}
{% load bootstrap4 %}
{% load compress %}
{% load i18n %}
{% load static %}
{% block content %}
<h2>{% translate "pretalx-musicrate settings" %}</h2>
{% 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>
<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>
{% endblock %}

View File

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

View File

@ -1,49 +1,22 @@
from django.urls import include, path, re_path
from pretalx.event.models.event import SLUG_REGEX
from django.urls import include, path
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(
"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(
"orga/event/<slug:event>/settings/p/pretalx_musicrate/",
MusicrateSettingsView.as_view(),
name="settings",
),
re_path(
rf"^(?P<event>{SLUG_REGEX})/p/pretalx_musicrate/",
path(
"<slug:event>/p/pretalx_musicrate/",
include(
[
path("", QRCodeView.as_view(), name="qrcode"),

View File

@ -1,11 +1,9 @@
import csv
import re
from hmac import compare_digest
from urllib.parse import parse_qs, urlparse
from django.contrib import messages
from django.db.models import Case, F, FilteredRelation, Q, Value, When
from django.http import HttpResponse, JsonResponse
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
@ -14,16 +12,9 @@ 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.views.mixins import EventPermissionRequired
from pretalx.orga.views.submission import BaseSubmissionList, SubmissionList
from pretalx.submission.models import Submission, SubmissionStates
from pretalx.common.mixins.views import EventPermissionRequired
from .forms import (
AssigneeForm,
EnhancedSubmissionFilterForm,
MusicrateSettingsForm,
RatingForm,
)
from .forms import MusicrateSettingsForm, RatingForm
from .models import Juror, Rating
youtube_re = re.compile(
@ -31,30 +22,12 @@ 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, frozen=False)
self.juror = Juror.objects.get(token=token)
return True
except Juror.DoesNotExist:
self.juror = None
@ -75,10 +48,14 @@ class JoinView(TemplateView):
def get(self, request, *args, token, **kwargs):
token_valid = self.validate_token(token)
if self.juror is not None:
submission = get_last_submission(
self.request.event.pretalx_musicrate_settings,
self.request.event.submissions,
self.juror.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()
)
if submission is not None:
return redirect(
@ -97,10 +74,14 @@ class JoinView(TemplateView):
event=self.request.event,
last_submission=self.request.event.pretalx_musicrate_settings.last_submission,
)
submission = get_last_submission(
self.request.event.pretalx_musicrate_settings,
self.request.event.submissions,
self.juror.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()
)
if submission is not None:
return redirect(
@ -150,9 +131,13 @@ class QRCodeView(EventPermissionRequired, TemplateView):
},
)
)
context["last_submission"] = get_last_submission(
self.request.event.pretalx_musicrate_settings,
self.request.event.submissions,
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()
)
return context
@ -163,8 +148,7 @@ 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(),
state=SubmissionStates.SUBMITTED,
submission_type__in=self.request.event.pretalx_musicrate_settings.submission_types.all()
)
@context
@ -256,7 +240,7 @@ class RatingView(FormView, SubmissionMixin):
@cached_property
def juror(self):
return get_object_or_404(
Juror, token=self.request.resolver_match.kwargs["token"], frozen=False
Juror, token=self.request.resolver_match.kwargs["token"]
)
@context
@ -291,22 +275,17 @@ 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 (
last_submission is not None
and self.object.created > last_submission.created
(settings := self.request.event.pretalx_musicrate_settings).last_submission
is not None
and self.object.created > settings.last_submission.created
):
return redirect(
self.request.resolver_match.view_name,
event=self.request.event.slug,
token=self.juror.token,
code=last_submission.code,
code=settings.last_submission.code,
)
return super().get(request, *args, **kwargs)
@ -342,15 +321,13 @@ class PresenterView(EventPermissionRequired, SubmissionMixin, TemplateView):
links.append(
(
"youtube",
(
url.removeprefix("http")
.removeprefix("s")
.removeprefix("://")
.removeprefix("youtu.be/")[:11]
if m[1] == "youtu.be/"
else "".join(
parse_qs(urlparse(url).query).get("v", "dQw4w9WgXcQ")
)
url.removeprefix("http")
.removeprefix("s")
.removeprefix("://")
.removeprefix("youtu.be/")[:11]
if m[1] == "youtu.be/"
else "".join(
parse_qs(urlparse(url).query).get("v", "dQw4w9WgXcQ")
),
)
)
@ -365,16 +342,16 @@ class PresenterView(EventPermissionRequired, SubmissionMixin, TemplateView):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
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:
if (
(settings := self.request.event.pretalx_musicrate_settings).last_submission
is None
or self.object.created > settings.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
@ -384,12 +361,12 @@ class MayAdvanceView(EventPermissionRequired, SubmissionMixin, View):
permission_required = "orga.view_submissions"
def get(self, request, *args, **kwargs):
num_ratings = self.submission.ratings.filter(juror__frozen=False).count()
num_jurors = self.request.event.jurors.filter(frozen=False).count()
num_ratings = self.submission.ratings.count()
num_jurors = self.request.event.jurors.count()
return JsonResponse(
{
"mayAdvance": num_ratings
>= round(
>= int(
num_jurors
* self.request.event.pretalx_musicrate_settings.advance_threshold
)
@ -402,138 +379,3 @@ 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

@ -12,7 +12,13 @@ maintainers = [
{name = "Luca", email = "Luca@hackerspace-bamberg.de"},
]
dependencies = []
dependencies = [
"Django",
"django-context-decorator",
"django-i18nfield",
"pretalx",
"qrcode",
]
[project.urls]
homepage = "https://git.luj0ga.de/kontakt/pretalx-musicrate"

View File

@ -6,6 +6,7 @@ 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