fixup facility maps a bit and add backoffice management of facilities

This commit is contained in:
Thomas Steen Rasmussen 2020-06-17 21:38:07 +02:00
parent 1451874ba7
commit f859f82b9c
23 changed files with 1056 additions and 81 deletions

View File

@ -0,0 +1,137 @@
{% extends 'base.html' %}
{% load leaflet_tags %}
{% load static %}
{% load commonmark %}
{% block extra_head %}
{% leaflet_css %}
<script src="{% static 'js/leaflet-1.6.0.js' %}" type="text/javascript"></script>
<script src="{% static 'js/proj4.js' %}" type="text/javascript"></script>
<script src="{% static 'js/proj4leaflet.js' %}" type="text/javascript"></script>
<script src="{% static 'js/leaflet-color-markers.js' %}" type="text/javascript"></script>
{% endblock extra_head %}
{% block title %}
{{ facility.name }} | Facilities | BackOffice | {{ block.super }}
{% endblock %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{ facility.name }} | Facilities | BackOffice</h3>
</div>
<div class="panel-body">
<p>
<a href="{% url 'backoffice:facility_update' camp_slug=camp.slug facility_uuid=facility.uuid %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update Facility</a>
<a href="{% url 'backoffice:facility_delete' camp_slug=camp.slug facility_uuid=facility.uuid %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete Facility</a>
<a href="{% url 'backoffice:facility_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Facility List</a>
</p>
<table class="table">
<tbody>
<tr>
<th>Facility Name</th>
<td>{{ facility.name }}</td>
</tr>
<tr>
<th>Facility Type</th>
<td><i class="{{ facility.facility_type.icon }}"></i> {{ facility.facility_type.name }}</p>
</tr>
<tr>
<th>Description</th>
<td>{{ facility.description }}</td>
</tr>
<tr>
<th>Opening Hours</th>
<td>
{% if facility.opening_hours.exists %}
<table class="table table-condensed">
<thead>
<tr>
<th>Opens</th>
<th>Closes</th>
<th>Duration</th>
<th>Notes</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for opening in facility.opening_hours.all %}
<tr>
<td>{{ opening.when.lower }}</td>
<td>{{ opening.when.upper }}</td>
<td>{{ opening.duration }}</td>
<td>{{ opening.notes|trustedcommonmark|default:"N/A" }}</td>
<td>
<div class="btn-group">
<a href="{% url 'backoffice:facility_opening_hours_update' camp_slug=camp.slug facility_uuid=facility.pk pk=opening.pk %}" class="btn btn-primary btn-sm"><i class="fas fa-edit"></i> Update</a>
<a href="{% url 'backoffice:facility_opening_hours_delete' camp_slug=camp.slug facility_uuid=facility.pk pk=opening.pk %}" class="btn btn-danger btn-sm"><i class="fas fa-times"></i> Delete</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
This facility does not have opening hours, it is always open.
{% endif %}
<p>
<a href="{% url 'backoffice:facility_opening_hours_create' camp_slug=camp.slug facility_uuid=facility.pk %}" class="btn btn-success btn-sm"><i class="fas fa-plus"></i> Add opening hours</a>
</td>
</tr>
<tr>
<th>Feedback</th>
<td>
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Created</th>
<th>Facility</th>
<th>Quick Feedback</th>
<th>Comment</th>
<th>Urgent</th>
<th>Handled</th>
</tr>
</thead>
<tbody>
{% for feedback in facility.feedbacks.all %}
<tr>
<td>{{ feedback.user|default:"N/A" }}</td>
<td>{{ feedback.created }}</td>
<td>{{ feedback.facility }}</td>
<td><i class="{{ feedback.quick_feedback.icon }} fa-2x"></i> {{ feedback.quick_feedback }}</td>
<td>{{ feedback.comment|default:"N/A" }}</td>
<td>{{ feedback.urgent|yesno }}</td>
<td>{{ feedback.handled|yesno }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</td>
</tr>
<tr>
<th>Location</th>
<td>
Lat {{ facility.location.y }} Long {{ facility.location.x }}<br>
<div id="map" class="map"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<script type="text/javascript">
function MapReadyCallback() {
// add a marker for this facility
var marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}])
marker.bindPopup("<b>{{ facility.name }}</b><br><p>{{ facility.description}}</p>").addTo(this);
// max zoom since we have only one marker
this.setView(marker.getLatLng(), 13);
};
</script>
<script src="{% static 'js/kfmap.js' %}" type="text/javascript"></script>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends 'base.html' %}
{% load leaflet_tags %}
{% load bootstrap3 %}
{% load static %}
{% block extra_head %}
{{ form.media }}
{% leaflet_css plugins="forms" %}
{% leaflet_js plugins="forms" %}
{% endblock extra_head %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% if request.resolver_match.url_name == "facility_update" %}Update{% else %}Create new{% endif %} Facility</h3>
</div>
<div class="panel-body">
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Save</button>
<a href="{% url 'backoffice:facility_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
{% endbuttons %}
</form>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,52 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}
Facilities | BackOffice | {{ block.super }}
{% endblock %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Facilities - BackOffice</h3>
</div>
<div class="panel-body">
<p class="lead">The following {{ facility_list.count }} facilities are defined for {{ camp.title }}</p>
<p>
<a href="{% url 'backoffice:facility_create' camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create Facility</a>
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
</p>
<table class="table datatable">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Team</th>
<th>Description</th>
<th>Location</th>
<th class="text-center">Feedback / Unhandled</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for facility in facility_list %}
<tr>
<td>{{ facility.name }}</td>
<td><i class="fas fa-{{ facility.facility_type.icon }} fa-2x fa-fw"></i> {{ facility.facility_type.name }}</td>
<td>{{ facility.team.name }} Team</td>
<td>{{ facility.description|default:"N/A" }}</td>
<td>{{ facility.location }}</td>
<td class="text-center"><span class="badge">{{ facility.feedbacks.count }}</span> / <span class="badge">{{ facility.unhandled_feedbacks.count }}</span></td>
<td>
<div class="btn-group btn-group-vertical">
<a href="{% url "backoffice:facility_detail" camp_slug=camp.slug facility_uuid=facility.pk %}" class="btn btn-primary"><i class="fas fa-search"></i> Details</a>
<a href="{% url "backoffice:facility_update" camp_slug=camp.slug facility_uuid=facility.pk %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update</a>
<a href="{% url "backoffice:facility_delete" camp_slug=camp.slug facility_uuid=facility.pk %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete</a>
</div>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Delete Facility Opening Hours for {{ object.facility.name }}?</h3>
</div>
<div class="panel-body">
<p class="lead">This object specifies that {{ object.facility.name }} opens at {{ object.when.lower }} and closes at {{ object.when.upper }}.</p>
<p>Really delete it?</p>
<form method="POST">
{% csrf_token %}
<button type="submit" name="Delete" class="btn btn-danger"><i class="fas fa-times"></i> Yes, Delete it</button>
<a href="{% url 'backoffice:facility_detail' camp_slug=camp.slug facility_uuid=object.facility.pk %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
</form>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% load leaflet_tags %}
{% load bootstrap3 %}
{% load static %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% if request.resolver_match.url_name == "facility_opening_hours_update" %}Update{% else %}Create new{% endif %} Facility Opening Hours for {{ facility.name }}</h3>
</div>
<div class="panel-body">
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Save</button>
<a href="{% url 'backoffice:facility_detail' camp_slug=camp.slug facility_uuid=facility.pk %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
{% endbuttons %}
</form>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Delete FacilityType {{ facility_type.name }}?</h2>
</div>
<div class="panel-body">
<p class="lead">This FacilityType has <b>{{ facility_type.facilities.count }}</b> Facilities which will also be deleted.</p>
<form method="POST">
{% csrf_token %}
<button type="submit" name="Delete" class="btn btn-danger"><i class="fas fa-times"></i> Yes, Delete it</button>
<a href="{% url 'backoffice:facility_type_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
</form>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% if form.instance.pk %}Update{% else %}Create new{% endif %} FacilityType</h3>
</div>
<div class="panel-body">
<p class="lead">{% if form.instance.pk %}Update{% else %}Create{% endif %} FacilityType</p>
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Save</button>
<a href="{% url 'backoffice:facility_type_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
</form>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,52 @@
{% extends 'base.html' %}
{% load leaflet_tags %}
{% load static %}
{% block title %}
Facility Types | BackOffice | {{ block.super }}
{% endblock %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Facility Types - BackOffice</h3>
</div>
<div class="panel-body">
<p class="lead">The following {{ facility_type_list.count }} facility types are defined for {{ camp.title }}</p>
<p>
<a href="{% url 'backoffice:facility_type_create' camp_slug=camp.slug %}" class="btn btn-success"><i class="fas fa-plus"></i> Create Facility Type</a>
<a class="btn btn-default" href="{% url 'backoffice:index' camp_slug=camp.slug %}"><i class="fas fa-undo"></i> Backoffice</a>
</p>
<table class="table datatable">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Team</th>
<th class="text-center">Icon</th>
<th class="text-center">Marker</th>
<th class="text-center">QuickFeedbacks</th>
<th class="text-center">Facilities</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for ft in facility_type_list %}
<tr>
<td>{{ ft.name }}</td>
<td>{{ ft.description|default:"N/A" }}</td>
<td>{{ ft.responsible_team.name }} Team</td>
<td class="text-center"><i class="fas fa-{{ ft.icon }} fa-2x fa-fw"></i></td>
<td class="text-center"><img src="{% static 'img/leaflet/marker-icon-'|add:ft.marker|slice:"-4"|add:'.png' %}"></td>
<td class="text-center"><span class="badge">{{ ft.quickfeedback_options.count }}</span></td>
<td class="text-center"><span class="badge">{{ ft.facilities.count }}</span></td>
<td>
<a href="{% url "backoffice:facility_type_update" camp_slug=camp.slug slug=ft.slug %}" class="btn btn-primary"><i class="fas fa-edit"></i> Update</a>
<a href="{% url "backoffice:facility_type_delete" camp_slug=camp.slug slug=ft.slug %}" class="btn btn-danger"><i class="fas fa-times"></i> Delete</a>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock %}

View File

@ -60,7 +60,7 @@ Facility Feedback for {{ team.name }} Team | {{ block.super }}
</form>
{% else %}
<p class="lead">No unhandled feedback found for any Facilities managed by {{ team.name }} Team. Good job!</p>
<a href="{% url 'backoffice:index' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Backoffice</a>
<a href="{% url 'backoffice:index' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Backoffice</a>
{% endif %}
</div>
</div>

View File

@ -16,27 +16,36 @@
<div class="panel-body">
<p class="lead">Welcome to the promised land! Please select your desired action below:</p>
<div class="list-group">
<h3>Facilities</h3>
{% if perms.camps.orgateam_permission %}
<a href="{% url 'backoffice:facility_type_list' camp_slug=camp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">
Facility Types
</h4>
<p class="list-group-item-text">
See and manage facility types
</p>
</a>
<a href="{% url 'backoffice:facility_list' camp_slug=camp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">
Facilities
</h4>
<p class="list-group-item-text">
See and manage facilites
</p>
</a>
{% endif %}
{% for team in facilityfeedback_teams %}
{% if "camps."|add:team.permission_set in perms %}
{% if forloop.first %}
<h3>Facility Feedback</h3>
{% endif %}
<a href="{% url 'backoffice:facilityfeedback' camp_slug=camp.slug team_slug=team.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">
{{ team.name }} Team
Feedback for {{ team.name }} Team
</h4>
<p class="list-group-item-text">
See unhandled feedback for facilities managed by {{ team.name }} Team
</p>
</a>
{% endif %}
{% empty %}
<div class="list-group-item">
<h4 class="list-group-item-heading">N/A</h4>
<p class="list-group-item-text">
No unhandled Facility Feedback found!
</p>
</div>
{% endfor %}
{% if perms.camps.infoteam_permission %}

View File

@ -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/<slug:proxy_slug>/", BackofficeProxyView.as_view(), name="proxy"),
# facility feedback
# facilities
path(
"feedback/facilities/<slug:team_slug>/",
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(
"<slug:slug>/",
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(
"<uuid:facility_uuid>/",
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(
"<int:pk>/",
include(
[
path(
"update/",
FacilityOpeningHoursUpdateView.as_view(),
name="facility_opening_hours_update",
),
path(
"delete/",
FacilityOpeningHoursDeleteView.as_view(),
name="facility_opening_hours_delete",
),
]
),
),
]
),
),
]
),
),
]
),
),
# infodesk
path(
"infodesk/",

View File

@ -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

View File

@ -189,3 +189,6 @@ LOGGING = {
}
GRAPHENE = {"SCHEMA": "bornhack.schema.schema"}
LEAFLET_CONFIG = {
"PLUGINS": {"forms": {"auto-include": True}},
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -1,6 +1,7 @@
{% extends 'program_base.html' %}
{% load leaflet_tags %}
{% load static %}
{% load commonmark %}
{% block extra_head %}
{% leaflet_css %}
@ -20,13 +21,61 @@
<h3 class="panel-title">{{ facility.facility_type.name }}: {{ facility.name }}</h3>
</div>
<div class="panel-body">
<p class="lead">
<i class="{{ facility.facility_type.icon }} fa-2x fa-pull-left fa-fw"></i> {{ facility.name }} {{ facility.description }}</p>
<table class="table">
<tbody>
<tr>
<th>Facility Name</th>
<td>{{ facility.name }}</td>
</tr>
<tr>
<th>Facility Type</th>
<td><i class="{{ facility.facility_type.icon }}"></i> {{ facility.facility_type.name }}</p>
</tr>
<tr>
<th>Description</th>
<td>{{ facility.description }}</td>
</tr>
<tr>
<th>Location</th>
<td>Lat {{ facility.location.y }} Long {{ facility.location.x }}</td>
</tr>
<tr>
<th>Opening Hours</th>
<td>
{% if facility.opening_hours.exists %}
<table class="table table-condensed">
<thead>
<tr>
<th>Opens</th>
<th>Closes</th>
<th>Duration</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{% for opening in facility.opening_hours.all %}
<tr>
<td>{{ opening.when.lower }}</td>
<td>{{ opening.when.upper }}</td>
<td>{{ opening.duration }}</td>
<td>{{ opening.notes|trustedcommonmark|default:"N/A" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
This facility does not have opening hours, it is always open.
{% endif %}
</td>
</tr>
</tbody>
</table>
<p>
{% if request.user.is_authenticated %}
<a href="{% url "facilities:facility_feedback" camp_slug=camp.slug facility_type_slug=facilitytype.slug facility_uuid=facility.uuid %}" class="btn btn-primary"><i class="fas fa-comment-dots"></i> Submit Feedback</a>
{% endif %}
<a href="{% url "facilities:facility_list" camp_slug=camp.slug facility_type_slug=facilitytype.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> Back to {{ facilitytype.name }} list</a>
<p>
<a href="{% url "facilities:facility_list" camp_slug=camp.slug facility_type_slug=facilitytype.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Back to {{ facilitytype.name }} list</a>
</p>
<div id="map" class="map"></div>
</div>
</div>
@ -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("<b>{{ facility.name }}</b><br><p>{{ facility.description }}</p><p>Responsible team: {{ facility.facility_type.responsible_team.name }} Team</p>{% if request.user.is_authenticated %}<p><a href='{{ feedback }}' class='btn btn-primary' style='color: white;'><i class='fas fa-comment-dots'></i> Feedback</a></p>{% endif %}").addTo(this);
// max zoom since we have only one marker
this.setView(marker.getLatLng(), 13);

View File

@ -17,7 +17,7 @@ Facilities of type {{ facilitytype }} | {{ block.super }}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Facilities of type {{ facilitytype }}</h3>
<h3 class="panel-title">Facilities of type {{ facilitytype }}</h3>
</div>
<div class="panel-body">
<div class="list-group">
@ -42,7 +42,7 @@ Facilities of type {{ facilitytype }} | {{ block.super }}
<p>
<div id="map" class="map"></div>
</div>
<a href="{% url "facilities:facility_type_list" camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> Back to facility type list</a>
<a href="{% url "facilities:facility_type_list" camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Back to facility type list</a>
</div>
</div>

View File

@ -29,18 +29,19 @@ class FacilityDetailView(FacilityTypeViewMixin, DetailView):
template_name = "facility_detail.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 FacilityFeedbackView(FacilityViewMixin, CreateView):
model = FacilityFeedback
template_name = "facility_feedback.html"
fields = ["quick_feedback", "comment", "urgent"]
def get_initial(self, *args, **kwargs):
initial = super().get_initial(*args, **kwargs)
return initial
def get_form(self, form_class=None):
"""
- Add quick feedback field to the form
- Add anon option to the form
"""
form = super().get_form(form_class)

View File

@ -339,3 +339,8 @@ body.bar-menu {
.no-js .hide-for-no-js-users {
display: none;
}
/* hide map in forms until we can show a real leaflet map */
div #id_location-map {
display: none;
}

View File

@ -96,6 +96,39 @@
// Add scale line to map, disable imperial units
L.control.scale({imperial: false}).addTo(map);
var Position = L.Control.extend({
_container: null,
options: {
position: 'bottomright'
},
onAdd: function (map) {
var latlng = L.DomUtil.create('div', 'mouseposition');
latlng.style = 'background: rgba(255, 255, 255, 0.7);';
this._latlng = latlng;
return latlng;
},
updateHTML: function(lat, lng) {
this._latlng.innerHTML = "&nbsp;Lat: " + lat + " Lng: " + lng + "<br>&nbsp;Right click to copy coordinates&nbsp;";
}
});
position = new Position();
map.addControl(position);
var lat;
var lng;
map.addEventListener('mousemove', (event) => {
lat = Math.round(event.latlng.lat * 100000) / 100000;
lng = Math.round(event.latlng.lng * 100000) / 100000;
this.position.updateHTML(lat, lng);
});
map.addEventListener("contextmenu", (event) => {
alert("Lat: " + lat + " Lng: " + lng + '\n\nGeoJSON:\n{ "type": "Point", "coordinates": [ ' + lng + ', ' + lat + ' ] }');
return false; // To disable default popup.
});
// fire our callback when ready
map.whenReady(MapReadyCallback);
})();