Event feedback (#451)

* Event feedback functionality and related commits:

* blackness and isort and flake8 - this branch does not have pre-commit so I forgot :/

* finish backoffice management of eventfeedback

* add username to eventfeedback detail panel when viewed in backoffice

* Add feedback url to elm schedule. Fix access when user is anonymous. Remove print statement.

* one prefetch_related call to rule them all

Co-authored-by: Víðir Valberg Guðmundsson <valberg@orn.li>
This commit is contained in:
Thomas Steen Rasmussen 2020-02-22 14:50:09 +01:00 committed by GitHub
parent 9d1fbf5176
commit 33383e6559
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1223 additions and 444 deletions

View file

@ -154,6 +154,9 @@ eventMetaDataSidebar event eventInstances model =
[] []
(List.map (\ei -> li [] <| eventInstanceItem ei model) instances) (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 in
div [] div []
([ h4 [] [ text "Metadata" ] ([ h4 [] [ text "Metadata" ]
@ -170,6 +173,7 @@ eventMetaDataSidebar event eventInstances model =
) )
] ]
++ eventInstanceMetaData ++ eventInstanceMetaData
++ feedbackUrl
) )

View file

@ -0,0 +1,32 @@
{% extends 'base.html' %}
{% load bornhack %}
{% block content %}
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<span class="h2">BackOffice - Approve Event Feedback</span>
</div>
<div class="panel-body">
<div class="lead">
The Content team can approve or reject pending EventFeedback from this page. The feedback will not be visible to the Event owner before it is approved. The Event owner will not be able to see the username of the feedback authors. The feedback author can see when the feedback has been approved or rejected by returning to the feedback page.
</div>
{% if eventfeedback_list %}
<form method="post">
{{ formset.management_form }}
{% csrf_token %}
{% for form, feedback in formset|zip:eventfeedback_list %}
{% include "includes/eventfeedback_detail_panel.html" with eventfeedback=feedback event=feedback.event %}
{% endfor %}
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Submit</button>
<a href="{% url 'backoffice:index' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
</form>
{% else %}
<div class="lead">There is no feedback awaiting approval.</div>
<a href="{% url 'backoffice:index' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
{% endif %}
</div>
</div>
</div>
{% endblock content %}

View file

@ -33,6 +33,10 @@
<h4 class="list-group-item-heading">Manage Proposals</h4> <h4 class="list-group-item-heading">Manage Proposals</h4>
<p class="list-group-item-text">Use this view to manage SpeakerProposals and EventProposals</p> <p class="list-group-item-text">Use this view to manage SpeakerProposals and EventProposals</p>
</a> </a>
<a href="{% url 'backoffice:approve_eventfeedback' camp_slug=camp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Approve Feedback</h4>
<p class="list-group-item-text">Use this view to approve or reject EventFeedback</p>
</a>
{% endif %} {% endif %}
{% if perms.camps.orgateam_permission %} {% if perms.camps.orgateam_permission %}

View file

@ -1,6 +1,7 @@
from django.urls import include, path from django.urls import include, path
from .views import ( from .views import (
ApproveFeedbackView,
ApproveNamesView, ApproveNamesView,
BackofficeIndexView, BackofficeIndexView,
BadgeHandoutView, BadgeHandoutView,
@ -80,6 +81,10 @@ urlpatterns = [
] ]
), ),
), ),
# approve eventfeedback objects
path(
"approve_feedback", ApproveFeedbackView.as_view(), name="approve_eventfeedback",
),
# economy # economy
path( path(
"economy/", "economy/",

View file

@ -2,22 +2,22 @@ import logging
import os import os
from itertools import chain from itertools import chain
from camps.mixins import CampViewMixin
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.files import File from django.core.files import File
from django.db.models import Sum from django.db.models import Sum
from django.forms import modelformset_factory
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.views.generic import DetailView, ListView, TemplateView from django.views.generic import DetailView, ListView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from camps.mixins import CampViewMixin
from economy.models import Chain, Credebtor, Expense, Reimbursement, Revenue from economy.models import Chain, Credebtor, Expense, Reimbursement, Revenue
from profiles.models import Profile 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 shop.models import Order, OrderProductRelation
from teams.models import Team from teams.models import Team
from tickets.models import DiscountTicket, ShopTicket, SponsorTicket, TicketType 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): class ManageProposalsView(CampViewMixin, ContentTeamPermissionMixin, ListView):
""" """
This view shows a list of pending SpeakerProposal and EventProposals. 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 We have two submit buttons in this form, Approve and Reject
""" """
logger.debug(form.data)
if "approve" in form.data: if "approve" in form.data:
# approve button was pressed # approve button was pressed
form.instance.mark_as_approved(self.request) form.instance.mark_as_approved(self.request)

View file

@ -123,6 +123,7 @@ MIDDLEWARE = [
"django_otp.middleware.OTPMiddleware", "django_otp.middleware.OTPMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"utils.middleware.RedirectExceptionMiddleware",
] ]
CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = True

View file

@ -3,15 +3,15 @@ from django.shortcuts import get_object_or_404
from camps.models import Camp from camps.models import Camp
class CampViewMixin(object): class CampViewMixin:
""" """
This mixin makes sure self.camp is available (taken from url kwarg camp_slug) 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 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"]) self.camp = get_object_or_404(Camp, slug=self.kwargs["camp_slug"])
return super().dispatch(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()

View file

@ -6,13 +6,12 @@ from django.utils import timezone
from django.views import View from django.views import View
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from .mixins import CampViewMixin
from .models import Camp from .models import Camp
logger = logging.getLogger("bornhack.%s" % __name__) logger = logging.getLogger("bornhack.%s" % __name__)
class CampRedirectView(CampViewMixin, View): class CampRedirectView(View):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
now = timezone.now() now = timezone.now()

View file

@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError
from .models import ( from .models import (
Event, Event,
EventFeedback,
EventInstance, EventInstance,
EventLocation, EventLocation,
EventProposal, EventProposal,
@ -120,3 +121,27 @@ class UrlTypeAdmin(admin.ModelAdmin):
@admin.register(Url) @admin.register(Url)
class UrlAdmin(admin.ModelAdmin): class UrlAdmin(admin.ModelAdmin):
pass 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"]

View file

@ -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},
),
]

View file

@ -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")},
),
]

View file

@ -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",
),
]

View file

@ -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?"
),
),
]

View file

@ -1,3 +1,4 @@
from camps.mixins import CampViewMixin
from django.contrib import messages from django.contrib import messages
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
@ -109,3 +110,35 @@ class UrlViewMixin(object):
return self.eventproposal.get_absolute_url() return self.eventproposal.get_absolute_url()
else: else:
return self.speakerproposal.get_absolute_url() 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

View file

@ -10,10 +10,9 @@ from django.contrib.postgres.fields import DateTimeRangeField
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.db import models 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 django.utils.text import slugify
from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel
from utils.models import CampRelatedModel, CreatedUpdatedModel
logger = logging.getLogger("bornhack.%s" % __name__) logger = logging.getLogger("bornhack.%s" % __name__)
@ -594,7 +593,7 @@ class Event(CampRelatedModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy( return reverse_lazy(
"program:event_detail", "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): def serialize(self):
@ -795,6 +794,67 @@ class Favorite(models.Model):
unique_together = ["user", "event_instance"] 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 # classes and functions below here was used by picture handling for speakers before it was removed in May 2018 by tyk

View file

@ -5851,418 +5851,6 @@ var _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required = F3(
decoder); 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( var _elm_lang$core$Set$foldr = F3(
function (f, b, _p0) { function (f, b, _p0) {
var _p1 = _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$millisecond = _elm_lang$core$Native_Date.millisecond;
var _elm_lang$core$Date$second = _elm_lang$core$Native_Date.second; var _elm_lang$core$Date$second = _elm_lang$core$Native_Date.second;
var _elm_lang$core$Date$minute = _elm_lang$core$Native_Date.minute; 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( var _user$project$Views_EventDetail$eventMetaDataSidebar = F3(
function (event, eventInstances, model) { 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 eventInstanceMetaData = function () {
var _p3 = eventInstances; var _p3 = eventInstances;
if ((_p3.ctor === '::') && (_p3._1.ctor === '[]')) { if ((_p3.ctor === '::') && (_p3._1.ctor === '[]')) {
@ -15950,7 +15976,7 @@ var _user$project$Views_EventDetail$eventMetaDataSidebar = F3(
_1: {ctor: '[]'} _1: {ctor: '[]'}
} }
}, },
eventInstanceMetaData)); A2(_elm_lang$core$Basics_ops['++'], eventInstanceMetaData, feedbackUrl)));
}); });
var _user$project$Views_EventDetail$getSpeakersFromSlugs = F3( var _user$project$Views_EventDetail$getSpeakersFromSlugs = F3(
function (speakers, slugs, collectedSpeakers) { function (speakers, slugs, collectedSpeakers) {

View file

@ -1,4 +1,5 @@
{% extends 'program_base.html' %} {% extends 'program_base.html' %}
{% load program %}
{% block program_content %} {% block program_content %}
{% if event_list %} {% if event_list %}
@ -14,6 +15,9 @@
<th>Title</th> <th>Title</th>
<th>Speakers</th> <th>Speakers</th>
<th>Scheduled</th> <th>Scheduled</th>
{% if not user.is_anonymous %}
<th>Feedback</th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -26,7 +30,7 @@
</a> </a>
</td> </td>
<td> <td>
<a href="{% url 'program:event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a> <a href="{% url 'program:event_detail' camp_slug=camp.slug event_slug=event.slug %}">{{ event.title }}</a>
</td> </td>
<td> <td>
{% for speaker in event.speakers.all %} {% for speaker in event.speakers.all %}
@ -42,6 +46,9 @@
<i>No instances scheduled yet</i> <i>No instances scheduled yet</i>
{% endfor %} {% endfor %}
</td> </td>
{% if not user.is_anonymous %}
<td>{% feedbackbutton %}</td>
{% endif %}
</tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View file

@ -0,0 +1,18 @@
{% extends 'program_base.html' %}
{% load commonmark %}
{% load bootstrap3 %}
{% block program_content %}
<h2>Delete Your Feedback?</h2>
{% include 'includes/eventfeedback_detail_panel.html' %}
<form method="POST">
{% csrf_token %}
<button type="submit" class="btn btn-danger">
<i class="fas fa-times"></i>
Delete Feedback
</button>
<a href="{% url 'program:eventfeedback_detail' camp_slug=camp.slug event_slug=event.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
</form>
{% endblock program_content %}

View file

@ -0,0 +1,7 @@
{% extends 'program_base.html' %}
{% load commonmark %}
{% block program_content %}
{% include 'includes/eventfeedback_detail_panel.html' with buttoninclude="includes/eventfeedback_buttons.html"%}
{% endblock program_content %}

View file

@ -0,0 +1,18 @@
{% extends 'program_base.html' %}
{% load commonmark %}
{% load bootstrap3 %}
{% block program_content %}
<h2>{% if request.resolver_match.url_name == "eventfeedback_update" %}Update{% else %}Submit{% endif %} Feedback for {{ camp.title }} event: {{ event.title }}</h2>
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-success">
<i class="fas fa-check"></i>
{% if request.resolver_match.url_name == "eventfeedback_update" %}Update{% else %}Submit{% endif %} Feedback
</button>
<a href="{% url 'program:event_detail' camp_slug=camp.slug event_slug=event.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
</form>
{% endblock program_content %}

View file

@ -0,0 +1,11 @@
{% extends 'program_base.html' %}
{% load program %}
{% block program_content %}
<h2>Feedback List</h2>
<p class="lead">This is a list of all the approved feedback for your {{ camp.title }} event <b>{{ event.title }}</b>. We currently have feedback from {{ eventfeedback_list|length }} users.</p>
{% for eventfeedback in eventfeedback_list %}
{% include 'includes/eventfeedback_detail_panel.html' %}
{% endfor %}
{% endblock program_content %}

View file

@ -0,0 +1,6 @@
<a href="{% url 'program:event_detail' camp_slug=camp.slug event_slug=event.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Back to Event</a>
<a href="{% url 'program:eventfeedback_update' camp_slug=camp.slug event_slug=event.slug %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update Feedback</a>
<a href="{% url 'program:eventfeedback_delete' camp_slug=camp.slug event_slug=event.slug %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete Feedback</a>

View file

@ -0,0 +1,57 @@
{% load bootstrap3 %}
{% load commonmark %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="h3">Feedback for Event: {{ event.title }}</span>
</div>
<div class="panel-body">
<table class="table table-bordered">
{% if request.resolver_match.url_name == "approve_eventfeedback" %}
<tr>
<th>Username (feedback submitter)</th>
<td>{{ eventfeedback.user.username }}</td>
</tr>
{% endif %}
<tr>
<th>Were Your Expectations Fulfilled?</th>
<td>{{ eventfeedback.expectations_fulfilled|yesno }}</td>
</tr>
<tr>
<th>Would You Attend the Same Speaker Again?</th>
<td>{{ eventfeedback.attend_speaker_again|yesno }}</td>
</tr>
<tr>
<th>Rating (0-5)?</th>
<td>{{ eventfeedback.rating }}/5</td>
</tr>
<tr>
<th>Created</th>
<td>{{ eventfeedback.created }}</td>
</tr>
<tr>
<th>Updated</th>
<td>{{ eventfeedback.updated|default:"N/A" }}</td>
</tr>
<tr>
<th>Approved</th>
<td>{{ eventfeedback.approved|yesno }}</td>
</tr>
<tr>
<th>Comment</th>
<td>{{ eventfeedback.comment|default:"N/A"|untrustedcommonmark }}</td>
</tr>
</table>
{% if form %}
{% bootstrap_form form %}
{% elif buttoninclude %}
{% include buttoninclude %}
{% endif %}
</div>
</div>

View file

@ -1,6 +1,6 @@
{% extends 'program_base.html' %} {% extends 'program_base.html' %}
{% load commonmark %} {% load commonmark %}
{% load program %}
{% block program_content %} {% block program_content %}
<div class="row"> <div class="row">
@ -14,7 +14,7 @@
<div class="row"> <div class="row">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" ><span style="font-size: x-large"><i class="fas fa-{{ event.event_type.icon }} fa-lg" style="color: {{ event.event_type.color }};"></i> {{ event.title }}</span></div> <div class="panel-heading" ><span style="font-size: x-large"><i class="fas fa-{{ event.event_type.icon }} fa-lg" style="color: {{ event.event_type.color }};"></i> {{ event.title }}</span><span class="pull-right">{% feedbackbutton %}</span></div>
<div class="panel-body"> <div class="panel-body">
<p> <p>
{{ event.abstract|untrustedcommonmark }} {{ event.abstract|untrustedcommonmark }}

View file

@ -30,7 +30,7 @@
<small style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;"> <small style="background-color: {{ event.event_type.color }}; border: 0; color: {% if event.event_type.light_text %}white{% else %}black{% endif %}; display: inline-block; padding: 5px;">
{{ event.event_type.name }} {{ event.event_type.name }}
</small> </small>
<a href="{% url 'program:event_detail' camp_slug=camp.slug slug=event.slug %}">{{ event.title }}</a> <a href="{% url 'program:event_detail' camp_slug=camp.slug event_slug=event.slug %}">{{ event.title }}</a>
</h3> </h3>
{{ event.abstract|untrustedcommonmark }} {{ event.abstract|untrustedcommonmark }}

View file

View file

@ -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(
"<a class='btn btn-primary' href='%s'><i class='fas fa-comments'></i> Read Feedback (%s)</a>"
% (
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(
"<a class='btn btn-default' href='%s'><i class='fas fa-comment-dots'></i> Change Feedback</a>"
% (
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(
"<a class='btn btn-success' href='%s'><i class='fas fa-comment'></i> Add Feedback</a>"
% (
reverse(
"program:eventfeedback_create",
kwargs={"camp_slug": event.camp.slug, "event_slug": event.slug},
)
)
)

View file

@ -15,6 +15,11 @@ from .views import (
EventProposalSelectPersonView, EventProposalSelectPersonView,
EventProposalTypeSelectView, EventProposalTypeSelectView,
EventProposalUpdateView, EventProposalUpdateView,
FeedbackCreateView,
FeedbackDeleteView,
FeedbackDetailView,
FeedbackListView,
FeedbackUpdateView,
ICSView, ICSView,
NoScriptScheduleView, NoScriptScheduleView,
ProgramControlCenter, ProgramControlCenter,
@ -151,6 +156,7 @@ urlpatterns = [
EventProposalRemovePersonView.as_view(), EventProposalRemovePersonView.as_view(),
name="eventproposal_removeperson", name="eventproposal_removeperson",
), ),
# event url views
path( path(
"<uuid:event_uuid>/add_url/", "<uuid:event_uuid>/add_url/",
UrlCreateView.as_view(), UrlCreateView.as_view(),
@ -196,6 +202,45 @@ urlpatterns = [
name="call_for_participation", name="call_for_participation",
), ),
path("calendar", ICSView.as_view(), name="ics_calendar"), path("calendar", ICSView.as_view(), name="ics_calendar"),
# this must be the last URL here or the regex will overrule the others # this must be the last URL here or the slug will overrule the others
path("<slug:slug>/", EventDetailView.as_view(), name="event_detail"), path(
"<slug:event_slug>/",
include(
[
path("", EventDetailView.as_view(), name="event_detail"),
path(
"feedback/",
include(
[
path(
"",
FeedbackListView.as_view(),
name="eventfeedback_list",
),
path(
"show/",
FeedbackDetailView.as_view(),
name="eventfeedback_detail",
),
path(
"create/",
FeedbackCreateView.as_view(),
name="eventfeedback_create",
),
path(
"update/",
FeedbackUpdateView.as_view(),
name="eventfeedback_update",
),
path(
"delete/",
FeedbackDeleteView.as_view(),
name="eventfeedback_delete",
),
]
),
),
]
),
),
] ]

View file

@ -2,6 +2,8 @@ import logging
from collections import OrderedDict from collections import OrderedDict
import icalendar import icalendar
from camps.mixins import CampViewMixin
from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
@ -13,8 +15,8 @@ from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import DetailView, ListView, TemplateView, View from django.views.generic import DetailView, ListView, TemplateView, View
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
from utils.middleware import RedirectException
from camps.mixins import CampViewMixin from utils.mixins import UserIsObjectOwnerMixin
from . import models from . import models
from .email import ( from .email import (
@ -28,6 +30,8 @@ from .mixins import (
EnsureCFPOpenMixin, EnsureCFPOpenMixin,
EnsureUserOwnsProposalMixin, EnsureUserOwnsProposalMixin,
EnsureWritableCampMixin, EnsureWritableCampMixin,
EventFeedbackViewMixin,
EventViewMixin,
UrlViewMixin, UrlViewMixin,
) )
from .multiform import MultiModelForm from .multiform import MultiModelForm
@ -762,6 +766,12 @@ class SpeakerListView(CampViewMixin, ListView):
model = models.Speaker model = models.Speaker
template_name = "speaker_list.html" template_name = "speaker_list.html"
def get_queryset(self, *args, **kwargs):
qs = super().get_queryset(*args, **kwargs)
qs = qs.prefetch_related("events")
qs = qs.prefetch_related("events__event_type")
return qs
################################################################################################### ###################################################################################################
# event views # event views
@ -771,10 +781,16 @@ class EventListView(CampViewMixin, ListView):
model = models.Event model = models.Event
template_name = "event_list.html" template_name = "event_list.html"
def get_queryset(self, *args, **kwargs):
qs = super().get_queryset(*args, **kwargs)
qs = qs.prefetch_related("event_type", "track", "instances", "speakers")
return qs
class EventDetailView(CampViewMixin, DetailView): class EventDetailView(CampViewMixin, DetailView):
model = models.Event model = models.Event
template_name = "schedule_event_detail.html" template_name = "schedule_event_detail.html"
slug_url_kwarg = "event_slug"
################################################################################################### ###################################################################################################
@ -1035,3 +1051,120 @@ class UrlDeleteView(
return redirect( return redirect(
reverse_lazy("program:proposal_list", kwargs={"camp_slug": self.camp.slug}) reverse_lazy("program:proposal_list", kwargs={"camp_slug": self.camp.slug})
) )
###################################################################################################
# Feedback views
class FeedbackListView(LoginRequiredMixin, EventViewMixin, ListView):
"""
The FeedbackListView is used by the event owner to see approved Feedback for the Event.
"""
model = models.EventFeedback
template_name = "eventfeedback_list.html"
def setup(self, *args, **kwargs):
super().setup(*args, **kwargs)
if not self.event.proposal or not self.event.proposal.user == self.request.user:
messages.error(self.request, "Only the event owner can read feedback!")
raise RedirectException(
reverse(
"program:event_detail",
kwargs={"camp_slug": self.camp.slug, "event_slug": self.event.slug},
)
)
def get_queryset(self, *args, **kwargs):
return models.EventFeedback.objects.filter(event=self.event, approved=True)
class FeedbackCreateView(LoginRequiredMixin, EventViewMixin, CreateView):
"""
Used by users to create Feedback for an Event. Available to all logged in users.
"""
model = models.EventFeedback
fields = ["expectations_fulfilled", "attend_speaker_again", "rating", "comment"]
template_name = "eventfeedback_form.html"
def setup(self, *args, **kwargs):
super().setup(*args, **kwargs)
if models.EventFeedback.objects.filter(
event=self.event, user=self.request.user
).exists():
raise RedirectException(
reverse(
"program:eventfeedback_detail",
kwargs={"camp_slug": self.camp.slug, "event_slug": self.event.slug},
)
)
def get_form(self, *args, **kwargs):
form = super().get_form(*args, **kwargs)
form.fields["expectations_fulfilled"].widget = forms.RadioSelect(
choices=models.EventFeedback.YESNO_CHOICES,
)
form.fields["attend_speaker_again"].widget = forms.RadioSelect(
choices=models.EventFeedback.YESNO_CHOICES,
)
form.fields["rating"].widget = forms.RadioSelect(
choices=models.EventFeedback.RATING_CHOICES,
)
return form
def form_valid(self, form):
feedback = form.save(commit=False)
feedback.user = self.request.user
feedback.event = self.event
feedback.save()
messages.success(
self.request, "Your feedback was submitted, it is now pending approval."
)
return redirect(feedback.get_absolute_url())
class FeedbackDetailView(
LoginRequiredMixin, EventFeedbackViewMixin, UserIsObjectOwnerMixin, DetailView
):
"""
Used by the EventFeedback owner to see their own feedback.
"""
model = models.EventFeedback
template_name = "eventfeedback_detail.html"
class FeedbackUpdateView(
LoginRequiredMixin, EventFeedbackViewMixin, UserIsObjectOwnerMixin, UpdateView
):
"""
Used by the EventFeedback owner to update their feedback.
"""
model = models.EventFeedback
fields = ["expectations_fulfilled", "attend_speaker_again", "rating", "comment"]
template_name = "eventfeedback_form.html"
def form_valid(self, form):
feedback = form.save(commit=False)
feedback.approved = False
feedback.save()
messages.success(self.request, "Your feedback was updated")
return redirect(feedback.get_absolute_url())
class FeedbackDeleteView(
LoginRequiredMixin, EventFeedbackViewMixin, UserIsObjectOwnerMixin, DeleteView
):
"""
Used by the EventFeedback owner to delete their own feedback.
"""
model = models.EventFeedback
template_name = "eventfeedback_delete.html"
def get_success_url(self):
messages.success(self.request, "Your feedback was deleted")
return self.event.get_absolute_url()

View file

@ -1,6 +1,6 @@
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.views.generic import ( from django.views.generic import (
@ -13,6 +13,7 @@ from django.views.generic import (
from camps.mixins import CampViewMixin from camps.mixins import CampViewMixin
from utils.email import add_outgoing_email from utils.email import add_outgoing_email
from utils.mixins import UserIsObjectOwnerMixin
from .models import Ride from .models import Ride
@ -91,12 +92,7 @@ class RideCreate(LoginRequiredMixin, CampViewMixin, CreateView):
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
class IsRideOwnerMixin(UserPassesTestMixin): class RideUpdate(LoginRequiredMixin, CampViewMixin, UserIsObjectOwnerMixin, UpdateView):
def test_func(self):
return self.get_object().user == self.request.user
class RideUpdate(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, UpdateView):
model = Ride model = Ride
fields = [ fields = [
"author", "author",
@ -109,7 +105,7 @@ class RideUpdate(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, UpdateView
] ]
class RideDelete(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, DeleteView): class RideDelete(LoginRequiredMixin, CampViewMixin, UserIsObjectOwnerMixin, DeleteView):
model = Ride model = Ride
def get_success_url(self): def get_success_url(self):

30
src/utils/middleware.py Normal file
View file

@ -0,0 +1,30 @@
from django.shortcuts import redirect
class RedirectException(Exception):
"""
An exception class meant to be used to redirect from places where
we cannot just return a HTTPResponse directly (like view setup() methods)
"""
def __init__(self, url):
self.url = url
class RedirectExceptionMiddleware:
"""
A simple middleware to catch exceptions of type RedirectException
and redirect to the url
"""
def __init__(self, get_response):
self.get_response = get_response
def process_exception(self, request, exception):
if isinstance(exception, RedirectException):
if hasattr(exception, "url"):
return redirect(exception.url)
def __call__(self, request):
response = self.get_response(request)
return response

View file

@ -1,5 +1,5 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
@ -25,3 +25,8 @@ class RaisePermissionRequiredMixin(PermissionRequiredMixin):
""" """
raise_exception = True raise_exception = True
class UserIsObjectOwnerMixin(UserPassesTestMixin):
def test_func(self):
return self.get_object().user == self.request.user

View file

@ -4,6 +4,11 @@ from django.utils.safestring import mark_safe
register = template.Library() register = template.Library()
@register.filter(name="zip")
def zip_lists(a, b):
return zip(a, b)
@register.filter() @register.filter()
def truefalseicon(value): def truefalseicon(value):
""" A templatetag to show a green checkbox or red x depending on True/False value """ """ A templatetag to show a green checkbox or red x depending on True/False value """