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:
parent
9d1fbf5176
commit
33383e6559
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
32
src/backoffice/templates/approve_feedback.html
Normal file
32
src/backoffice/templates/approve_feedback.html
Normal 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 %}
|
||||
|
|
@ -33,6 +33,10 @@
|
|||
<h4 class="list-group-item-heading">Manage Proposals</h4>
|
||||
<p class="list-group-item-text">Use this view to manage SpeakerProposals and EventProposals</p>
|
||||
</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 %}
|
||||
|
||||
{% if perms.camps.orgateam_permission %}
|
||||
|
|
|
@ -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/",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
77
src/program/migrations/0075_eventfeedback.py
Normal file
77
src/program/migrations/0075_eventfeedback.py
Normal 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},
|
||||
),
|
||||
]
|
34
src/program/migrations/0076_auto_20200214_0143.py
Normal file
34
src/program/migrations/0076_auto_20200214_0143.py
Normal 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")},
|
||||
),
|
||||
]
|
16
src/program/migrations/0077_auto_20200214_0246.py
Normal file
16
src/program/migrations/0077_auto_20200214_0246.py
Normal 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",
|
||||
),
|
||||
]
|
27
src/program/migrations/0078_auto_20200214_2100.py
Normal file
27
src/program/migrations/0078_auto_20200214_2100.py
Normal 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?"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends 'program_base.html' %}
|
||||
{% load program %}
|
||||
|
||||
{% block program_content %}
|
||||
{% if event_list %}
|
||||
|
@ -14,6 +15,9 @@
|
|||
<th>Title</th>
|
||||
<th>Speakers</th>
|
||||
<th>Scheduled</th>
|
||||
{% if not user.is_anonymous %}
|
||||
<th>Feedback</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -26,7 +30,7 @@
|
|||
</a>
|
||||
</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>
|
||||
{% for speaker in event.speakers.all %}
|
||||
|
@ -42,6 +46,9 @@
|
|||
<i>No instances scheduled yet</i>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% if not user.is_anonymous %}
|
||||
<td>{% feedbackbutton %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
|
18
src/program/templates/eventfeedback_delete.html
Normal file
18
src/program/templates/eventfeedback_delete.html
Normal 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 %}
|
7
src/program/templates/eventfeedback_detail.html
Normal file
7
src/program/templates/eventfeedback_detail.html
Normal 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 %}
|
||||
|
18
src/program/templates/eventfeedback_form.html
Normal file
18
src/program/templates/eventfeedback_form.html
Normal 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 %}
|
11
src/program/templates/eventfeedback_list.html
Normal file
11
src/program/templates/eventfeedback_list.html
Normal 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 %}
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'program_base.html' %}
|
||||
{% load commonmark %}
|
||||
|
||||
{% load program %}
|
||||
{% block program_content %}
|
||||
|
||||
<div class="row">
|
||||
|
@ -14,7 +14,7 @@
|
|||
|
||||
<div class="row">
|
||||
<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">
|
||||
<p>
|
||||
{{ event.abstract|untrustedcommonmark }}
|
||||
|
|
|
@ -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;">
|
||||
{{ event.event_type.name }}
|
||||
</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>
|
||||
{{ event.abstract|untrustedcommonmark }}
|
||||
|
||||
|
|
0
src/program/templatetags/__init__.py
Normal file
0
src/program/templatetags/__init__.py
Normal file
50
src/program/templatetags/program.py
Normal file
50
src/program/templatetags/program.py
Normal 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},
|
||||
)
|
||||
)
|
||||
)
|
|
@ -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(
|
||||
"<uuid:event_uuid>/add_url/",
|
||||
UrlCreateView.as_view(),
|
||||
|
@ -196,6 +202,45 @@ urlpatterns = [
|
|||
name="call_for_participation",
|
||||
),
|
||||
path("calendar", ICSView.as_view(), name="ics_calendar"),
|
||||
# this must be the last URL here or the regex will overrule the others
|
||||
path("<slug:slug>/", EventDetailView.as_view(), name="event_detail"),
|
||||
# this must be the last URL here or the slug will overrule the others
|
||||
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",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -2,6 +2,8 @@ import logging
|
|||
from collections import OrderedDict
|
||||
|
||||
import icalendar
|
||||
from camps.mixins import CampViewMixin
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
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.views.generic import DetailView, ListView, TemplateView, View
|
||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||
|
||||
from camps.mixins import CampViewMixin
|
||||
from utils.middleware import RedirectException
|
||||
from utils.mixins import UserIsObjectOwnerMixin
|
||||
|
||||
from . import models
|
||||
from .email import (
|
||||
|
@ -28,6 +30,8 @@ from .mixins import (
|
|||
EnsureCFPOpenMixin,
|
||||
EnsureUserOwnsProposalMixin,
|
||||
EnsureWritableCampMixin,
|
||||
EventFeedbackViewMixin,
|
||||
EventViewMixin,
|
||||
UrlViewMixin,
|
||||
)
|
||||
from .multiform import MultiModelForm
|
||||
|
@ -762,6 +766,12 @@ class SpeakerListView(CampViewMixin, ListView):
|
|||
model = models.Speaker
|
||||
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
|
||||
|
@ -771,10 +781,16 @@ class EventListView(CampViewMixin, ListView):
|
|||
model = models.Event
|
||||
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):
|
||||
model = models.Event
|
||||
template_name = "schedule_event_detail.html"
|
||||
slug_url_kwarg = "event_slug"
|
||||
|
||||
|
||||
###################################################################################################
|
||||
|
@ -1035,3 +1051,120 @@ class UrlDeleteView(
|
|||
return redirect(
|
||||
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()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django import forms
|
||||
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.urls import reverse
|
||||
from django.views.generic import (
|
||||
|
@ -13,6 +13,7 @@ from django.views.generic import (
|
|||
|
||||
from camps.mixins import CampViewMixin
|
||||
from utils.email import add_outgoing_email
|
||||
from utils.mixins import UserIsObjectOwnerMixin
|
||||
|
||||
from .models import Ride
|
||||
|
||||
|
@ -91,12 +92,7 @@ class RideCreate(LoginRequiredMixin, CampViewMixin, CreateView):
|
|||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
class IsRideOwnerMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
return self.get_object().user == self.request.user
|
||||
|
||||
|
||||
class RideUpdate(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, UpdateView):
|
||||
class RideUpdate(LoginRequiredMixin, CampViewMixin, UserIsObjectOwnerMixin, UpdateView):
|
||||
model = Ride
|
||||
fields = [
|
||||
"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
|
||||
|
||||
def get_success_url(self):
|
||||
|
|
30
src/utils/middleware.py
Normal file
30
src/utils/middleware.py
Normal 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
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
||||
|
||||
|
@ -25,3 +25,8 @@ class RaisePermissionRequiredMixin(PermissionRequiredMixin):
|
|||
"""
|
||||
|
||||
raise_exception = True
|
||||
|
||||
|
||||
class UserIsObjectOwnerMixin(UserPassesTestMixin):
|
||||
def test_func(self):
|
||||
return self.get_object().user == self.request.user
|
||||
|
|
|
@ -4,6 +4,11 @@ from django.utils.safestring import mark_safe
|
|||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name="zip")
|
||||
def zip_lists(a, b):
|
||||
return zip(a, b)
|
||||
|
||||
|
||||
@register.filter()
|
||||
def truefalseicon(value):
|
||||
""" A templatetag to show a green checkbox or red x depending on True/False value """
|
||||
|
|
Loading…
Reference in a new issue