diff --git a/schedule/src/Views/EventDetail.elm b/schedule/src/Views/EventDetail.elm index 97bb69b0..dc08da2e 100644 --- a/schedule/src/Views/EventDetail.elm +++ b/schedule/src/Views/EventDetail.elm @@ -154,6 +154,9 @@ eventMetaDataSidebar event eventInstances model = [] (List.map (\ei -> li [] <| eventInstanceItem ei model) instances) ] + + feedbackUrl = + [a [href <| "https://bornhack.dk/" ++ model.flags.camp_slug ++ "/program/" ++ event.slug ++ "/feedback/create/" ] [text "Give feedback"]] in div [] ([ h4 [] [ text "Metadata" ] @@ -170,6 +173,7 @@ eventMetaDataSidebar event eventInstances model = ) ] ++ eventInstanceMetaData + ++ feedbackUrl ) diff --git a/src/backoffice/templates/approve_feedback.html b/src/backoffice/templates/approve_feedback.html new file mode 100644 index 00000000..6fa62e0f --- /dev/null +++ b/src/backoffice/templates/approve_feedback.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} +{% load bornhack %} + +{% block content %} +
Use this view to manage SpeakerProposals and EventProposals
+ +Use this view to approve or reject EventFeedback
+ {% endif %} {% if perms.camps.orgateam_permission %} diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index b24d15e5..2e5d231b 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -1,6 +1,7 @@ from django.urls import include, path from .views import ( + ApproveFeedbackView, ApproveNamesView, BackofficeIndexView, BadgeHandoutView, @@ -80,6 +81,10 @@ urlpatterns = [ ] ), ), + # approve eventfeedback objects + path( + "approve_feedback", ApproveFeedbackView.as_view(), name="approve_eventfeedback", + ), # economy path( "economy/", diff --git a/src/backoffice/views.py b/src/backoffice/views.py index 322ea296..fa470951 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -2,22 +2,22 @@ import logging import os from itertools import chain +from camps.mixins import CampViewMixin from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.core.files import File from django.db.models import Sum +from django.forms import modelformset_factory from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils import timezone from django.views.generic import DetailView, ListView, TemplateView -from django.views.generic.edit import CreateView, DeleteView, UpdateView - -from camps.mixins import CampViewMixin +from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from economy.models import Chain, Credebtor, Expense, Reimbursement, Revenue from profiles.models import Profile -from program.models import EventProposal, SpeakerProposal +from program.models import EventFeedback, EventProposal, SpeakerProposal from shop.models import Order, OrderProductRelation from teams.models import Team from tickets.models import DiscountTicket, ShopTicket, SponsorTicket, TicketType @@ -86,6 +86,55 @@ class ApproveNamesView(CampViewMixin, OrgaTeamPermissionMixin, ListView): ) +class ApproveFeedbackView(CampViewMixin, ContentTeamPermissionMixin, FormView): + """ + This view shows a list of EventFeedback objects which are pending approval. + """ + + model = EventFeedback + template_name = "approve_feedback.html" + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + self.queryset = EventFeedback.objects.filter( + event__track__camp=self.camp, approved__isnull=True + ) + + self.form_class = modelformset_factory( + EventFeedback, + fields=("approved",), + min_num=self.queryset.count(), + validate_min=True, + max_num=self.queryset.count(), + validate_max=True, + extra=0, + ) + + def get_context_data(self, *args, **kwargs): + """ + Include the queryset used for the modelformset_factory so we have + some idea which object is which in the template + Why the hell do the forms in the formset not include the object? + """ + context = super().get_context_data(*args, **kwargs) + context["eventfeedback_list"] = self.queryset + context["formset"] = self.form_class(queryset=self.queryset) + return context + + def form_valid(self, form): + form.save() + if form.changed_objects: + messages.success( + self.request, f"Updated {len(form.changed_objects)} EventFeedbacks" + ) + return redirect(self.get_success_url()) + + def get_success_url(self, *args, **kwargs): + return reverse( + "backoffice:approve_eventfeedback", kwargs={"camp_slug": self.camp.slug} + ) + + class ManageProposalsView(CampViewMixin, ContentTeamPermissionMixin, ListView): """ This view shows a list of pending SpeakerProposal and EventProposals. @@ -118,7 +167,6 @@ class ProposalManageBaseView(CampViewMixin, ContentTeamPermissionMixin, UpdateVi """ We have two submit buttons in this form, Approve and Reject """ - logger.debug(form.data) if "approve" in form.data: # approve button was pressed form.instance.mark_as_approved(self.request) diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py index fe5df780..da4c5027 100644 --- a/src/bornhack/settings.py +++ b/src/bornhack/settings.py @@ -123,6 +123,7 @@ MIDDLEWARE = [ "django_otp.middleware.OTPMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "utils.middleware.RedirectExceptionMiddleware", ] CORS_ORIGIN_ALLOW_ALL = True diff --git a/src/camps/mixins.py b/src/camps/mixins.py index dafa8c67..c8edf6bd 100644 --- a/src/camps/mixins.py +++ b/src/camps/mixins.py @@ -3,15 +3,15 @@ from django.shortcuts import get_object_or_404 from camps.models import Camp -class CampViewMixin(object): +class CampViewMixin: """ This mixin makes sure self.camp is available (taken from url kwarg camp_slug) It also filters out objects that belong to other camps when the queryset has a camp_filter """ - def dispatch(self, request, *args, **kwargs): + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) self.camp = get_object_or_404(Camp, slug=self.kwargs["camp_slug"]) - return super().dispatch(request, *args, **kwargs) def get_queryset(self): queryset = super().get_queryset() diff --git a/src/camps/views.py b/src/camps/views.py index f975c90f..0c990ea9 100644 --- a/src/camps/views.py +++ b/src/camps/views.py @@ -6,13 +6,12 @@ from django.utils import timezone from django.views import View from django.views.generic import DetailView, ListView -from .mixins import CampViewMixin from .models import Camp logger = logging.getLogger("bornhack.%s" % __name__) -class CampRedirectView(CampViewMixin, View): +class CampRedirectView(View): def dispatch(self, request, *args, **kwargs): now = timezone.now() diff --git a/src/program/admin.py b/src/program/admin.py index fbedcded..b0549617 100644 --- a/src/program/admin.py +++ b/src/program/admin.py @@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError from .models import ( Event, + EventFeedback, EventInstance, EventLocation, EventProposal, @@ -120,3 +121,27 @@ class UrlTypeAdmin(admin.ModelAdmin): @admin.register(Url) class UrlAdmin(admin.ModelAdmin): pass + + +@admin.register(EventFeedback) +class EventFeedbackAdmin(admin.ModelAdmin): + list_display = [ + "camp", + "user", + "event", + "expectations_fulfilled", + "attend_speaker_again", + "rating", + "created", + "updated", + "approved", + "comment", + ] + list_filter = [ + "event__track__camp", + "expectations_fulfilled", + "attend_speaker_again", + "rating", + "approved", + ] + search_fields = ["event__title", "user__username"] diff --git a/src/program/migrations/0075_eventfeedback.py b/src/program/migrations/0075_eventfeedback.py new file mode 100644 index 00000000..0bc8d335 --- /dev/null +++ b/src/program/migrations/0075_eventfeedback.py @@ -0,0 +1,77 @@ +# Generated by Django 3.0.3 on 2020-02-13 16:51 + +import uuid + +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), + ("program", "0074_auto_20190801_0933"), + ] + + operations = [ + migrations.CreateModel( + name="EventFeedback", + fields=[ + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ( + "expectations_fulfilled", + models.BooleanField( + choices=[(True, "Yes"), (False, "No")], + help_text="Did the event live up to your expectations?", + ), + ), + ( + "attend_speaker_again", + models.BooleanField( + choices=[(True, "Yes"), (False, "No")], + help_text="Would you attend another event with the same speaker?", + ), + ), + ( + "rating", + models.IntegerField( + choices=[(0, "0"), (1, "1"), (2, "2"), (3, "3"), (4, "4")], + help_text="Rating/Score (5 is best)", + ), + ), + ( + "feedback", + models.TextField(help_text="Any other comments or feedback?"), + ), + ( + "event", + models.ForeignKey( + help_text="The Event this feedback is about", + on_delete=django.db.models.deletion.PROTECT, + related_name="feedbacks", + to="program.Event", + ), + ), + ( + "user", + models.ForeignKey( + help_text="The User who wrote this feedback", + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={"abstract": False}, + ), + ] diff --git a/src/program/migrations/0076_auto_20200214_0143.py b/src/program/migrations/0076_auto_20200214_0143.py new file mode 100644 index 00000000..55a91160 --- /dev/null +++ b/src/program/migrations/0076_auto_20200214_0143.py @@ -0,0 +1,34 @@ +# Generated by Django 3.0.3 on 2020-02-14 00:43 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("program", "0075_eventfeedback"), + ] + + operations = [ + migrations.AddField( + model_name="eventfeedback", + name="approved", + field=models.BooleanField( + default=False, + help_text="Approve feedback? It will not be visible to Event owner before it is approved.", + ), + ), + migrations.AlterField( + model_name="eventfeedback", + name="rating", + field=models.IntegerField( + choices=[(0, "0"), (1, "1"), (2, "2"), (3, "3"), (4, "4"), (5, "5")], + help_text="Rating/Score (5 is best)", + ), + ), + migrations.AlterUniqueTogether( + name="eventfeedback", unique_together={("user", "event")}, + ), + ] diff --git a/src/program/migrations/0077_auto_20200214_0246.py b/src/program/migrations/0077_auto_20200214_0246.py new file mode 100644 index 00000000..50c23525 --- /dev/null +++ b/src/program/migrations/0077_auto_20200214_0246.py @@ -0,0 +1,16 @@ +# Generated by Django 3.0.3 on 2020-02-14 01:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("program", "0076_auto_20200214_0143"), + ] + + operations = [ + migrations.RenameField( + model_name="eventfeedback", old_name="feedback", new_name="comment", + ), + ] diff --git a/src/program/migrations/0078_auto_20200214_2100.py b/src/program/migrations/0078_auto_20200214_2100.py new file mode 100644 index 00000000..7d1b2e1a --- /dev/null +++ b/src/program/migrations/0078_auto_20200214_2100.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.3 on 2020-02-14 20:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("program", "0077_auto_20200214_0246"), + ] + + operations = [ + migrations.AlterField( + model_name="eventfeedback", + name="approved", + field=models.NullBooleanField( + help_text="Approve feedback? It will not be visible to the Event owner before it is approved." + ), + ), + migrations.AlterField( + model_name="eventfeedback", + name="comment", + field=models.TextField( + blank=True, help_text="Any other comments or feedback?" + ), + ), + ] diff --git a/src/program/mixins.py b/src/program/mixins.py index 8f86978b..f26cfcba 100644 --- a/src/program/mixins.py +++ b/src/program/mixins.py @@ -1,3 +1,4 @@ +from camps.mixins import CampViewMixin from django.contrib import messages from django.http import Http404 from django.shortcuts import get_object_or_404, redirect @@ -109,3 +110,35 @@ class UrlViewMixin(object): return self.eventproposal.get_absolute_url() else: return self.speakerproposal.get_absolute_url() + + +class EventViewMixin(CampViewMixin): + """ + A mixin to get the Event object based on event_uuid in url kwargs + """ + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + self.event = get_object_or_404( + models.Event, track__camp=self.camp, slug=self.kwargs["event_slug"] + ) + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["event"] = self.event + return context + + +class EventFeedbackViewMixin(EventViewMixin): + """ + A mixin to get the EventFeedback object based on self.event and self.request.user + """ + + def setup(self, *args, **kwargs): + super().setup(*args, **kwargs) + self.eventfeedback = get_object_or_404( + models.EventFeedback, event=self.event, user=self.request.user, + ) + + def get_object(self): + return self.eventfeedback diff --git a/src/program/models.py b/src/program/models.py index 6238a790..35c9d7ca 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -10,10 +10,9 @@ from django.contrib.postgres.fields import DateTimeRangeField from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.files.storage import FileSystemStorage from django.db import models -from django.urls import reverse_lazy +from django.urls import reverse, reverse_lazy from django.utils.text import slugify - -from utils.models import CampRelatedModel, CreatedUpdatedModel +from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel logger = logging.getLogger("bornhack.%s" % __name__) @@ -594,7 +593,7 @@ class Event(CampRelatedModel): def get_absolute_url(self): return reverse_lazy( "program:event_detail", - kwargs={"camp_slug": self.camp.slug, "slug": self.slug}, + kwargs={"camp_slug": self.camp.slug, "event_slug": self.slug}, ) def serialize(self): @@ -795,6 +794,67 @@ class Favorite(models.Model): unique_together = ["user", "event_instance"] +############################################################################### + + +class EventFeedback(CampRelatedModel, UUIDModel): + """ + This model contains all feedback for Events + Each user can submit exactly one feedback per Event + """ + + class Meta: + unique_together = [("user", "event")] + + YESNO_CHOICES = [(True, "Yes"), (False, "No")] + + user = models.ForeignKey( + "auth.User", + on_delete=models.PROTECT, + help_text="The User who wrote this feedback", + ) + + event = models.ForeignKey( + "program.event", + related_name="feedbacks", + on_delete=models.PROTECT, + help_text="The Event this feedback is about", + ) + + expectations_fulfilled = models.BooleanField( + choices=YESNO_CHOICES, help_text="Did the event live up to your expectations?", + ) + + attend_speaker_again = models.BooleanField( + choices=YESNO_CHOICES, + help_text="Would you attend another event with the same speaker?", + ) + + RATING_CHOICES = [(n, f"{n}") for n in range(0, 6)] + + rating = models.IntegerField( + choices=RATING_CHOICES, help_text="Rating/Score (5 is best)", + ) + + comment = models.TextField(blank=True, help_text="Any other comments or feedback?") + + approved = models.NullBooleanField( + help_text="Approve feedback? It will not be visible to the Event owner before it is approved." + ) + + @property + def camp(self): + return self.event.camp + + camp_filter = "event__track__camp" + + def get_absolute_url(self): + return reverse( + "program:eventfeedback_detail", + kwargs={"camp_slug": self.camp.slug, "event_slug": self.event.slug}, + ) + + # classes and functions below here was used by picture handling for speakers before it was removed in May 2018 by tyk diff --git a/src/program/static/js/elm_based_schedule.js b/src/program/static/js/elm_based_schedule.js index 1ce403e8..63185e4f 100644 --- a/src/program/static/js/elm_based_schedule.js +++ b/src/program/static/js/elm_based_schedule.js @@ -5851,418 +5851,6 @@ var _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required = F3( decoder); }); -//import Native.Scheduler // - -var _elm_lang$core$Native_Time = function() { - -var now = _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) -{ - callback(_elm_lang$core$Native_Scheduler.succeed(Date.now())); -}); - -function setInterval_(interval, task) -{ - return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) - { - var id = setInterval(function() { - _elm_lang$core$Native_Scheduler.rawSpawn(task); - }, interval); - - return function() { clearInterval(id); }; - }); -} - -return { - now: now, - setInterval_: F2(setInterval_) -}; - -}(); -var _elm_lang$core$Task$onError = _elm_lang$core$Native_Scheduler.onError; -var _elm_lang$core$Task$andThen = _elm_lang$core$Native_Scheduler.andThen; -var _elm_lang$core$Task$spawnCmd = F2( - function (router, _p0) { - var _p1 = _p0; - return _elm_lang$core$Native_Scheduler.spawn( - A2( - _elm_lang$core$Task$andThen, - _elm_lang$core$Platform$sendToApp(router), - _p1._0)); - }); -var _elm_lang$core$Task$fail = _elm_lang$core$Native_Scheduler.fail; -var _elm_lang$core$Task$mapError = F2( - function (convert, task) { - return A2( - _elm_lang$core$Task$onError, - function (_p2) { - return _elm_lang$core$Task$fail( - convert(_p2)); - }, - task); - }); -var _elm_lang$core$Task$succeed = _elm_lang$core$Native_Scheduler.succeed; -var _elm_lang$core$Task$map = F2( - function (func, taskA) { - return A2( - _elm_lang$core$Task$andThen, - function (a) { - return _elm_lang$core$Task$succeed( - func(a)); - }, - taskA); - }); -var _elm_lang$core$Task$map2 = F3( - function (func, taskA, taskB) { - return A2( - _elm_lang$core$Task$andThen, - function (a) { - return A2( - _elm_lang$core$Task$andThen, - function (b) { - return _elm_lang$core$Task$succeed( - A2(func, a, b)); - }, - taskB); - }, - taskA); - }); -var _elm_lang$core$Task$map3 = F4( - function (func, taskA, taskB, taskC) { - return A2( - _elm_lang$core$Task$andThen, - function (a) { - return A2( - _elm_lang$core$Task$andThen, - function (b) { - return A2( - _elm_lang$core$Task$andThen, - function (c) { - return _elm_lang$core$Task$succeed( - A3(func, a, b, c)); - }, - taskC); - }, - taskB); - }, - taskA); - }); -var _elm_lang$core$Task$map4 = F5( - function (func, taskA, taskB, taskC, taskD) { - return A2( - _elm_lang$core$Task$andThen, - function (a) { - return A2( - _elm_lang$core$Task$andThen, - function (b) { - return A2( - _elm_lang$core$Task$andThen, - function (c) { - return A2( - _elm_lang$core$Task$andThen, - function (d) { - return _elm_lang$core$Task$succeed( - A4(func, a, b, c, d)); - }, - taskD); - }, - taskC); - }, - taskB); - }, - taskA); - }); -var _elm_lang$core$Task$map5 = F6( - function (func, taskA, taskB, taskC, taskD, taskE) { - return A2( - _elm_lang$core$Task$andThen, - function (a) { - return A2( - _elm_lang$core$Task$andThen, - function (b) { - return A2( - _elm_lang$core$Task$andThen, - function (c) { - return A2( - _elm_lang$core$Task$andThen, - function (d) { - return A2( - _elm_lang$core$Task$andThen, - function (e) { - return _elm_lang$core$Task$succeed( - A5(func, a, b, c, d, e)); - }, - taskE); - }, - taskD); - }, - taskC); - }, - taskB); - }, - taskA); - }); -var _elm_lang$core$Task$sequence = function (tasks) { - var _p3 = tasks; - if (_p3.ctor === '[]') { - return _elm_lang$core$Task$succeed( - {ctor: '[]'}); - } else { - return A3( - _elm_lang$core$Task$map2, - F2( - function (x, y) { - return {ctor: '::', _0: x, _1: y}; - }), - _p3._0, - _elm_lang$core$Task$sequence(_p3._1)); - } -}; -var _elm_lang$core$Task$onEffects = F3( - function (router, commands, state) { - return A2( - _elm_lang$core$Task$map, - function (_p4) { - return {ctor: '_Tuple0'}; - }, - _elm_lang$core$Task$sequence( - A2( - _elm_lang$core$List$map, - _elm_lang$core$Task$spawnCmd(router), - commands))); - }); -var _elm_lang$core$Task$init = _elm_lang$core$Task$succeed( - {ctor: '_Tuple0'}); -var _elm_lang$core$Task$onSelfMsg = F3( - function (_p7, _p6, _p5) { - return _elm_lang$core$Task$succeed( - {ctor: '_Tuple0'}); - }); -var _elm_lang$core$Task$command = _elm_lang$core$Native_Platform.leaf('Task'); -var _elm_lang$core$Task$Perform = function (a) { - return {ctor: 'Perform', _0: a}; -}; -var _elm_lang$core$Task$perform = F2( - function (toMessage, task) { - return _elm_lang$core$Task$command( - _elm_lang$core$Task$Perform( - A2(_elm_lang$core$Task$map, toMessage, task))); - }); -var _elm_lang$core$Task$attempt = F2( - function (resultToMessage, task) { - return _elm_lang$core$Task$command( - _elm_lang$core$Task$Perform( - A2( - _elm_lang$core$Task$onError, - function (_p8) { - return _elm_lang$core$Task$succeed( - resultToMessage( - _elm_lang$core$Result$Err(_p8))); - }, - A2( - _elm_lang$core$Task$andThen, - function (_p9) { - return _elm_lang$core$Task$succeed( - resultToMessage( - _elm_lang$core$Result$Ok(_p9))); - }, - task)))); - }); -var _elm_lang$core$Task$cmdMap = F2( - function (tagger, _p10) { - var _p11 = _p10; - return _elm_lang$core$Task$Perform( - A2(_elm_lang$core$Task$map, tagger, _p11._0)); - }); -_elm_lang$core$Native_Platform.effectManagers['Task'] = {pkg: 'elm-lang/core', init: _elm_lang$core$Task$init, onEffects: _elm_lang$core$Task$onEffects, onSelfMsg: _elm_lang$core$Task$onSelfMsg, tag: 'cmd', cmdMap: _elm_lang$core$Task$cmdMap}; - -var _elm_lang$core$Time$setInterval = _elm_lang$core$Native_Time.setInterval_; -var _elm_lang$core$Time$spawnHelp = F3( - function (router, intervals, processes) { - var _p0 = intervals; - if (_p0.ctor === '[]') { - return _elm_lang$core$Task$succeed(processes); - } else { - var _p1 = _p0._0; - var spawnRest = function (id) { - return A3( - _elm_lang$core$Time$spawnHelp, - router, - _p0._1, - A3(_elm_lang$core$Dict$insert, _p1, id, processes)); - }; - var spawnTimer = _elm_lang$core$Native_Scheduler.spawn( - A2( - _elm_lang$core$Time$setInterval, - _p1, - A2(_elm_lang$core$Platform$sendToSelf, router, _p1))); - return A2(_elm_lang$core$Task$andThen, spawnRest, spawnTimer); - } - }); -var _elm_lang$core$Time$addMySub = F2( - function (_p2, state) { - var _p3 = _p2; - var _p6 = _p3._1; - var _p5 = _p3._0; - var _p4 = A2(_elm_lang$core$Dict$get, _p5, state); - if (_p4.ctor === 'Nothing') { - return A3( - _elm_lang$core$Dict$insert, - _p5, - { - ctor: '::', - _0: _p6, - _1: {ctor: '[]'} - }, - state); - } else { - return A3( - _elm_lang$core$Dict$insert, - _p5, - {ctor: '::', _0: _p6, _1: _p4._0}, - state); - } - }); -var _elm_lang$core$Time$inMilliseconds = function (t) { - return t; -}; -var _elm_lang$core$Time$millisecond = 1; -var _elm_lang$core$Time$second = 1000 * _elm_lang$core$Time$millisecond; -var _elm_lang$core$Time$minute = 60 * _elm_lang$core$Time$second; -var _elm_lang$core$Time$hour = 60 * _elm_lang$core$Time$minute; -var _elm_lang$core$Time$inHours = function (t) { - return t / _elm_lang$core$Time$hour; -}; -var _elm_lang$core$Time$inMinutes = function (t) { - return t / _elm_lang$core$Time$minute; -}; -var _elm_lang$core$Time$inSeconds = function (t) { - return t / _elm_lang$core$Time$second; -}; -var _elm_lang$core$Time$now = _elm_lang$core$Native_Time.now; -var _elm_lang$core$Time$onSelfMsg = F3( - function (router, interval, state) { - var _p7 = A2(_elm_lang$core$Dict$get, interval, state.taggers); - if (_p7.ctor === 'Nothing') { - return _elm_lang$core$Task$succeed(state); - } else { - var tellTaggers = function (time) { - return _elm_lang$core$Task$sequence( - A2( - _elm_lang$core$List$map, - function (tagger) { - return A2( - _elm_lang$core$Platform$sendToApp, - router, - tagger(time)); - }, - _p7._0)); - }; - return A2( - _elm_lang$core$Task$andThen, - function (_p8) { - return _elm_lang$core$Task$succeed(state); - }, - A2(_elm_lang$core$Task$andThen, tellTaggers, _elm_lang$core$Time$now)); - } - }); -var _elm_lang$core$Time$subscription = _elm_lang$core$Native_Platform.leaf('Time'); -var _elm_lang$core$Time$State = F2( - function (a, b) { - return {taggers: a, processes: b}; - }); -var _elm_lang$core$Time$init = _elm_lang$core$Task$succeed( - A2(_elm_lang$core$Time$State, _elm_lang$core$Dict$empty, _elm_lang$core$Dict$empty)); -var _elm_lang$core$Time$onEffects = F3( - function (router, subs, _p9) { - var _p10 = _p9; - var rightStep = F3( - function (_p12, id, _p11) { - var _p13 = _p11; - return { - ctor: '_Tuple3', - _0: _p13._0, - _1: _p13._1, - _2: A2( - _elm_lang$core$Task$andThen, - function (_p14) { - return _p13._2; - }, - _elm_lang$core$Native_Scheduler.kill(id)) - }; - }); - var bothStep = F4( - function (interval, taggers, id, _p15) { - var _p16 = _p15; - return { - ctor: '_Tuple3', - _0: _p16._0, - _1: A3(_elm_lang$core$Dict$insert, interval, id, _p16._1), - _2: _p16._2 - }; - }); - var leftStep = F3( - function (interval, taggers, _p17) { - var _p18 = _p17; - return { - ctor: '_Tuple3', - _0: {ctor: '::', _0: interval, _1: _p18._0}, - _1: _p18._1, - _2: _p18._2 - }; - }); - var newTaggers = A3(_elm_lang$core$List$foldl, _elm_lang$core$Time$addMySub, _elm_lang$core$Dict$empty, subs); - var _p19 = A6( - _elm_lang$core$Dict$merge, - leftStep, - bothStep, - rightStep, - newTaggers, - _p10.processes, - { - ctor: '_Tuple3', - _0: {ctor: '[]'}, - _1: _elm_lang$core$Dict$empty, - _2: _elm_lang$core$Task$succeed( - {ctor: '_Tuple0'}) - }); - var spawnList = _p19._0; - var existingDict = _p19._1; - var killTask = _p19._2; - return A2( - _elm_lang$core$Task$andThen, - function (newProcesses) { - return _elm_lang$core$Task$succeed( - A2(_elm_lang$core$Time$State, newTaggers, newProcesses)); - }, - A2( - _elm_lang$core$Task$andThen, - function (_p20) { - return A3(_elm_lang$core$Time$spawnHelp, router, spawnList, existingDict); - }, - killTask)); - }); -var _elm_lang$core$Time$Every = F2( - function (a, b) { - return {ctor: 'Every', _0: a, _1: b}; - }); -var _elm_lang$core$Time$every = F2( - function (interval, tagger) { - return _elm_lang$core$Time$subscription( - A2(_elm_lang$core$Time$Every, interval, tagger)); - }); -var _elm_lang$core$Time$subMap = F2( - function (f, _p21) { - var _p22 = _p21; - return A2( - _elm_lang$core$Time$Every, - _p22._0, - function (_p23) { - return f( - _p22._1(_p23)); - }); - }); -_elm_lang$core$Native_Platform.effectManagers['Time'] = {pkg: 'elm-lang/core', init: _elm_lang$core$Time$init, onEffects: _elm_lang$core$Time$onEffects, onSelfMsg: _elm_lang$core$Time$onSelfMsg, tag: 'sub', subMap: _elm_lang$core$Time$subMap}; - var _elm_lang$core$Set$foldr = F3( function (f, b, _p0) { var _p1 = _p0; @@ -7635,6 +7223,418 @@ return { }; }(); +var _elm_lang$core$Task$onError = _elm_lang$core$Native_Scheduler.onError; +var _elm_lang$core$Task$andThen = _elm_lang$core$Native_Scheduler.andThen; +var _elm_lang$core$Task$spawnCmd = F2( + function (router, _p0) { + var _p1 = _p0; + return _elm_lang$core$Native_Scheduler.spawn( + A2( + _elm_lang$core$Task$andThen, + _elm_lang$core$Platform$sendToApp(router), + _p1._0)); + }); +var _elm_lang$core$Task$fail = _elm_lang$core$Native_Scheduler.fail; +var _elm_lang$core$Task$mapError = F2( + function (convert, task) { + return A2( + _elm_lang$core$Task$onError, + function (_p2) { + return _elm_lang$core$Task$fail( + convert(_p2)); + }, + task); + }); +var _elm_lang$core$Task$succeed = _elm_lang$core$Native_Scheduler.succeed; +var _elm_lang$core$Task$map = F2( + function (func, taskA) { + return A2( + _elm_lang$core$Task$andThen, + function (a) { + return _elm_lang$core$Task$succeed( + func(a)); + }, + taskA); + }); +var _elm_lang$core$Task$map2 = F3( + function (func, taskA, taskB) { + return A2( + _elm_lang$core$Task$andThen, + function (a) { + return A2( + _elm_lang$core$Task$andThen, + function (b) { + return _elm_lang$core$Task$succeed( + A2(func, a, b)); + }, + taskB); + }, + taskA); + }); +var _elm_lang$core$Task$map3 = F4( + function (func, taskA, taskB, taskC) { + return A2( + _elm_lang$core$Task$andThen, + function (a) { + return A2( + _elm_lang$core$Task$andThen, + function (b) { + return A2( + _elm_lang$core$Task$andThen, + function (c) { + return _elm_lang$core$Task$succeed( + A3(func, a, b, c)); + }, + taskC); + }, + taskB); + }, + taskA); + }); +var _elm_lang$core$Task$map4 = F5( + function (func, taskA, taskB, taskC, taskD) { + return A2( + _elm_lang$core$Task$andThen, + function (a) { + return A2( + _elm_lang$core$Task$andThen, + function (b) { + return A2( + _elm_lang$core$Task$andThen, + function (c) { + return A2( + _elm_lang$core$Task$andThen, + function (d) { + return _elm_lang$core$Task$succeed( + A4(func, a, b, c, d)); + }, + taskD); + }, + taskC); + }, + taskB); + }, + taskA); + }); +var _elm_lang$core$Task$map5 = F6( + function (func, taskA, taskB, taskC, taskD, taskE) { + return A2( + _elm_lang$core$Task$andThen, + function (a) { + return A2( + _elm_lang$core$Task$andThen, + function (b) { + return A2( + _elm_lang$core$Task$andThen, + function (c) { + return A2( + _elm_lang$core$Task$andThen, + function (d) { + return A2( + _elm_lang$core$Task$andThen, + function (e) { + return _elm_lang$core$Task$succeed( + A5(func, a, b, c, d, e)); + }, + taskE); + }, + taskD); + }, + taskC); + }, + taskB); + }, + taskA); + }); +var _elm_lang$core$Task$sequence = function (tasks) { + var _p3 = tasks; + if (_p3.ctor === '[]') { + return _elm_lang$core$Task$succeed( + {ctor: '[]'}); + } else { + return A3( + _elm_lang$core$Task$map2, + F2( + function (x, y) { + return {ctor: '::', _0: x, _1: y}; + }), + _p3._0, + _elm_lang$core$Task$sequence(_p3._1)); + } +}; +var _elm_lang$core$Task$onEffects = F3( + function (router, commands, state) { + return A2( + _elm_lang$core$Task$map, + function (_p4) { + return {ctor: '_Tuple0'}; + }, + _elm_lang$core$Task$sequence( + A2( + _elm_lang$core$List$map, + _elm_lang$core$Task$spawnCmd(router), + commands))); + }); +var _elm_lang$core$Task$init = _elm_lang$core$Task$succeed( + {ctor: '_Tuple0'}); +var _elm_lang$core$Task$onSelfMsg = F3( + function (_p7, _p6, _p5) { + return _elm_lang$core$Task$succeed( + {ctor: '_Tuple0'}); + }); +var _elm_lang$core$Task$command = _elm_lang$core$Native_Platform.leaf('Task'); +var _elm_lang$core$Task$Perform = function (a) { + return {ctor: 'Perform', _0: a}; +}; +var _elm_lang$core$Task$perform = F2( + function (toMessage, task) { + return _elm_lang$core$Task$command( + _elm_lang$core$Task$Perform( + A2(_elm_lang$core$Task$map, toMessage, task))); + }); +var _elm_lang$core$Task$attempt = F2( + function (resultToMessage, task) { + return _elm_lang$core$Task$command( + _elm_lang$core$Task$Perform( + A2( + _elm_lang$core$Task$onError, + function (_p8) { + return _elm_lang$core$Task$succeed( + resultToMessage( + _elm_lang$core$Result$Err(_p8))); + }, + A2( + _elm_lang$core$Task$andThen, + function (_p9) { + return _elm_lang$core$Task$succeed( + resultToMessage( + _elm_lang$core$Result$Ok(_p9))); + }, + task)))); + }); +var _elm_lang$core$Task$cmdMap = F2( + function (tagger, _p10) { + var _p11 = _p10; + return _elm_lang$core$Task$Perform( + A2(_elm_lang$core$Task$map, tagger, _p11._0)); + }); +_elm_lang$core$Native_Platform.effectManagers['Task'] = {pkg: 'elm-lang/core', init: _elm_lang$core$Task$init, onEffects: _elm_lang$core$Task$onEffects, onSelfMsg: _elm_lang$core$Task$onSelfMsg, tag: 'cmd', cmdMap: _elm_lang$core$Task$cmdMap}; + +//import Native.Scheduler // + +var _elm_lang$core$Native_Time = function() { + +var now = _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) +{ + callback(_elm_lang$core$Native_Scheduler.succeed(Date.now())); +}); + +function setInterval_(interval, task) +{ + return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) + { + var id = setInterval(function() { + _elm_lang$core$Native_Scheduler.rawSpawn(task); + }, interval); + + return function() { clearInterval(id); }; + }); +} + +return { + now: now, + setInterval_: F2(setInterval_) +}; + +}(); +var _elm_lang$core$Time$setInterval = _elm_lang$core$Native_Time.setInterval_; +var _elm_lang$core$Time$spawnHelp = F3( + function (router, intervals, processes) { + var _p0 = intervals; + if (_p0.ctor === '[]') { + return _elm_lang$core$Task$succeed(processes); + } else { + var _p1 = _p0._0; + var spawnRest = function (id) { + return A3( + _elm_lang$core$Time$spawnHelp, + router, + _p0._1, + A3(_elm_lang$core$Dict$insert, _p1, id, processes)); + }; + var spawnTimer = _elm_lang$core$Native_Scheduler.spawn( + A2( + _elm_lang$core$Time$setInterval, + _p1, + A2(_elm_lang$core$Platform$sendToSelf, router, _p1))); + return A2(_elm_lang$core$Task$andThen, spawnRest, spawnTimer); + } + }); +var _elm_lang$core$Time$addMySub = F2( + function (_p2, state) { + var _p3 = _p2; + var _p6 = _p3._1; + var _p5 = _p3._0; + var _p4 = A2(_elm_lang$core$Dict$get, _p5, state); + if (_p4.ctor === 'Nothing') { + return A3( + _elm_lang$core$Dict$insert, + _p5, + { + ctor: '::', + _0: _p6, + _1: {ctor: '[]'} + }, + state); + } else { + return A3( + _elm_lang$core$Dict$insert, + _p5, + {ctor: '::', _0: _p6, _1: _p4._0}, + state); + } + }); +var _elm_lang$core$Time$inMilliseconds = function (t) { + return t; +}; +var _elm_lang$core$Time$millisecond = 1; +var _elm_lang$core$Time$second = 1000 * _elm_lang$core$Time$millisecond; +var _elm_lang$core$Time$minute = 60 * _elm_lang$core$Time$second; +var _elm_lang$core$Time$hour = 60 * _elm_lang$core$Time$minute; +var _elm_lang$core$Time$inHours = function (t) { + return t / _elm_lang$core$Time$hour; +}; +var _elm_lang$core$Time$inMinutes = function (t) { + return t / _elm_lang$core$Time$minute; +}; +var _elm_lang$core$Time$inSeconds = function (t) { + return t / _elm_lang$core$Time$second; +}; +var _elm_lang$core$Time$now = _elm_lang$core$Native_Time.now; +var _elm_lang$core$Time$onSelfMsg = F3( + function (router, interval, state) { + var _p7 = A2(_elm_lang$core$Dict$get, interval, state.taggers); + if (_p7.ctor === 'Nothing') { + return _elm_lang$core$Task$succeed(state); + } else { + var tellTaggers = function (time) { + return _elm_lang$core$Task$sequence( + A2( + _elm_lang$core$List$map, + function (tagger) { + return A2( + _elm_lang$core$Platform$sendToApp, + router, + tagger(time)); + }, + _p7._0)); + }; + return A2( + _elm_lang$core$Task$andThen, + function (_p8) { + return _elm_lang$core$Task$succeed(state); + }, + A2(_elm_lang$core$Task$andThen, tellTaggers, _elm_lang$core$Time$now)); + } + }); +var _elm_lang$core$Time$subscription = _elm_lang$core$Native_Platform.leaf('Time'); +var _elm_lang$core$Time$State = F2( + function (a, b) { + return {taggers: a, processes: b}; + }); +var _elm_lang$core$Time$init = _elm_lang$core$Task$succeed( + A2(_elm_lang$core$Time$State, _elm_lang$core$Dict$empty, _elm_lang$core$Dict$empty)); +var _elm_lang$core$Time$onEffects = F3( + function (router, subs, _p9) { + var _p10 = _p9; + var rightStep = F3( + function (_p12, id, _p11) { + var _p13 = _p11; + return { + ctor: '_Tuple3', + _0: _p13._0, + _1: _p13._1, + _2: A2( + _elm_lang$core$Task$andThen, + function (_p14) { + return _p13._2; + }, + _elm_lang$core$Native_Scheduler.kill(id)) + }; + }); + var bothStep = F4( + function (interval, taggers, id, _p15) { + var _p16 = _p15; + return { + ctor: '_Tuple3', + _0: _p16._0, + _1: A3(_elm_lang$core$Dict$insert, interval, id, _p16._1), + _2: _p16._2 + }; + }); + var leftStep = F3( + function (interval, taggers, _p17) { + var _p18 = _p17; + return { + ctor: '_Tuple3', + _0: {ctor: '::', _0: interval, _1: _p18._0}, + _1: _p18._1, + _2: _p18._2 + }; + }); + var newTaggers = A3(_elm_lang$core$List$foldl, _elm_lang$core$Time$addMySub, _elm_lang$core$Dict$empty, subs); + var _p19 = A6( + _elm_lang$core$Dict$merge, + leftStep, + bothStep, + rightStep, + newTaggers, + _p10.processes, + { + ctor: '_Tuple3', + _0: {ctor: '[]'}, + _1: _elm_lang$core$Dict$empty, + _2: _elm_lang$core$Task$succeed( + {ctor: '_Tuple0'}) + }); + var spawnList = _p19._0; + var existingDict = _p19._1; + var killTask = _p19._2; + return A2( + _elm_lang$core$Task$andThen, + function (newProcesses) { + return _elm_lang$core$Task$succeed( + A2(_elm_lang$core$Time$State, newTaggers, newProcesses)); + }, + A2( + _elm_lang$core$Task$andThen, + function (_p20) { + return A3(_elm_lang$core$Time$spawnHelp, router, spawnList, existingDict); + }, + killTask)); + }); +var _elm_lang$core$Time$Every = F2( + function (a, b) { + return {ctor: 'Every', _0: a, _1: b}; + }); +var _elm_lang$core$Time$every = F2( + function (interval, tagger) { + return _elm_lang$core$Time$subscription( + A2(_elm_lang$core$Time$Every, interval, tagger)); + }); +var _elm_lang$core$Time$subMap = F2( + function (f, _p21) { + var _p22 = _p21; + return A2( + _elm_lang$core$Time$Every, + _p22._0, + function (_p23) { + return f( + _p22._1(_p23)); + }); + }); +_elm_lang$core$Native_Platform.effectManagers['Time'] = {pkg: 'elm-lang/core', init: _elm_lang$core$Time$init, onEffects: _elm_lang$core$Time$onEffects, onSelfMsg: _elm_lang$core$Time$onSelfMsg, tag: 'sub', subMap: _elm_lang$core$Time$subMap}; + var _elm_lang$core$Date$millisecond = _elm_lang$core$Native_Date.millisecond; var _elm_lang$core$Date$second = _elm_lang$core$Native_Date.second; var _elm_lang$core$Date$minute = _elm_lang$core$Native_Date.minute; @@ -15825,6 +15825,32 @@ var _user$project$Views_EventDetail$eventInstanceItem = F2( }); var _user$project$Views_EventDetail$eventMetaDataSidebar = F3( function (event, eventInstances, model) { + var feedbackUrl = { + ctor: '::', + _0: A2( + _elm_lang$html$Html$a, + { + ctor: '::', + _0: _elm_lang$html$Html_Attributes$href( + A2( + _elm_lang$core$Basics_ops['++'], + 'https://bornhack.dk/', + A2( + _elm_lang$core$Basics_ops['++'], + model.flags.camp_slug, + A2( + _elm_lang$core$Basics_ops['++'], + '/program/', + A2(_elm_lang$core$Basics_ops['++'], event.slug, '/feedback/create/'))))), + _1: {ctor: '[]'} + }, + { + ctor: '::', + _0: _elm_lang$html$Html$text('Give feedback'), + _1: {ctor: '[]'} + }), + _1: {ctor: '[]'} + }; var eventInstanceMetaData = function () { var _p3 = eventInstances; if ((_p3.ctor === '::') && (_p3._1.ctor === '[]')) { @@ -15950,7 +15976,7 @@ var _user$project$Views_EventDetail$eventMetaDataSidebar = F3( _1: {ctor: '[]'} } }, - eventInstanceMetaData)); + A2(_elm_lang$core$Basics_ops['++'], eventInstanceMetaData, feedbackUrl))); }); var _user$project$Views_EventDetail$getSpeakersFromSlugs = F3( function (speakers, slugs, collectedSpeakers) { diff --git a/src/program/templates/event_list.html b/src/program/templates/event_list.html index 3389fe50..71c44cf2 100644 --- a/src/program/templates/event_list.html +++ b/src/program/templates/event_list.html @@ -1,4 +1,5 @@ {% extends 'program_base.html' %} +{% load program %} {% block program_content %} {% if event_list %} @@ -14,6 +15,9 @@This is a list of all the approved feedback for your {{ camp.title }} event {{ event.title }}. We currently have feedback from {{ eventfeedback_list|length }} users.
+ +{% for eventfeedback in eventfeedback_list %} +{% include 'includes/eventfeedback_detail_panel.html' %} +{% endfor %} +{% endblock program_content %} diff --git a/src/program/templates/includes/eventfeedback_buttons.html b/src/program/templates/includes/eventfeedback_buttons.html new file mode 100644 index 00000000..decd66df --- /dev/null +++ b/src/program/templates/includes/eventfeedback_buttons.html @@ -0,0 +1,6 @@ + Back to Event + + Update Feedback + + Delete Feedback + diff --git a/src/program/templates/includes/eventfeedback_detail_panel.html b/src/program/templates/includes/eventfeedback_detail_panel.html new file mode 100644 index 00000000..f9d0ab15 --- /dev/null +++ b/src/program/templates/includes/eventfeedback_detail_panel.html @@ -0,0 +1,57 @@ +{% load bootstrap3 %} +{% load commonmark %} +Username (feedback submitter) | +{{ eventfeedback.user.username }} | +
---|---|
Were Your Expectations Fulfilled? | +{{ eventfeedback.expectations_fulfilled|yesno }} | +
Would You Attend the Same Speaker Again? | +{{ eventfeedback.attend_speaker_again|yesno }} | +
Rating (0-5)? | +{{ eventfeedback.rating }}/5 | +
Created | +{{ eventfeedback.created }} | +
Updated | +{{ eventfeedback.updated|default:"N/A" }} | +
Approved | +{{ eventfeedback.approved|yesno }} | +
Comment | +{{ eventfeedback.comment|default:"N/A"|untrustedcommonmark }} | +
{{ event.abstract|untrustedcommonmark }}
diff --git a/src/program/templates/speaker_detail.html b/src/program/templates/speaker_detail.html
index 387a814a..70ca767e 100644
--- a/src/program/templates/speaker_detail.html
+++ b/src/program/templates/speaker_detail.html
@@ -30,7 +30,7 @@
{{ event.event_type.name }}
- {{ event.title }}
+ {{ event.title }}
{{ event.abstract|untrustedcommonmark }}
diff --git a/src/program/templatetags/__init__.py b/src/program/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/program/templatetags/program.py b/src/program/templatetags/program.py
new file mode 100644
index 00000000..f687c0a2
--- /dev/null
+++ b/src/program/templatetags/program.py
@@ -0,0 +1,50 @@
+from django import template
+from django.urls import reverse
+from django.utils.safestring import mark_safe
+
+register = template.Library()
+
+
+@register.simple_tag(takes_context=True)
+def feedbackbutton(context):
+ """ A templatetag to show a suitable button for EventFeedback """
+
+ if context.request.user.is_anonymous:
+ return None
+
+ event = context["event"]
+ if event.proposal and event.proposal.user == context.request.user:
+ # current user is the event owner, show a link to EventFeedbackList
+ return mark_safe(
+ " Read Feedback (%s)"
+ % (
+ reverse(
+ "program:eventfeedback_list",
+ kwargs={"camp_slug": event.camp.slug, "event_slug": event.slug},
+ ),
+ event.feedbacks.filter(approved=True).count(),
+ )
+ )
+ # FIXME: for some reason this triggers a lookup even though all feedbacks have been prefetched..
+ elif event.feedbacks.filter(user=context.request.user).exists():
+ # this user already submitted feedback for this event, show a link to DetailView
+ return mark_safe(
+ " Change Feedback"
+ % (
+ reverse(
+ "program:eventfeedback_detail",
+ kwargs={"camp_slug": event.camp.slug, "event_slug": event.slug},
+ )
+ )
+ )
+ else:
+ # this user has not submitted feedback yet, show a link to CreateView
+ return mark_safe(
+ " Add Feedback"
+ % (
+ reverse(
+ "program:eventfeedback_create",
+ kwargs={"camp_slug": event.camp.slug, "event_slug": event.slug},
+ )
+ )
+ )
diff --git a/src/program/urls.py b/src/program/urls.py
index fc133e3a..0a0e2311 100644
--- a/src/program/urls.py
+++ b/src/program/urls.py
@@ -15,6 +15,11 @@ from .views import (
EventProposalSelectPersonView,
EventProposalTypeSelectView,
EventProposalUpdateView,
+ FeedbackCreateView,
+ FeedbackDeleteView,
+ FeedbackDetailView,
+ FeedbackListView,
+ FeedbackUpdateView,
ICSView,
NoScriptScheduleView,
ProgramControlCenter,
@@ -151,6 +156,7 @@ urlpatterns = [
EventProposalRemovePersonView.as_view(),
name="eventproposal_removeperson",
),
+ # event url views
path(
"