Skip to content

Commit

Permalink
Merge branch 'master' into issue_1200
Browse files Browse the repository at this point in the history
  • Loading branch information
zarya committed Oct 5, 2024
2 parents fdeb72d + e48961f commit 3745502
Show file tree
Hide file tree
Showing 173 changed files with 6,241 additions and 676 deletions.
45 changes: 45 additions & 0 deletions src/backoffice/forms.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from django.contrib.auth.models import User
from django import forms
from django.core.exceptions import ValidationError
import json
from django.forms import modelformset_factory

from program.models import Event
Expand Down Expand Up @@ -241,3 +244,45 @@ class PosSalesJSONForm(forms.Form):
sales = forms.FileField(
help_text="POS sales.json file. Previously imported sales will be skipped and will not create duplicates.",
)


class MapLayerFeaturesImportForm(forms.Form):
"""Form to import features in a map layer. Only accepts geojson type FeatureCollection."""

geojson_data = forms.CharField(
widget=forms.Textarea(),
help_text="The GeoJSON geometries to import.",
)

def clean(self):
"""Parse geojson and return as dict."""
cleaned_data = super().clean()
# validate json
try:
geojson = json.loads(cleaned_data["geojson_data"])
except json.JSONDecodeError:
raise ValidationError("Invalid JSON")
# validate geojson
if "type" not in geojson or geojson["type"] != "FeatureCollection":
raise ValidationError("Invalid GeoJSON - only FeatureCollection supported!")
# all good
return geojson


class ManageTeamPermissionsForm(forms.Form):
"""The form used in backoffice to manage permissions for a team."""

def __init__(self, matrix: dict[str, list[str]], *args, **kwargs):
"""Build a form of bool fields for the teams users permissions."""
super().__init__(*args, **kwargs)
for username in matrix.keys():
for perm in matrix[username]:
if perm in ["lead", "member"] or User.objects.get(username=username).is_superuser:
disabled = True
else:
disabled = False
self.fields[f"{username}_{perm}"] = forms.BooleanField(
label=perm,
required=False,
disabled=disabled,
)
54 changes: 32 additions & 22 deletions src/backoffice/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,43 @@ class OrgaTeamPermissionMixin(RaisePermissionRequiredMixin):
Permission mixin for views used by Orga Team
"""

permission_required = ("camps.backoffice_permission", "camps.orgateam_permission")
permission_required = "camps.orga_team_member"


class EconomyTeamPermissionMixin(RaisePermissionRequiredMixin):
"""
Permission mixin for views used by Economy Team
"""

permission_required = (
"camps.backoffice_permission",
"camps.economyteam_permission",
)
permission_required = "camps.economy_team_member"


class InfoTeamPermissionMixin(RaisePermissionRequiredMixin):
"""
Permission mixin for views used by Info Team/InfoDesk
"""

permission_required = ("camps.backoffice_permission", "camps.infoteam_permission")
permission_required = "camps.info_team_member"


class ContentTeamPermissionMixin(RaisePermissionRequiredMixin):
"""
Permission mixin for views used by Content Team
"""

permission_required = (
"camps.backoffice_permission",
"camps.contentteam_permission",
)
permission_required = "camps.content_team_member"


class GisTeamPermissionMixin(RaisePermissionRequiredMixin):
"""
Permission mixin for views used by GIS Team
"""

permission_required = "camps.gis_team_member"


class PosViewMixin(CampViewMixin, UserPassesTestMixin):
"""A mixin to set self.pos based on pos_slug in url kwargs."""
"""A mixin to set self.pos based on pos_slug in url kwargs, and only permit access for users with pos permission for the team."""

def setup(self, *args, **kwargs):
super().setup(*args, **kwargs)
Expand All @@ -58,20 +60,28 @@ def setup(self, *args, **kwargs):

def test_func(self):
"""
This view requires two permissions, camps.backoffice_permission and the permission_set for the team in question.
This view requires camps.<posteam>_team_pos permission.
"""
if not self.pos.team.permission_set:
raise PermissionDenied("No permissions set defined for this team")
if not self.request.user.has_perm("camps.backoffice_permission"):
raise PermissionDenied("User has no backoffice permission")

if not self.request.user.has_perm(
"camps.orgateam_permission",
) and not self.request.user.has_perm("camps." + self.pos.team.permission_set):
raise PermissionDenied("User has no permission for this Pos")
return True
if self.request.user.has_perm(f"camps.{self.pos.team.slug}_team_pos"):
return True
raise PermissionDenied("This PoS user has no permission for this PoS")

def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["pos"] = self.pos
return context


class OrgaOrTeamLeadViewMixin(CampViewMixin, UserPassesTestMixin):
"""A mixin for views that should be accessible only to orga and team leads."""

def test_func(self):
"""
This view requires camps.orga_team_member or camps.<any team>_team_lead permission.
"""
if self.request.user.has_perm("camps.orga_team_member"):
return True
for perm in self.request.user.get_all_permissions():
if perm.startswith("camps.") and perm.endswith("_team_lead"):
return True
raise PermissionDenied("No thanks")
22 changes: 22 additions & 0 deletions src/backoffice/templates/facility_delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% load bootstrap3 %}

{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Delete Facility {{ facility.name }}?</h2>
</div>
<div class="panel-body">
{% if facility.feedbacks.count > 0 %}
<p class="lead">This Facility has <b>{{ facility.feedbacks.count }}</b> Feedback and can not be deleted</p>
{% endif %}
<form method="POST">
{% csrf_token %}
{% if facility.feedbacks.count == 0 %}
<button type="submit" name="Delete" class="btn btn-danger"><i class="fas fa-times"></i> Yes, Delete it</button>
{% endif %}
<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 %}
30 changes: 12 additions & 18 deletions src/backoffice/templates/facility_detail_backoffice.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@

{% 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>
<link href="{% static 'css/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'css/leaflet.awesome-markers.css' %}" rel="stylesheet">
<script src="{% static 'vendor/leaflet/leaflet-1.6.0.js' %}" type="text/javascript"></script>
<script src="{% static 'vendor/leaflet/proj4.js' %}" type="text/javascript"></script>
<script src="{% static 'vendor/leaflet/proj4leaflet.js' %}" type="text/javascript"></script>
<script src="{% static 'vendor/leaflet/leaflet.awesome-markers.min.js' %}" type="text/javascript"></script>
<script src="{% static 'vendor/leaflet/leaflet-color-markers.js' %}" type="text/javascript"></script>
{{ mapData|json_script:"mapData" }}
<script src="{% static 'js/maps/generic/mapVars.js' %}" type="text/javascript"></script>
<script src="{% static 'js/maps/generic/map.js' %}" type="text/javascript"></script>
{% endblock extra_head %}

{% block title %}
Expand Down Expand Up @@ -112,25 +118,13 @@ <h3 class="panel-title">{{ facility.name }} | Facilities | BackOffice</h3>
<tr>
<th>Location</th>
<td>
Lat {{ facility.location.y }} Long {{ facility.location.x }}<br>
Lat {{ facility.location.y }} Long {{ facility.location.x }} Grid: <span id="gridLocator"></span><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 }}], {icon: {{ facility.facility_type.marker }}})
marker.bindPopup("<b>{{ facility.name }}</b><br><p>{{ facility.description|linebreaksbr }}</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>

<script type="text/javascript" src="{% static 'js/backoffice/facility_detail_backoffice.js' %}"></script>
{% endblock %}
4 changes: 4 additions & 0 deletions src/backoffice/templates/facility_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
{{ form.media }}
{% leaflet_css plugins="forms" %}
{% leaflet_js plugins="forms" %}
{{ mapData|json_script:"mapData" }}
<script src="{% static 'js/maps/generic/mapVars.js' %}" type="text/javascript"></script>
<script src="{% static 'js/maps/generic/map.js' %}" type="text/javascript"></script>
{% endblock extra_head %}

{% block content %}
Expand All @@ -25,4 +28,5 @@ <h3 class="panel-title">{% if request.resolver_match.url_name == "facility_updat
</form>
</div>
</div>
<script src="{% static 'js/maps/generic/init_loader.js' %}"></script>
{% endblock content %}
39 changes: 14 additions & 25 deletions src/backoffice/templates/facility_list_backoffice.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@

{% 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>
<link href="{% static 'css/leaflet.css' %}" rel="stylesheet">
<link href="{% static 'css/leaflet.awesome-markers.css' %}" rel="stylesheet">
<link href="{% static 'vendor/leaflet-fullscreen/leaflet.fullscreen.css' %}" rel="stylesheet">
<script src="{% static 'vendor/leaflet/leaflet-1.6.0.js' %}" type="text/javascript"></script>
<script src="{% static 'vendor/leaflet/proj4.js' %}" type="text/javascript"></script>
<script src="{% static 'vendor/leaflet/proj4leaflet.js' %}" type="text/javascript"></script>
<script src="{% static 'vendor/leaflet/leaflet.awesome-markers.min.js' %}" type="text/javascript"></script>
<script src="{% static 'vendor/leaflet/leaflet-color-markers.js' %}" type="text/javascript"></script>
<script src="{% static 'vendor/leaflet-fullscreen/Leaflet.fullscreen.min.js' %}" type="text/javascript"></script>
{{ mapData|json_script:"mapData" }}
<script src="{% static 'js/maps/generic/mapVars.js' %}" type="text/javascript"></script>
<script src="{% static 'js/maps/generic/map.js' %}" type="text/javascript"></script>
{% endblock extra_head %}

{% block content %}
Expand Down Expand Up @@ -41,7 +49,7 @@ <h3 class="panel-title">Facilities - BackOffice</h3>
{% 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><i class="{{ facility.facility_type.icon }} fa-2x fa-fw"></i> {{ facility.facility_type.name }}</td>
<td>{{ facility.team.name }} Team</td>
<td>{{ facility.description|linebreaksbr|default:"N/A" }}</td>
<td>{{ facility.location }}</td>
Expand All @@ -61,25 +69,6 @@ <h3 class="panel-title">Facilities - BackOffice</h3>

</div>
</div>

<script type="text/javascript">
function MapReadyCallback() {
// loop over facilities and add a marker for each,
// include a popup in the marker
var markers = new Array();
{% for facility in facility_list %}
{% url "backoffice:facility_detail" camp_slug=facility.camp.slug facility_uuid=facility.uuid as detail %}
marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}], {icon: {{ facility.facility_type.marker }}}).bindPopup("<b>{{ facility.name }}</b><br><p>{{ facility.description|linebreaksbr }}</p><p>Responsible team: {{ facility.facility_type.responsible_team.name }} Team</p>{% if request.user.is_authenticated %}<p><a href='{{ detail }}' class='btn btn-primary' style='color: white;'><i class='fas fa-search'></i> Details</a></p>{% endif %}").addTo(this);
markers.push(marker.getLatLng());
{% endfor %}
var markerBounds = L.latLngBounds(markers);
// fitBounds appears to not respect maxZoom from the tilelayer,
// leading to the "Error: Attempted to load an infinite number of tiles" mess,
// so hardcode maxZoom to 13 here
this.fitBounds(markerBounds, {maxZoom: 13});
};
</script>

<script src="{% static 'js/kfmap.js' %}" type="text/javascript"></script>
<script type="text/javascript" src="{% static 'js/backoffice/facility_list_backoffice.js' %}"></script>

{% endblock %}
3 changes: 3 additions & 0 deletions src/backoffice/templates/facility_type_form.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block extra_head %}
{{ form.media }}
{% endblock extra_head %}

{% block content %}
<div class="panel panel-default">
Expand Down
50 changes: 50 additions & 0 deletions src/backoffice/templates/facility_type_import_backoffice.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{% extends 'base.html' %}
{% load leaflet_tags %}
{% load bootstrap3 %}
{% load static %}

{% block title %}
Import | Facility Types | BackOffice | {{ block.super }}
{% endblock %}

{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Import Facility Types - BackOffice</h3>
</div>
<div class="panel-body">
<p>
Only the properties <b>name</b> and <b>description</b> will be imported!
</p>
<div class="mb-3">
<label for="selectFiles" class="form-label">Import from file</label>
<input type="file" class="form-control" id="selectFiles" value="Import" />
</div>
<div class="mb-3">
<div class="mb-1">
<label for="importURL" class="form-label">Import from URL</label>
<input id="importURL" class="form-control" />
</div>
<div class="mb-1">
<button class="btn btn-success" id="importURLFetch">Fetch</button>
</div>
</div>
<br/><br/>
<form class="mb-3" method="post">
{% csrf_token %}
<label class="form-label" for="geojson_data">GeoJSON:</label>
<textarea class="form-control" id="geojson_data" name="geojson_data"></textarea>
{% if error %}
<div class="invalid-feedback">
<p style="color: red;">{{ error }}</p>
</div>
{% endif %}
{% buttons %}
<button type="submit" class="btn btn-success">Import</button>
<a href="{% url 'backoffice:facility_type_list' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Cancel</a>
{% endbuttons %}
</form>
</div>
</div>
<script src="{% static 'js/backoffice/maps_layer_import_backoffice.js' %}"></script>
{% endblock %}
2 changes: 2 additions & 0 deletions src/backoffice/templates/facility_type_list_backoffice.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ <h3 class="panel-title">Facility Types - BackOffice</h3>
<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_import" camp_slug=camp.slug slug=ft.slug %}" class="btn btn-success"><i class="fas fa-file-import"></i> Import</a>
<a href="{% url "facilities:facility_list_geojson" camp_slug=camp.slug facility_type_slug=ft.slug %}" class="btn btn-info" target="_blank"><i class="fas fa-file-export"></i> Export</a>
<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>
Expand Down
2 changes: 1 addition & 1 deletion src/backoffice/templates/facilityfeedback_backoffice.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends 'program_base.html' %}
{% extends 'base.html' %}
{% load bootstrap3 %}
{% load bornhack %}

Expand Down
17 changes: 17 additions & 0 deletions src/backoffice/templates/includes/permissions_explainer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<p>The available permissions are:</p>
<p>
<dl>
<dt>lead</dt>
<dd>The team lead permission enables a user to manage team memberships and permissions. This permission is granted to and removed from team leads automatically when the team membership is updated. Codename <b>camps.{{ team.slug }}_team_lead</b>.</dd>
<dt>member</dt>
<dd>The team member permission grants access to the team area with tasks and the team guide. It also grants access to handle facility feedback in backoffice. Some sections of backoffice and the main site are restricted to users with this permission. Codename <b>camps.{{ team.slug }}_team_member</b>.</dd>
<dt>mapper</dt>
<dd>The team mapper permission grants access to gis/map layer management in backoffice. Codename <b>camps.{{ team.slug }}_team_mapper</b>.</dd>
<dt>facilitator</dt>
<dd>The team facilitator permission grants access to manage facilities in backoffice. Codename <b>camps.{{ team.slug }}_team_facilitator</b>.</dd>
<dt>infopager</dt>
<dd>The team infopager permission grants access to manage infopage sections for the team. Codename <b>camps.{{ team.slug }}_team_infopager</b>.</dd>
<dt>pos</dt>
<dd>The team pos permission grants access to submit point-of-sale reports on behalf of the team. Codename <b>camps.{{ team.slug }}_team_pos</b>.</dd>
</p>
<p><a href="{% url 'backoffice:index' camp_slug=camp.slug %}" class="btn btn-default"><i class="fas fa-undo"></i> Backoffice</a></p>
Loading

0 comments on commit 3745502

Please sign in to comment.