diff --git a/src/backoffice/templates/facility_detail_backoffice.html b/src/backoffice/templates/facility_detail_backoffice.html
new file mode 100644
index 00000000..24ee8cd0
--- /dev/null
+++ b/src/backoffice/templates/facility_detail_backoffice.html
@@ -0,0 +1,137 @@
+{% extends 'base.html' %}
+{% load leaflet_tags %}
+{% load static %}
+{% load commonmark %}
+
+{% block extra_head %}
+ {% leaflet_css %}
+
+
+
+
+{% endblock extra_head %}
+
+{% block title %}
+{{ facility.name }} | Facilities | BackOffice | {{ block.super }}
+{% endblock %}
+
+{% block content %}
+
No unhandled feedback found for any Facilities managed by {{ team.name }} Team. Good job!
-
Welcome to the promised land! Please select your desired action below:
+
Facilities
+ {% if perms.camps.orgateam_permission %}
+
+
+ Facility Types
+
+
+ See and manage facility types
+
+
+
+
+ Facilities
+
+
+ See and manage facilites
+
+
+ {% endif %}
{% for team in facilityfeedback_teams %}
{% if "camps."|add:team.permission_set in perms %}
- {% if forloop.first %}
-
Facility Feedback
- {% endif %}
- {{ team.name }} Team
+ Feedback for {{ team.name }} Team
See unhandled feedback for facilities managed by {{ team.name }} Team
{% endif %}
- {% empty %}
-
-
N/A
-
- No unhandled Facility Feedback found!
-
-
{% endfor %}
{% if perms.camps.infoteam_permission %}
diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py
index 04c6b8e0..b8a3d688 100644
--- a/src/backoffice/urls.py
+++ b/src/backoffice/urls.py
@@ -43,7 +43,19 @@ from .views import (
EventUpdateView,
ExpenseDetailView,
ExpenseListView,
+ FacilityCreateView,
+ FacilityDeleteView,
+ FacilityDetailView,
FacilityFeedbackView,
+ FacilityListView,
+ FacilityOpeningHoursCreateView,
+ FacilityOpeningHoursDeleteView,
+ FacilityOpeningHoursUpdateView,
+ FacilityTypeCreateView,
+ FacilityTypeDeleteView,
+ FacilityTypeListView,
+ FacilityTypeUpdateView,
+ FacilityUpdateView,
MerchandiseOrdersView,
MerchandiseToOrderView,
PendingProposalsView,
@@ -77,11 +89,99 @@ urlpatterns = [
# proxy view
path("proxy/", BackofficeProxyView.as_view(), name="proxy"),
path("proxy/
/", BackofficeProxyView.as_view(), name="proxy"),
- # facility feedback
+ # facilities
path(
"feedback/facilities//",
include([path("", FacilityFeedbackView.as_view(), name="facilityfeedback")]),
),
+ path(
+ "facility_types/",
+ include(
+ [
+ path("", FacilityTypeListView.as_view(), name="facility_type_list"),
+ path(
+ "create/",
+ FacilityTypeCreateView.as_view(),
+ name="facility_type_create",
+ ),
+ path(
+ "/",
+ include(
+ [
+ path(
+ "update/",
+ FacilityTypeUpdateView.as_view(),
+ name="facility_type_update",
+ ),
+ path(
+ "delete/",
+ FacilityTypeDeleteView.as_view(),
+ name="facility_type_delete",
+ ),
+ ]
+ ),
+ ),
+ ]
+ ),
+ ),
+ path(
+ "facilities/",
+ include(
+ [
+ path("", FacilityListView.as_view(), name="facility_list"),
+ path("create/", FacilityCreateView.as_view(), name="facility_create"),
+ path(
+ "/",
+ include(
+ [
+ path(
+ "", FacilityDetailView.as_view(), name="facility_detail"
+ ),
+ path(
+ "update/",
+ FacilityUpdateView.as_view(),
+ name="facility_update",
+ ),
+ path(
+ "delete/",
+ FacilityDeleteView.as_view(),
+ name="facility_delete",
+ ),
+ path(
+ "opening_hours/",
+ include(
+ [
+ path(
+ "create/",
+ FacilityOpeningHoursCreateView.as_view(),
+ name="facility_opening_hours_create",
+ ),
+ path(
+ "/",
+ include(
+ [
+ path(
+ "update/",
+ FacilityOpeningHoursUpdateView.as_view(),
+ name="facility_opening_hours_update",
+ ),
+ path(
+ "delete/",
+ FacilityOpeningHoursDeleteView.as_view(),
+ name="facility_opening_hours_delete",
+ ),
+ ]
+ ),
+ ),
+ ]
+ ),
+ ),
+ ]
+ ),
+ ),
+ ]
+ ),
+ ),
# infodesk
path(
"infodesk/",
diff --git a/src/backoffice/views.py b/src/backoffice/views.py
index 08e3f235..ffea3203 100644
--- a/src/backoffice/views.py
+++ b/src/backoffice/views.py
@@ -21,7 +21,13 @@ from django.utils.safestring import mark_safe
from django.views.generic import DetailView, ListView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from economy.models import Chain, Credebtor, Expense, Reimbursement, Revenue
-from facilities.models import FacilityFeedback
+from facilities.models import (
+ Facility,
+ FacilityFeedback,
+ FacilityOpeningHours,
+ FacilityType,
+)
+from leaflet.forms.widgets import LeafletWidget
from profiles.models import Profile
from program.autoscheduler import AutoScheduler
from program.mixins import AvailabilityMatrixViewMixin
@@ -81,59 +87,6 @@ class BackofficeIndexView(CampViewMixin, RaisePermissionRequiredMixin, TemplateV
return context
-class FacilityFeedbackView(CampViewMixin, RaisePermissionRequiredMixin, FormView):
- template_name = "facilityfeedback_backoffice.html"
-
- def get_permission_required(self):
- """
- This view requires two permissions, camps.backoffice_permission and
- the permission_set for the team in question.
- """
- if not self.team.permission_set:
- raise PermissionDenied("No permissions set defined for this team")
- return ["camps.backoffice_permission", self.team.permission_set]
-
- def setup(self, *args, **kwargs):
- super().setup(*args, **kwargs)
- self.team = get_object_or_404(
- Team, camp=self.camp, slug=self.kwargs["team_slug"]
- )
- self.queryset = FacilityFeedback.objects.filter(
- facility__facility_type__responsible_team=self.team, handled=False
- )
- self.form_class = modelformset_factory(
- FacilityFeedback,
- fields=("handled",),
- 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):
- context = super().get_context_data(*args, **kwargs)
- context["team"] = self.team
- context["feedback_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"Marked {len(form.changed_objects)} FacilityFeedbacks as handled!",
- )
- return redirect(self.get_success_url())
-
- def get_success_url(self, *args, **kwargs):
- return reverse(
- "backoffice:facilityfeedback",
- kwargs={"camp_slug": self.camp.slug, "team_slug": self.team.slug},
- )
-
-
class ProductHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView):
template_name = "product_handout.html"
@@ -227,6 +180,298 @@ class ApproveFeedbackView(CampViewMixin, ContentTeamPermissionMixin, FormView):
)
+##########################
+# MANAGE FACILITIES VIEWS
+
+
+class FacilityTypeListView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
+ model = FacilityType
+ template_name = "facility_type_list_backoffice.html"
+ context_object_name = "facility_type_list"
+
+
+class FacilityTypeCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView):
+ model = FacilityType
+ template_name = "facility_type_form.html"
+ fields = [
+ "name",
+ "description",
+ "icon",
+ "marker",
+ "responsible_team",
+ "quickfeedback_options",
+ ]
+
+ def get_context_data(self, **kwargs):
+ """
+ Do not show teams that are not part of the current camp in the dropdown
+ """
+ context = super().get_context_data(**kwargs)
+ context["form"].fields["responsible_team"].queryset = Team.objects.filter(
+ camp=self.camp
+ )
+ return context
+
+ def get_success_url(self):
+ return reverse(
+ "backoffice:facility_type_list", kwargs={"camp_slug": self.camp.slug}
+ )
+
+
+class FacilityTypeUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView):
+ model = FacilityType
+ template_name = "facility_type_form.html"
+ fields = [
+ "name",
+ "description",
+ "icon",
+ "marker",
+ "responsible_team",
+ "quickfeedback_options",
+ ]
+
+ def get_context_data(self, **kwargs):
+ """
+ Do not show teams that are not part of the current camp in the dropdown
+ """
+ context = super().get_context_data(**kwargs)
+ context["form"].fields["responsible_team"].queryset = Team.objects.filter(
+ camp=self.camp
+ )
+ return context
+
+ def get_success_url(self):
+ return reverse(
+ "backoffice:facility_type_list", kwargs={"camp_slug": self.camp.slug}
+ )
+
+
+class FacilityTypeDeleteView(CampViewMixin, OrgaTeamPermissionMixin, DeleteView):
+ model = FacilityType
+ template_name = "facility_type_delete.html"
+ context_object_name = "facility_type"
+
+ def delete(self, *args, **kwargs):
+ for facility in self.get_object().facilities.all():
+ facility.feedbacks.all().delete()
+ facility.opening_hours.all().delete()
+ facility.delete()
+ return super().delete(*args, **kwargs)
+
+ def get_success_url(self):
+ messages.success(self.request, "The FacilityType has been deleted")
+ return reverse(
+ "backoffice:facility_type_list", kwargs={"camp_slug": self.camp.slug}
+ )
+
+
+class FacilityListView(CampViewMixin, OrgaTeamPermissionMixin, ListView):
+ model = Facility
+ template_name = "facility_list_backoffice.html"
+
+
+class FacilityDetailView(CampViewMixin, OrgaTeamPermissionMixin, DetailView):
+ model = Facility
+ template_name = "facility_detail_backoffice.html"
+ pk_url_kwarg = "facility_uuid"
+
+ def get_queryset(self, *args, **kwargs):
+ qs = super().get_queryset(*args, **kwargs)
+ return qs.prefetch_related("opening_hours")
+
+
+class FacilityCreateView(CampViewMixin, OrgaTeamPermissionMixin, CreateView):
+ model = Facility
+ template_name = "facility_form.html"
+ fields = ["facility_type", "name", "description", "location"]
+
+ def get_form(self, *args, **kwargs):
+ form = super().get_form(*args, **kwargs)
+ form.fields["location"].widget = LeafletWidget(attrs={"display_raw": "true",})
+ return form
+
+ def get_context_data(self, **kwargs):
+ """
+ Do not show types that are not part of the current camp in the dropdown
+ """
+ context = super().get_context_data(**kwargs)
+ context["form"].fields["facility_type"].queryset = FacilityType.objects.filter(
+ responsible_team__camp=self.camp
+ )
+ return context
+
+ def get_success_url(self):
+ messages.success(self.request, "The Facility has been created")
+ return reverse("backoffice:facility_list", kwargs={"camp_slug": self.camp.slug})
+
+
+class FacilityUpdateView(CampViewMixin, OrgaTeamPermissionMixin, UpdateView):
+ model = Facility
+ template_name = "facility_form.html"
+ pk_url_kwarg = "facility_uuid"
+ fields = ["facility_type", "name", "description", "location"]
+
+ def get_form(self, *args, **kwargs):
+ form = super().get_form(*args, **kwargs)
+ form.fields["location"].widget = LeafletWidget(attrs={"display_raw": "true",})
+ return form
+
+ def get_success_url(self):
+ messages.success(self.request, "The Facility has been updated")
+ return reverse(
+ "backoffice:facility_detail",
+ kwargs={
+ "camp_slug": self.camp.slug,
+ "facility_uuid": self.get_object().uuid,
+ },
+ )
+
+
+class FacilityDeleteView(CampViewMixin, OrgaTeamPermissionMixin, DeleteView):
+ model = Facility
+ template_name = "facility_delete.html"
+ pk_url_kwarg = "facility_uuid"
+
+ def delete(self, *args, **kwargs):
+ self.get_object().feedbacks.all().delete()
+ self.get_object().opening_hours.all().delete()
+ return super().delete(*args, **kwargs)
+
+ def get_success_url(self):
+ messages.success(self.request, "The Facility has been deleted")
+ return reverse("backoffice:facility_list", kwargs={"camp_slug": self.camp.slug})
+
+
+class FacilityFeedbackView(CampViewMixin, RaisePermissionRequiredMixin, FormView):
+ template_name = "facilityfeedback_backoffice.html"
+
+ def get_permission_required(self):
+ """
+ This view requires two permissions, camps.backoffice_permission and
+ the permission_set for the team in question.
+ """
+ if not self.team.permission_set:
+ raise PermissionDenied("No permissions set defined for this team")
+ return ["camps.backoffice_permission", self.team.permission_set]
+
+ def setup(self, *args, **kwargs):
+ super().setup(*args, **kwargs)
+ self.team = get_object_or_404(
+ Team, camp=self.camp, slug=self.kwargs["team_slug"]
+ )
+ self.queryset = FacilityFeedback.objects.filter(
+ facility__facility_type__responsible_team=self.team, handled=False
+ )
+ self.form_class = modelformset_factory(
+ FacilityFeedback,
+ fields=("handled",),
+ 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):
+ context = super().get_context_data(*args, **kwargs)
+ context["team"] = self.team
+ context["feedback_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"Marked {len(form.changed_objects)} FacilityFeedbacks as handled!",
+ )
+ return redirect(self.get_success_url())
+
+ def get_success_url(self, *args, **kwargs):
+ return reverse(
+ "backoffice:facilityfeedback",
+ kwargs={"camp_slug": self.camp.slug, "team_slug": self.team.slug},
+ )
+
+
+class FacilityMixin(CampViewMixin):
+ def setup(self, *args, **kwargs):
+ super().setup(*args, **kwargs)
+ self.facility = get_object_or_404(Facility, uuid=kwargs["facility_uuid"])
+
+ def get_form(self, *args, **kwargs):
+ """
+ The default range widgets are a bit shit because they eat the help_text and
+ have no indication of which field is for what. So we add a nice placeholder.
+ """
+ form = super().get_form(*args, **kwargs)
+ form.fields["when"].widget.widgets[0].attrs = {
+ "placeholder": f"Open Date and Time (YYYY-MM-DD HH:MM). Active time zone is {settings.TIME_ZONE}.",
+ }
+ form.fields["when"].widget.widgets[1].attrs = {
+ "placeholder": f"Close Date and Time (YYYY-MM-DD HH:MM). Active time zone is {settings.TIME_ZONE}.",
+ }
+ return form
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["facility"] = self.facility
+ return context
+
+
+class FacilityOpeningHoursCreateView(
+ FacilityMixin, OrgaTeamPermissionMixin, CreateView
+):
+ model = FacilityOpeningHours
+ template_name = "facility_opening_hours_form.html"
+ fields = ["when", "notes"]
+
+ def form_valid(self, form):
+ """
+ Set facility before saving
+ """
+ hours = form.save(commit=False)
+ hours.facility = self.facility
+ hours.save()
+ messages.success(self.request, f"New opening hours created successfully!")
+ return redirect(
+ reverse(
+ "backoffice:facility_detail",
+ kwargs={"camp_slug": self.camp.slug, "facility_uuid": self.facility.pk},
+ )
+ )
+
+
+class FacilityOpeningHoursUpdateView(
+ FacilityMixin, OrgaTeamPermissionMixin, UpdateView
+):
+ model = FacilityOpeningHours
+ template_name = "facility_opening_hours_form.html"
+ fields = ["when", "notes"]
+
+ def get_success_url(self):
+ messages.success(self.request, "Opening hours have been updated successfully")
+ return reverse(
+ "backoffice:facility_detail",
+ kwargs={"camp_slug": self.camp.slug, "facility_uuid": self.facility.pk},
+ )
+
+
+class FacilityOpeningHoursDeleteView(
+ FacilityMixin, OrgaTeamPermissionMixin, DeleteView
+):
+ model = FacilityOpeningHours
+ template_name = "facility_opening_hours_delete.html"
+
+ def get_success_url(self):
+ messages.success(self.request, "Opening hours have been deleted successfully")
+ return reverse(
+ "backoffice:facility_detail",
+ kwargs={"camp_slug": self.camp.slug, "facility_uuid": self.facility.pk},
+ )
+
+
#######################################
# MANAGE SPEAKER/EVENT PROPOSAL VIEWS
diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py
index 14a6f8d9..4a3c2861 100644
--- a/src/bornhack/settings.py
+++ b/src/bornhack/settings.py
@@ -189,3 +189,6 @@ LOGGING = {
}
GRAPHENE = {"SCHEMA": "bornhack.schema.schema"}
+LEAFLET_CONFIG = {
+ "PLUGINS": {"forms": {"auto-include": True}},
+}
diff --git a/src/facilities/admin.py b/src/facilities/admin.py
index ec00cff3..177a830a 100644
--- a/src/facilities/admin.py
+++ b/src/facilities/admin.py
@@ -2,7 +2,13 @@ from django.contrib import admin
from django.utils.html import format_html
from leaflet.admin import LeafletGeoAdmin
-from .models import Facility, FacilityFeedback, FacilityQuickFeedback, FacilityType
+from .models import (
+ Facility,
+ FacilityFeedback,
+ FacilityOpeningHours,
+ FacilityQuickFeedback,
+ FacilityType,
+)
@admin.register(FacilityQuickFeedback)
@@ -63,3 +69,9 @@ class FacilityFeedbackAdmin(admin.ModelAdmin):
"facility__facility_type__responsible_team",
"facility",
]
+
+
+@admin.register(FacilityOpeningHours)
+class FacilityOpeningHoursAdmin(admin.ModelAdmin):
+ list_display = ["facility", "when", "notes"]
+ list_filter = ["facility"]
diff --git a/src/facilities/migrations/0005_facilityopeninghours.py b/src/facilities/migrations/0005_facilityopeninghours.py
new file mode 100644
index 00000000..55c885f8
--- /dev/null
+++ b/src/facilities/migrations/0005_facilityopeninghours.py
@@ -0,0 +1,55 @@
+# Generated by Django 3.0.3 on 2020-06-06 12:06
+
+import django.contrib.postgres.fields.ranges
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("facilities", "0004_facilitytype_marker"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="FacilityOpeningHours",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("created", models.DateTimeField(auto_now_add=True)),
+ ("updated", models.DateTimeField(auto_now=True)),
+ (
+ "when",
+ django.contrib.postgres.fields.ranges.DateTimeRangeField(
+ db_index=True,
+ help_text="The period when this facility is open.",
+ ),
+ ),
+ (
+ "notes",
+ models.TextField(
+ blank=True,
+ help_text="Any notes for this period like 'no hot food after 20' or 'no alcohol sale after 02'. Optional.",
+ ),
+ ),
+ (
+ "facility",
+ models.ForeignKey(
+ help_text="The Facility to which these opening hours belong.",
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="opening_hours",
+ to="facilities.Facility",
+ ),
+ ),
+ ],
+ options={"abstract": False,},
+ ),
+ ]
diff --git a/src/facilities/migrations/0006_auto_20200616_2330.py b/src/facilities/migrations/0006_auto_20200616_2330.py
new file mode 100644
index 00000000..381d2c92
--- /dev/null
+++ b/src/facilities/migrations/0006_auto_20200616_2330.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.0.3 on 2020-06-16 21:30
+
+import django.contrib.gis.db.models.fields
+import django.contrib.gis.geos.point
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("facilities", "0005_facilityopeninghours"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="facility",
+ name="location",
+ field=django.contrib.gis.db.models.fields.PointField(
+ default=django.contrib.gis.geos.point.Point(9.93891, 55.38562),
+ help_text="The location of this facility",
+ srid=4326,
+ ),
+ ),
+ ]
diff --git a/src/facilities/migrations/0007_default_location_and_hours_overlap_contraint.py b/src/facilities/migrations/0007_default_location_and_hours_overlap_contraint.py
new file mode 100644
index 00000000..b61fd55d
--- /dev/null
+++ b/src/facilities/migrations/0007_default_location_and_hours_overlap_contraint.py
@@ -0,0 +1,35 @@
+# Generated by Django 3.0.3 on 2020-06-17 15:48
+
+import django.contrib.gis.db.models.fields
+import django.contrib.gis.geos.point
+import django.contrib.postgres.constraints
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("facilities", "0006_auto_20200616_2330"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="facilityopeninghours", options={"ordering": ["when"]},
+ ),
+ migrations.AlterField(
+ model_name="facility",
+ name="location",
+ field=django.contrib.gis.db.models.fields.PointField(
+ default=django.contrib.gis.geos.point.Point(9.93891, 55.38562),
+ help_text="The location of this facility.",
+ srid=4326,
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="facilityopeninghours",
+ constraint=django.contrib.postgres.constraints.ExclusionConstraint(
+ expressions=[("when", "&&"), ("facility", "=")],
+ name="prevent_facility_opening_hours_overlaps",
+ ),
+ ),
+ ]
diff --git a/src/facilities/models.py b/src/facilities/models.py
index df0930b2..ef1fc556 100644
--- a/src/facilities/models.py
+++ b/src/facilities/models.py
@@ -4,6 +4,9 @@ import logging
import qrcode
from django.contrib.gis.db.models import PointField
+from django.contrib.gis.geos import Point
+from django.contrib.postgres.constraints import ExclusionConstraint
+from django.contrib.postgres.fields import DateTimeRangeField, RangeOperators
from django.db import models
from django.shortcuts import reverse
from maps.utils import LeafletMarkerChoices
@@ -37,7 +40,7 @@ class FacilityType(CampRelatedModel):
"""
Facility types are used to group similar facilities, like Toilets, Showers, Thrashcans...
facilities.Type has a m2m relationship with FeedbackChoice which determines which choices
- are presented for giving feedback for this Facility
+ are presented for giving feedback for facilities of this type
"""
class Meta:
@@ -113,7 +116,10 @@ class Facility(CampRelatedModel, UUIDModel):
description = models.TextField(help_text="Description of this facility")
- location = PointField(help_text="The location of this facility")
+ # default to near the workshop rooms / cabins
+ location = PointField(
+ default=Point(9.93891, 55.38562), help_text="The location of this facility."
+ )
@property
def team(self):
@@ -151,6 +157,9 @@ class Facility(CampRelatedModel, UUIDModel):
qrcode_base64 = base64.b64encode(file_like.getvalue()).decode("utf-8")
return f"data:image/png;base64,{qrcode_base64}"
+ def unhandled_feedbacks(self):
+ return self.feedbacks.filter(handled=False)
+
class FacilityFeedback(CampRelatedModel):
"""
@@ -210,3 +219,51 @@ class FacilityFeedback(CampRelatedModel):
return self.facility.camp
camp_filter = "facility__facility_type__responsible_team__camp"
+
+
+class FacilityOpeningHours(CampRelatedModel):
+ """
+ This model contains opening hours for facilities which are not always open.
+ If a facility has zero entries in this model it means is always open.
+ If a facility has one or more periods of opening hours defined in this model
+ it is considered closed outside of the period(s) defined in this model.
+ """
+
+ class Meta:
+ ordering = ["when"]
+ constraints = [
+ # we do not want overlapping hours for the same Facility
+ ExclusionConstraint(
+ name="prevent_facility_opening_hours_overlaps",
+ expressions=[
+ ("when", RangeOperators.OVERLAPS),
+ ("facility", RangeOperators.EQUAL),
+ ],
+ ),
+ ]
+
+ facility = models.ForeignKey(
+ "facilities.Facility",
+ related_name="opening_hours",
+ on_delete=models.PROTECT,
+ help_text="The Facility to which these opening hours belong.",
+ )
+
+ when = DateTimeRangeField(
+ db_index=True, help_text="The period when this facility is open.",
+ )
+
+ notes = models.TextField(
+ blank=True,
+ help_text="Any notes for this period like 'no hot food after 20' or 'no alcohol sale after 02'. Optional.",
+ )
+
+ @property
+ def camp(self):
+ return self.facility.camp
+
+ camp_filter = "facility__facility_type__responsible_team__camp"
+
+ @property
+ def duration(self):
+ return self.when.upper - self.when.lower
diff --git a/src/facilities/templates/facility_detail.html b/src/facilities/templates/facility_detail.html
index c84bccda..d783fd0d 100644
--- a/src/facilities/templates/facility_detail.html
+++ b/src/facilities/templates/facility_detail.html
@@ -1,6 +1,7 @@
{% extends 'program_base.html' %}
{% load leaflet_tags %}
{% load static %}
+{% load commonmark %}
{% block extra_head %}
{% leaflet_css %}
@@ -20,13 +21,61 @@
{{ facility.facility_type.name }}: {{ facility.name }}
-
- {{ facility.name }} {{ facility.description }}
+
+
+
+ Facility Name |
+ {{ facility.name }} |
+
+
+ Facility Type |
+ {{ facility.facility_type.name }}
+ |
+
+ Description |
+ {{ facility.description }} |
+
+
+ Location |
+ Lat {{ facility.location.y }} Long {{ facility.location.x }} |
+
+
+ Opening Hours |
+
+ {% if facility.opening_hours.exists %}
+
+
+
+ Opens |
+ Closes |
+ Duration |
+ Notes |
+
+
+
+ {% for opening in facility.opening_hours.all %}
+
+ {{ opening.when.lower }} |
+ {{ opening.when.upper }} |
+ {{ opening.duration }} |
+ {{ opening.notes|trustedcommonmark|default:"N/A" }} |
+
+ {% endfor %}
+
+
+ {% else %}
+ This facility does not have opening hours, it is always open.
+ {% endif %}
+ |
+
+
+
+
{% if request.user.is_authenticated %}
Submit Feedback
{% endif %}
- Back to {{ facilitytype.name }} list
-
+ Back to {{ facilitytype.name }} list
+
@@ -35,7 +84,7 @@
function MapReadyCallback() {
// add a marker for this facility
{% url "facilities:facility_feedback" camp_slug=facility.camp.slug facility_type_slug=facility.facility_type.slug facility_uuid=facility.uuid as feedback %}
- var marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}])
+ var marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}], {icon: {{ facility.facility_type.marker }}})
marker.bindPopup("