fixup facility maps a bit and add backoffice management of facilities
This commit is contained in:
parent
1451874ba7
commit
f859f82b9c
137
src/backoffice/templates/facility_detail_backoffice.html
Normal file
137
src/backoffice/templates/facility_detail_backoffice.html
Normal 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 %}
|
||||||
|
|
28
src/backoffice/templates/facility_form.html
Normal file
28
src/backoffice/templates/facility_form.html
Normal 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 %}
|
52
src/backoffice/templates/facility_list_backoffice.html
Normal file
52
src/backoffice/templates/facility_list_backoffice.html
Normal 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 %}
|
19
src/backoffice/templates/facility_opening_hours_delete.html
Normal file
19
src/backoffice/templates/facility_opening_hours_delete.html
Normal 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 %}
|
22
src/backoffice/templates/facility_opening_hours_form.html
Normal file
22
src/backoffice/templates/facility_opening_hours_form.html
Normal 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 %}
|
18
src/backoffice/templates/facility_type_delete.html
Normal file
18
src/backoffice/templates/facility_type_delete.html
Normal 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 %}
|
19
src/backoffice/templates/facility_type_form.html
Normal file
19
src/backoffice/templates/facility_type_form.html
Normal 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 %}
|
52
src/backoffice/templates/facility_type_list_backoffice.html
Normal file
52
src/backoffice/templates/facility_type_list_backoffice.html
Normal 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 %}
|
|
@ -60,7 +60,7 @@ Facility Feedback for {{ team.name }} Team | {{ block.super }}
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="lead">No unhandled feedback found for any Facilities managed by {{ team.name }} Team. Good job!</p>
|
<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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,27 +16,36 @@
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p class="lead">Welcome to the promised land! Please select your desired action below:</p>
|
<p class="lead">Welcome to the promised land! Please select your desired action below:</p>
|
||||||
<div class="list-group">
|
<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 %}
|
{% for team in facilityfeedback_teams %}
|
||||||
{% if "camps."|add:team.permission_set in perms %}
|
{% 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">
|
<a href="{% url 'backoffice:facilityfeedback' camp_slug=camp.slug team_slug=team.slug %}" class="list-group-item">
|
||||||
<h4 class="list-group-item-heading">
|
<h4 class="list-group-item-heading">
|
||||||
{{ team.name }} Team
|
Feedback for {{ team.name }} Team
|
||||||
</h4>
|
</h4>
|
||||||
<p class="list-group-item-text">
|
<p class="list-group-item-text">
|
||||||
See unhandled feedback for facilities managed by {{ team.name }} Team
|
See unhandled feedback for facilities managed by {{ team.name }} Team
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% 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 %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if perms.camps.infoteam_permission %}
|
{% if perms.camps.infoteam_permission %}
|
||||||
|
|
|
@ -43,7 +43,19 @@ from .views import (
|
||||||
EventUpdateView,
|
EventUpdateView,
|
||||||
ExpenseDetailView,
|
ExpenseDetailView,
|
||||||
ExpenseListView,
|
ExpenseListView,
|
||||||
|
FacilityCreateView,
|
||||||
|
FacilityDeleteView,
|
||||||
|
FacilityDetailView,
|
||||||
FacilityFeedbackView,
|
FacilityFeedbackView,
|
||||||
|
FacilityListView,
|
||||||
|
FacilityOpeningHoursCreateView,
|
||||||
|
FacilityOpeningHoursDeleteView,
|
||||||
|
FacilityOpeningHoursUpdateView,
|
||||||
|
FacilityTypeCreateView,
|
||||||
|
FacilityTypeDeleteView,
|
||||||
|
FacilityTypeListView,
|
||||||
|
FacilityTypeUpdateView,
|
||||||
|
FacilityUpdateView,
|
||||||
MerchandiseOrdersView,
|
MerchandiseOrdersView,
|
||||||
MerchandiseToOrderView,
|
MerchandiseToOrderView,
|
||||||
PendingProposalsView,
|
PendingProposalsView,
|
||||||
|
@ -77,11 +89,99 @@ urlpatterns = [
|
||||||
# proxy view
|
# proxy view
|
||||||
path("proxy/", BackofficeProxyView.as_view(), name="proxy"),
|
path("proxy/", BackofficeProxyView.as_view(), name="proxy"),
|
||||||
path("proxy/<slug:proxy_slug>/", BackofficeProxyView.as_view(), name="proxy"),
|
path("proxy/<slug:proxy_slug>/", BackofficeProxyView.as_view(), name="proxy"),
|
||||||
# facility feedback
|
# facilities
|
||||||
path(
|
path(
|
||||||
"feedback/facilities/<slug:team_slug>/",
|
"feedback/facilities/<slug:team_slug>/",
|
||||||
include([path("", FacilityFeedbackView.as_view(), name="facilityfeedback")]),
|
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
|
# infodesk
|
||||||
path(
|
path(
|
||||||
"infodesk/",
|
"infodesk/",
|
||||||
|
|
|
@ -21,7 +21,13 @@ from django.utils.safestring import mark_safe
|
||||||
from django.views.generic import DetailView, ListView, TemplateView
|
from django.views.generic import DetailView, ListView, TemplateView
|
||||||
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
||||||
from economy.models import Chain, Credebtor, Expense, Reimbursement, Revenue
|
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 profiles.models import Profile
|
||||||
from program.autoscheduler import AutoScheduler
|
from program.autoscheduler import AutoScheduler
|
||||||
from program.mixins import AvailabilityMatrixViewMixin
|
from program.mixins import AvailabilityMatrixViewMixin
|
||||||
|
@ -81,59 +87,6 @@ class BackofficeIndexView(CampViewMixin, RaisePermissionRequiredMixin, TemplateV
|
||||||
return context
|
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):
|
class ProductHandoutView(CampViewMixin, InfoTeamPermissionMixin, ListView):
|
||||||
template_name = "product_handout.html"
|
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
|
# MANAGE SPEAKER/EVENT PROPOSAL VIEWS
|
||||||
|
|
||||||
|
|
|
@ -189,3 +189,6 @@ LOGGING = {
|
||||||
}
|
}
|
||||||
|
|
||||||
GRAPHENE = {"SCHEMA": "bornhack.schema.schema"}
|
GRAPHENE = {"SCHEMA": "bornhack.schema.schema"}
|
||||||
|
LEAFLET_CONFIG = {
|
||||||
|
"PLUGINS": {"forms": {"auto-include": True}},
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,13 @@ from django.contrib import admin
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from leaflet.admin import LeafletGeoAdmin
|
from leaflet.admin import LeafletGeoAdmin
|
||||||
|
|
||||||
from .models import Facility, FacilityFeedback, FacilityQuickFeedback, FacilityType
|
from .models import (
|
||||||
|
Facility,
|
||||||
|
FacilityFeedback,
|
||||||
|
FacilityOpeningHours,
|
||||||
|
FacilityQuickFeedback,
|
||||||
|
FacilityType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(FacilityQuickFeedback)
|
@admin.register(FacilityQuickFeedback)
|
||||||
|
@ -63,3 +69,9 @@ class FacilityFeedbackAdmin(admin.ModelAdmin):
|
||||||
"facility__facility_type__responsible_team",
|
"facility__facility_type__responsible_team",
|
||||||
"facility",
|
"facility",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(FacilityOpeningHours)
|
||||||
|
class FacilityOpeningHoursAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["facility", "when", "notes"]
|
||||||
|
list_filter = ["facility"]
|
||||||
|
|
55
src/facilities/migrations/0005_facilityopeninghours.py
Normal file
55
src/facilities/migrations/0005_facilityopeninghours.py
Normal 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,},
|
||||||
|
),
|
||||||
|
]
|
24
src/facilities/migrations/0006_auto_20200616_2330.py
Normal file
24
src/facilities/migrations/0006_auto_20200616_2330.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -4,6 +4,9 @@ import logging
|
||||||
|
|
||||||
import qrcode
|
import qrcode
|
||||||
from django.contrib.gis.db.models import PointField
|
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.db import models
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from maps.utils import LeafletMarkerChoices
|
from maps.utils import LeafletMarkerChoices
|
||||||
|
@ -37,7 +40,7 @@ class FacilityType(CampRelatedModel):
|
||||||
"""
|
"""
|
||||||
Facility types are used to group similar facilities, like Toilets, Showers, Thrashcans...
|
Facility types are used to group similar facilities, like Toilets, Showers, Thrashcans...
|
||||||
facilities.Type has a m2m relationship with FeedbackChoice which determines which choices
|
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:
|
class Meta:
|
||||||
|
@ -113,7 +116,10 @@ class Facility(CampRelatedModel, UUIDModel):
|
||||||
|
|
||||||
description = models.TextField(help_text="Description of this facility")
|
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
|
@property
|
||||||
def team(self):
|
def team(self):
|
||||||
|
@ -151,6 +157,9 @@ class Facility(CampRelatedModel, UUIDModel):
|
||||||
qrcode_base64 = base64.b64encode(file_like.getvalue()).decode("utf-8")
|
qrcode_base64 = base64.b64encode(file_like.getvalue()).decode("utf-8")
|
||||||
return f"data:image/png;base64,{qrcode_base64}"
|
return f"data:image/png;base64,{qrcode_base64}"
|
||||||
|
|
||||||
|
def unhandled_feedbacks(self):
|
||||||
|
return self.feedbacks.filter(handled=False)
|
||||||
|
|
||||||
|
|
||||||
class FacilityFeedback(CampRelatedModel):
|
class FacilityFeedback(CampRelatedModel):
|
||||||
"""
|
"""
|
||||||
|
@ -210,3 +219,51 @@ class FacilityFeedback(CampRelatedModel):
|
||||||
return self.facility.camp
|
return self.facility.camp
|
||||||
|
|
||||||
camp_filter = "facility__facility_type__responsible_team__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
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% extends 'program_base.html' %}
|
{% extends 'program_base.html' %}
|
||||||
{% load leaflet_tags %}
|
{% load leaflet_tags %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load commonmark %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
{% leaflet_css %}
|
{% leaflet_css %}
|
||||||
|
@ -20,13 +21,61 @@
|
||||||
<h3 class="panel-title">{{ facility.facility_type.name }}: {{ facility.name }}</h3>
|
<h3 class="panel-title">{{ facility.facility_type.name }}: {{ facility.name }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p class="lead">
|
<table class="table">
|
||||||
<i class="{{ facility.facility_type.icon }} fa-2x fa-pull-left fa-fw"></i> {{ facility.name }} {{ facility.description }}</p>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% 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>
|
<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>
|
</p>
|
||||||
<div id="map" class="map"></div>
|
<div id="map" class="map"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,7 +84,7 @@
|
||||||
function MapReadyCallback() {
|
function MapReadyCallback() {
|
||||||
// add a marker for this facility
|
// 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 %}
|
{% 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);
|
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
|
// max zoom since we have only one marker
|
||||||
this.setView(marker.getLatLng(), 13);
|
this.setView(marker.getLatLng(), 13);
|
||||||
|
|
|
@ -42,7 +42,7 @@ Facilities of type {{ facilitytype }} | {{ block.super }}
|
||||||
<p>
|
<p>
|
||||||
<div id="map" class="map"></div>
|
<div id="map" class="map"></div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -29,18 +29,19 @@ class FacilityDetailView(FacilityTypeViewMixin, DetailView):
|
||||||
template_name = "facility_detail.html"
|
template_name = "facility_detail.html"
|
||||||
pk_url_kwarg = "facility_uuid"
|
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):
|
class FacilityFeedbackView(FacilityViewMixin, CreateView):
|
||||||
model = FacilityFeedback
|
model = FacilityFeedback
|
||||||
template_name = "facility_feedback.html"
|
template_name = "facility_feedback.html"
|
||||||
fields = ["quick_feedback", "comment", "urgent"]
|
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):
|
def get_form(self, form_class=None):
|
||||||
"""
|
"""
|
||||||
|
- Add quick feedback field to the form
|
||||||
- Add anon option to the form
|
- Add anon option to the form
|
||||||
"""
|
"""
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
|
|
|
@ -339,3 +339,8 @@ body.bar-menu {
|
||||||
.no-js .hide-for-no-js-users {
|
.no-js .hide-for-no-js-users {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* hide map in forms until we can show a real leaflet map */
|
||||||
|
div #id_location-map {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -96,6 +96,39 @@
|
||||||
// Add scale line to map, disable imperial units
|
// Add scale line to map, disable imperial units
|
||||||
L.control.scale({imperial: false}).addTo(map);
|
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 = " Lat: " + lat + " Lng: " + lng + "<br> Right click to copy coordinates ";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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
|
// fire our callback when ready
|
||||||
map.whenReady(MapReadyCallback);
|
map.whenReady(MapReadyCallback);
|
||||||
})();
|
})();
|
||||||
|
|
Loading…
Reference in a new issue