From 45ea203af8e85993ec4e2187bbb17212dd3675e6 Mon Sep 17 00:00:00 2001 From: knuth Date: Fri, 6 Dec 2024 12:46:54 +0100 Subject: [PATCH 1/5] fix typo --- src/rockon/crew/templates/join.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rockon/crew/templates/join.html b/src/rockon/crew/templates/join.html index f4f2af4..dc6a658 100644 --- a/src/rockon/crew/templates/join.html +++ b/src/rockon/crew/templates/join.html @@ -163,7 +163,7 @@
{{ phase.name }}
From 7664d8d0826cc84c4cda14b80f8fea732864cc77 Mon Sep 17 00:00:00 2001 From: knuth Date: Fri, 6 Dec 2024 12:52:39 +0100 Subject: [PATCH 2/5] allow booking on bid vote --- src/rockon/bands/views/bid.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/rockon/bands/views/bid.py b/src/rockon/bands/views/bid.py index 14992a3..9e9dd01 100644 --- a/src/rockon/bands/views/bid.py +++ b/src/rockon/bands/views/bid.py @@ -107,16 +107,19 @@ def bid_form(request, slug, guid): @login_required @user_passes_test(lambda u: u.groups.filter(name="crew").exists()) def bid_vote(request, bid: str = None, track: str = None, slug: str = None): - if not request.user.crewmember_set.filter( - crew__event__slug=slug, state="confirmed" - ).exists(): + is_booking = request.user.groups.filter(name="booking").exists() + if ( + not ( + request.user.crewmember_set.filter( + crew__event__slug=slug, state="confirmed" + ).exists() + ) + and not is_booking + ): raise PermissionDenied( "Du bist nicht berechtigt, Bandbewertungen abzugeben, bitte wende dich an die Crewkoordination und lasse dich für die Crew freischalten." ) - if ( - not Event.objects.get(slug=slug).bid_vote_allowed - and not request.user.groups.filter(name="booking").exists() - ): + if not (not Event.objects.get(slug=slug).bid_vote_allowed) or not is_booking: template = loader.get_template("errors/403.html") return HttpResponseForbidden( template.render( From 6d955a43bde724f9d5d6ae5894b079e85b14cc85 Mon Sep 17 00:00:00 2001 From: knuth Date: Fri, 6 Dec 2024 13:55:06 +0100 Subject: [PATCH 3/5] add blob to csp --- src/rockon/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rockon/settings.py b/src/rockon/settings.py index 5f65f5a..d2247a4 100644 --- a/src/rockon/settings.py +++ b/src/rockon/settings.py @@ -354,7 +354,7 @@ CSP_FONT_SRC = ["'self'"] CSP_OBJECT_SRC = ["'none'"] CSP_FRAME_SRC = ["'none'"] -CSP_MEDIA_SRC = ["'self'"] +CSP_MEDIA_SRC = ["'self'", "blob:"] CSP_FRAME_ANCESTORS = ["'none'"] CSP_FORM_ACTION = ["'self'"] CSP_BASE_URI = ["'self'"] From 2b0e821b230e83f0779b2780aeca0cd551d45553 Mon Sep 17 00:00:00 2001 From: knuth Date: Fri, 6 Dec 2024 14:05:41 +0100 Subject: [PATCH 4/5] add booking admin fields --- src/rockon/api/permissions/__init__.py | 2 +- src/rockon/api/permissions/bands.py | 28 ++++++++++++++++++++++++++ src/rockon/api/views/bands.py | 7 +++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/rockon/api/permissions/__init__.py b/src/rockon/api/permissions/__init__.py index 5b0a3f7..a3a2393 100644 --- a/src/rockon/api/permissions/__init__.py +++ b/src/rockon/api/permissions/__init__.py @@ -1,3 +1,3 @@ from __future__ import annotations -from .bands import IsCrewReadOnly, IsOwner +from .bands import BookingFieldsPermission, IsCrewReadOnly, IsOwner diff --git a/src/rockon/api/permissions/bands.py b/src/rockon/api/permissions/bands.py index 53e0649..efd760f 100644 --- a/src/rockon/api/permissions/bands.py +++ b/src/rockon/api/permissions/bands.py @@ -27,3 +27,31 @@ def has_object_permission(self, request, view, obj): and request.method in permissions.SAFE_METHODS ): return True + + +class IsInGroupForFields(permissions.BasePermission): + """ + Custom permission to only allow users in specific groups to update certain fields + """ + + group_name = None + protected_fields = [] + + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + return True + + if request.data: + has_protected_fields = any( + field in request.data for field in self.protected_fields + ) + + if has_protected_fields: + return request.user.groups.filter(name=self.group_name).exists() + + return True + + +class BookingFieldsPermission(IsInGroupForFields): + group_name = "booking" + protected_fields = ["track", "bid_status"] diff --git a/src/rockon/api/views/bands.py b/src/rockon/api/views/bands.py index c21b0b0..34a3499 100644 --- a/src/rockon/api/views/bands.py +++ b/src/rockon/api/views/bands.py @@ -6,7 +6,7 @@ from rest_framework.parsers import MultiPartParser from rest_framework.response import Response -from rockon.api.permissions import IsCrewReadOnly, IsOwner +from rockon.api.permissions import BookingFieldsPermission, IsCrewReadOnly, IsOwner from rockon.api.serializers import ( BandDetailSerializer, BandListSerializer, @@ -23,7 +23,10 @@ class BandViewSet(viewsets.ModelViewSet): """ queryset = Band.objects.all() - permission_classes = [IsCrewReadOnly | permissions.IsAdminUser | IsOwner] + permission_classes = [ + IsCrewReadOnly | permissions.IsAdminUser | IsOwner, + BookingFieldsPermission, + ] def get_serializer_class(self): if self.action == "list": From 358a22dc96a383125f0ca54dc5163238d4d4869c Mon Sep 17 00:00:00 2001 From: knuth Date: Fri, 6 Dec 2024 17:01:57 +0100 Subject: [PATCH 5/5] misc improvements for voting system --- src/rockon/bands/static/js/voting.js | 121 +++++++++++++-- src/rockon/bands/templates/bid_vote.html | 122 ++++++++------- .../bands/templates/booking/bid_overview.html | 140 ++++++++++-------- src/rockon/bands/views/bid.py | 4 + 4 files changed, 256 insertions(+), 131 deletions(-) diff --git a/src/rockon/bands/static/js/voting.js b/src/rockon/bands/static/js/voting.js index e4a8db8..de90de5 100644 --- a/src/rockon/bands/static/js/voting.js +++ b/src/rockon/bands/static/js/voting.js @@ -143,14 +143,15 @@ const TrackDropdown = Vue.defineComponent({ ) }, template: ` -
- +
+ +
`, methods: { @@ -166,6 +167,44 @@ const TrackDropdown = Vue.defineComponent({ } }) +const BidStatusDropdown = Vue.defineComponent({ + props: ['bidStates', 'selectedBandDetails'], + emits: ['update:bidStatus'], + template: ` +
+ + +
+ `, + methods: { + updateBidStatus (event) { + console.debug('BidStatusDropdown updateBidStatus:', event.target.value) + this.$emit('update:bidStatus', event.target.value) + } + } +}) + +const BackstageLink = Vue.defineComponent({ + props: ['selectedBandDetails'], + computed: { + url () { + return `/backstage/rockonbands/band/${this.selectedBandDetails.id}/change/` + } + }, + template: ` +
+ + + Band bearbeiten + +
+ ` +}) + const TrackList = Vue.defineComponent({ props: [ 'tracks', @@ -519,6 +558,7 @@ const BandRating = Vue.defineComponent({ const BandDetails = Vue.defineComponent({ props: [ 'tracks', + 'bidStates', 'media', 'federalStates', 'selectedBandDetails', @@ -528,6 +568,8 @@ const BandDetails = Vue.defineComponent({ emits: ['update:track', 'update:select-song', 'update:rating'], components: { TrackDropdown, + BackstageLink, + BidStatusDropdown, SongList, BandImages, BandDocuments, @@ -557,6 +599,9 @@ const BandDetails = Vue.defineComponent({ return 'Kein Cover Letter' } return this.selectedBandDetails.cover_letter.replace(/\r\n/g, '
') + }, + isUnknownOrPending() { + return this.selectedBandDetails.bid_status === 'unknown' || this.selectedBandDetails.bid_status === 'pending'; } }, template: ` @@ -565,13 +610,23 @@ const BandDetails = Vue.defineComponent({

{{ bandName }}

-
+
+
+
+
+
+
@@ -579,7 +634,7 @@ const BandDetails = Vue.defineComponent({
-
+
@@ -619,10 +674,16 @@ const BandDetails = Vue.defineComponent({
-

Track

+

Verwaltung

+
+ +
+
+ +

Booking

@@ -658,6 +719,10 @@ const BandDetails = Vue.defineComponent({ console.debug('BandDetails updateTrack:', trackId) this.$emit('update:track', trackId) }, + updateBidStatus (bidStatus) { + console.debug('BandDetails updateBidStatus:', bidStatus) + this.$emit('update:bidStatus', bidStatus) + }, handleSongSelect (song) { // Update the data with the selected song console.debug('BandDetails handleSongSelect:', song) @@ -671,7 +736,7 @@ const BandDetails = Vue.defineComponent({ console.debug('BandDetails emitRating:', rating) this.$emit('update:rating', rating) } - } + }, }) const app = createApp({ @@ -686,6 +751,7 @@ const app = createApp({ tracks: window.rockon_data.tracks, bands: [], federalStates: window.rockon_data.federal_states, + bidStates: window.rockon_data.bid_states, selectedTrack: null, selectedBand: null, userVotes: window.rockon_data.user_votes, @@ -709,6 +775,7 @@ const app = createApp({ BandList, BandDetails, TrackDropdown, + BidStatusDropdown, SongList, SongInfo, BandImages, @@ -812,6 +879,32 @@ const app = createApp({ this.selectedBandDetails.track = trackId this.selectedTrack = track }, + updateBidStatus (bidStatus) { + api_url = window.rockon_api.update_band.replace( + 'pk_placeholder', + this.selectedBandDetails.id + ) + console.debug( + 'Selected band:', + this.selectedBandDetails.id, + bidStatus, + api_url + ) + fetch(api_url, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': this.crsf_token + }, + body: JSON.stringify({ + bid_status: bidStatus + }) + }) + .then(response => response.json()) + .then(data => console.log('Success:', data)) + .catch(error => console.error('Error:', error)) + this.selectedBandDetails.bid_state = bidStatus + }, handleSongSelect (song) { console.debug('app handleSongSelect:', song) if (this.playSong === song) { diff --git a/src/rockon/bands/templates/bid_vote.html b/src/rockon/bands/templates/bid_vote.html index a7f1d80..6770d10 100644 --- a/src/rockon/bands/templates/bid_vote.html +++ b/src/rockon/bands/templates/bid_vote.html @@ -1,31 +1,27 @@ {% extends "base.html" %} {% block content %} -{% load static %} -{% load compress %} -{% csrf_token %} -

Bandbewertung

- -{% compress js file vue %} - -{% endcompress %} - -{% compress js file wavesurfer %} - -{% endcompress %} - -{% compress js file simplelightbox %} - -{% endcompress %} - -{% compress css file simplelightbox %} - -{% endcompress %} - - + {% endcompress %} + {% compress js file wavesurfer %} + + {% endcompress %} + {% compress js file simplelightbox %} + + {% endcompress %} + {% compress css file simplelightbox %} + + {% endcompress %} + - -{% verbatim %} -
- - - - -
+ + {% verbatim %} +
+ + + + +
- -
-
{% endverbatim %} - - {% compress js file voting %} - + {% endcompress %} - {% endblock content %} diff --git a/src/rockon/bands/templates/booking/bid_overview.html b/src/rockon/bands/templates/booking/bid_overview.html index 87077bd..80ec1b9 100644 --- a/src/rockon/bands/templates/booking/bid_overview.html +++ b/src/rockon/bands/templates/booking/bid_overview.html @@ -1,26 +1,24 @@ -{% extends 'base.html' %} {% block content %} -{% load static %} -{% load compress %} - -

Band Bewertungen

- -{% compress css file datatables %} - -{% endcompress %} - -{% compress js file datatables %} - - - - - -{% endcompress %} - -{% compress js file sparklines %} - -{% endcompress %} - - + + + + + {% endcompress %} + {% compress js file sparklines %} + + {% endcompress %} + - - - -
-
- - - - - - - - - - - - - - - - {% for band in bands %} - - - - - - - - - - - + +
+
+
BandnameØ ⭐SummeStimmenVerteilungStatusTrackKontaktBackstage
{{ band.name|default_if_none:band.guid }}{{ band.votes_avg|default:"0" |floatformat }}{{ band.votes_sum|default:"0" }}{{ band.votes_count|default:"0" }}{% for _, v in band.counters.items %}{{v}}{% if not forloop.last %},{% endif %}{% endfor %}{{ band.bid_status }}{{ band.track.name }}{{ band.contact.email }}
+ + + + + + + + + + + + + + + {% for band in bands %} + + + + + + -
BandnameØ ⭐SummeStimmenVerteilungStatusTrackKontaktBackstage
{{ band.name|default_if_none:band.guid }}{{ band.votes_avg|default:"0" |floatformat }}{{ band.votes_sum|default:"0" }}{{ band.votes_count|default:"0" }} + {% for _, v in band.counters.items %} + {{ v }} + {% if not forloop.last %},{% endif %} {% endfor %} -
-
+ + {{ band.bid_status }} + {{ band.track.name }} + + {{ band.contact.email }} + + + + + + +{% endfor %} + + +
- -{% endblock %} +{% endblock content %} diff --git a/src/rockon/bands/views/bid.py b/src/rockon/bands/views/bid.py index 9e9dd01..569e83e 100644 --- a/src/rockon/bands/views/bid.py +++ b/src/rockon/bands/views/bid.py @@ -14,6 +14,7 @@ from django.utils.safestring import mark_safe from rockon.bands.models import Band, BandMedia, MediaType, Track +from rockon.bands.models.band import BidStatus from rockon.base.models import Event from rockon.library.decorators import check_band_application_open from rockon.library.federal_states import FederalState @@ -130,6 +131,8 @@ def bid_vote(request, bid: str = None, track: str = None, slug: str = None): template = loader.get_template("bid_vote.html") tracks = Track.objects.filter(events__slug=slug) tracks_json = mark_safe(json.dumps(list(tracks.values()), cls=CustomJSONEncoder)) + bid_states = BidStatus.choices + bid_states_json = mark_safe(json.dumps(bid_states)) federal_states = FederalState.choices federal_states_json = mark_safe(json.dumps(federal_states)) track_slug_json = mark_safe(json.dumps(track, cls=CustomJSONEncoder)) @@ -147,6 +150,7 @@ def bid_vote(request, bid: str = None, track: str = None, slug: str = None): "site_title": "Band Bewertung", "tracks": tracks_json, "federal_states": federal_states_json, + "bid_states": bid_states_json, "trackid": track_slug_json, "bandid": band_guid_json, "user_votes": user_votes_json,