Facilities (#458)
* update font-awesome to 5.12.1 * prefetch members to considerably lower number of SQL queries for team list view * add facilities app with facility feeedback functionality, working on #383 * Add GeoDjango (django.contrib.gis) and switch to PostGIS db backend. Add location field for Facility model. Add django-leaflet to requirements. * better migration names * tweaking travis config, we use py3.7 now, and add postgis * Add qr code support for facilities (visible in the admin). Make facitilies browsable without logging in. Feedback can be submitted without logging in, given the facility UUID, which is not revealed to unauthenticated users. * show quickfeedback icons when creating and when reading feedback * only show anon option if user is logged in * django-reversion somehow went missing from requirements
This commit is contained in:
parent
df1a6564b7
commit
12a1c9a0ce
2
.flake8
2
.flake8
|
@ -1,4 +1,4 @@
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 88
|
max-line-length = 88
|
||||||
ignore = E501 W503
|
ignore = E501 W503 E231
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
|
dist: bionic
|
||||||
|
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "3.6"
|
- "3.7"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- postgresql
|
- postgresql
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
postgresql: "9.6"
|
postgresql: "9.6"
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- postgresql-9.6-postgis-2.5
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install -r src/requirements/dev.txt
|
- pip install -r src/requirements/dev.txt
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- psql -c "CREATE ROLE bornhack WITH CREATEDB LOGIN PASSWORD 'bornhack';"
|
- psql -c "CREATE ROLE bornhack WITH SUPERUSER LOGIN PASSWORD 'bornhack';"
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- cp src/bornhack/environment_settings.py.dist.dev src/bornhack/environment_settings.py
|
- cp src/bornhack/environment_settings.py.dist.dev src/bornhack/environment_settings.py
|
||||||
|
|
|
@ -57,9 +57,7 @@ Install pip packages:
|
||||||
|
|
||||||
### Postgres
|
### Postgres
|
||||||
|
|
||||||
You need to have a running Postgres instance (we use Postgres-specific datetime range fields). Install Postgress, and add a database `bornhack` (or whichever you like) with some way for the application to connect to it, for instance adding a user with a password.
|
You need to have a running Postgres instance (we use Postgres-specific fields and PostGIS/GeoDjango). Install Postgres and PostGIS, and add a database `bornhack` (or whichever you like) with some way for the application to connect to it, for instance adding a user with a password. Connect to the database as a superuser and run `create extension postgis`.
|
||||||
|
|
||||||
You can also use Unix socket connections if you know how to. It's faster, easier and perhaps more secure.
|
|
||||||
|
|
||||||
### Configuration file
|
### Configuration file
|
||||||
|
|
||||||
|
|
68
src/backoffice/templates/facilityfeedback_backoffice.html
Normal file
68
src/backoffice/templates/facilityfeedback_backoffice.html
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load bornhack %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Facility Feedback for {{ team.name }} Team | {{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Facility Feedback for {{ team.name }} Team</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% if feedback_list %}
|
||||||
|
<form method="post">
|
||||||
|
{{ formset.management_form }}
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for form, feedback in formset|zip:feedback_list %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">Feedback for {{ feedback.facility.name }} by {{ feedback.user.username|default:"Anonymous User" }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<td>{{ feedback.user }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Created</th>
|
||||||
|
<td>{{ feedback.created }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Facility</th>
|
||||||
|
<td>{{ feedback.facility }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Quick Feedback</th>
|
||||||
|
<td><i class="{{ feedback.quick_feedback.icon }} fa-2x"></i> {{ feedback.quick_feedback }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Comment</th>
|
||||||
|
<td>{{ feedback.comment|default:"N/A" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Urgent</th>
|
||||||
|
<td>{{ feedback.urgent|yesno }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Handled</th>
|
||||||
|
<td>{% bootstrap_form form %}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Submit</button>
|
||||||
|
<a href="{% url 'backoffice:index' camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -14,6 +14,29 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
|
{% 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
|
||||||
|
</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 %}
|
{% if perms.camps.infoteam_permission %}
|
||||||
<h3>Info Team</h3>
|
<h3>Info Team</h3>
|
||||||
<a href="{% url 'backoffice:scan_tickets' camp_slug=camp.slug %}"
|
<a href="{% url 'backoffice:scan_tickets' camp_slug=camp.slug %}"
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .views import (
|
||||||
EventProposalManageView,
|
EventProposalManageView,
|
||||||
ExpenseDetailView,
|
ExpenseDetailView,
|
||||||
ExpenseListView,
|
ExpenseListView,
|
||||||
|
FacilityFeedbackView,
|
||||||
ManageProposalsView,
|
ManageProposalsView,
|
||||||
MerchandiseOrdersView,
|
MerchandiseOrdersView,
|
||||||
MerchandiseToOrderView,
|
MerchandiseToOrderView,
|
||||||
|
@ -35,6 +36,10 @@ app_name = "backoffice"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", BackofficeIndexView.as_view(), name="index"),
|
path("", BackofficeIndexView.as_view(), name="index"),
|
||||||
|
path(
|
||||||
|
"feedback/facilities/<slug:team_slug>/",
|
||||||
|
include([path("", FacilityFeedbackView.as_view(), name="facilityfeedback")]),
|
||||||
|
),
|
||||||
# infodesk
|
# infodesk
|
||||||
path(
|
path(
|
||||||
"infodesk/",
|
"infodesk/",
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
from django.forms import modelformset_factory
|
from django.forms import modelformset_factory
|
||||||
|
@ -16,6 +17,7 @@ from django.utils import timezone
|
||||||
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 profiles.models import Profile
|
from profiles.models import Profile
|
||||||
from program.models import EventFeedback, EventProposal, SpeakerProposal
|
from program.models import EventFeedback, EventProposal, SpeakerProposal
|
||||||
from shop.models import Order, OrderProductRelation
|
from shop.models import Order, OrderProductRelation
|
||||||
|
@ -41,6 +43,73 @@ class BackofficeIndexView(CampViewMixin, RaisePermissionRequiredMixin, TemplateV
|
||||||
permission_required = "camps.backoffice_permission"
|
permission_required = "camps.backoffice_permission"
|
||||||
template_name = "index.html"
|
template_name = "index.html"
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context["facilityfeedback_teams"] = Team.objects.filter(
|
||||||
|
id__in=set(
|
||||||
|
FacilityFeedback.objects.filter(
|
||||||
|
facility__facility_type__responsible_team__camp=self.camp,
|
||||||
|
handled=False,
|
||||||
|
).values_list(
|
||||||
|
"facility__facility_type__responsible_team__id", flat=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
|
|
@ -7,7 +7,7 @@ ALLOWED_HOSTS = {{ django_allowed_hostnames }}
|
||||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
'ENGINE': 'django.contrib.gis.db.backends.postgis',
|
||||||
'NAME': '{{ django_postgres_dbname }}',
|
'NAME': '{{ django_postgres_dbname }}',
|
||||||
'USER': '{{ django_postgres_user }}',
|
'USER': '{{ django_postgres_user }}',
|
||||||
'PASSWORD': '{{ django_postgres_password }}',
|
'PASSWORD': '{{ django_postgres_password }}',
|
||||||
|
|
|
@ -14,7 +14,7 @@ ALLOWED_HOSTS = "*"
|
||||||
# Database settings - modify to match your database configuration!
|
# Database settings - modify to match your database configuration!
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
"ENGINE": 'django.contrib.gis.db.backends.postgis',
|
||||||
"NAME": "bornhack",
|
"NAME": "bornhack",
|
||||||
"USER": "bornhack",
|
"USER": "bornhack",
|
||||||
# Comment back in if you are connecting via TCP
|
# Comment back in if you are connecting via TCP
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .environment_settings import *
|
from .environment_settings import * # noqa: F403
|
||||||
|
|
||||||
|
|
||||||
def local_dir(entry):
|
def local_dir(entry):
|
||||||
|
@ -27,6 +27,7 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"django.contrib.sites",
|
"django.contrib.sites",
|
||||||
|
"django.contrib.gis",
|
||||||
"graphene_django",
|
"graphene_django",
|
||||||
"channels",
|
"channels",
|
||||||
"corsheaders",
|
"corsheaders",
|
||||||
|
@ -51,6 +52,7 @@ INSTALLED_APPS = [
|
||||||
"feedback",
|
"feedback",
|
||||||
"economy",
|
"economy",
|
||||||
"wishlist",
|
"wishlist",
|
||||||
|
"facilities",
|
||||||
"allauth",
|
"allauth",
|
||||||
"allauth.account",
|
"allauth.account",
|
||||||
"allauth_2fa",
|
"allauth_2fa",
|
||||||
|
@ -60,6 +62,7 @@ INSTALLED_APPS = [
|
||||||
"bootstrap3",
|
"bootstrap3",
|
||||||
"django_extensions",
|
"django_extensions",
|
||||||
"reversion",
|
"reversion",
|
||||||
|
"leaflet",
|
||||||
]
|
]
|
||||||
|
|
||||||
# MEDIA_URL = '/media/'
|
# MEDIA_URL = '/media/'
|
||||||
|
@ -129,7 +132,7 @@ MIDDLEWARE = [
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
CORS_URLS_REGEX = r"^/api/*$"
|
CORS_URLS_REGEX = r"^/api/*$"
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG: # noqa: F405
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
INSTALLED_APPS += ["debug_toolbar"]
|
INSTALLED_APPS += ["debug_toolbar"]
|
||||||
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE
|
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
from allauth.account.views import LoginView, LogoutView
|
from allauth.account.views import LoginView, LogoutView
|
||||||
|
from bar.views import MenuView
|
||||||
|
from camps.views import CampDetailView, CampListView, CampRedirectView
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include
|
from django.conf.urls import include
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
@ -6,11 +8,8 @@ from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from graphene_django.views import GraphQLView
|
|
||||||
|
|
||||||
from bar.views import MenuView
|
|
||||||
from camps.views import CampDetailView, CampListView, CampRedirectView
|
|
||||||
from feedback.views import FeedbackCreate
|
from feedback.views import FeedbackCreate
|
||||||
|
from graphene_django.views import GraphQLView
|
||||||
from info.views import CampInfoView
|
from info.views import CampInfoView
|
||||||
from people.views import PeopleView
|
from people.views import PeopleView
|
||||||
from sponsors.views import SponsorsView
|
from sponsors.views import SponsorsView
|
||||||
|
@ -90,6 +89,12 @@ urlpatterns = [
|
||||||
kwargs={"page": "wishlist:list"},
|
kwargs={"page": "wishlist:list"},
|
||||||
name="wish_list_redirect",
|
name="wish_list_redirect",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"backoffice/",
|
||||||
|
CampRedirectView.as_view(),
|
||||||
|
kwargs={"page": "backoffice:index"},
|
||||||
|
name="backoffice_redirect",
|
||||||
|
),
|
||||||
path("people/", PeopleView.as_view(), name="people"),
|
path("people/", PeopleView.as_view(), name="people"),
|
||||||
# camp specific urls below here
|
# camp specific urls below here
|
||||||
path(
|
path(
|
||||||
|
@ -136,6 +141,7 @@ urlpatterns = [
|
||||||
path("feedback/", FeedbackCreate.as_view(), name="feedback"),
|
path("feedback/", FeedbackCreate.as_view(), name="feedback"),
|
||||||
path("economy/", include("economy.urls", namespace="economy")),
|
path("economy/", include("economy.urls", namespace="economy")),
|
||||||
path("wishlist/", include("wishlist.urls", namespace="wishlist")),
|
path("wishlist/", include("wishlist.urls", namespace="wishlist")),
|
||||||
|
path("facilities/", include("facilities.urls", namespace="facilities")),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
53
src/camps/migrations/0034_add_team_permission_sets.py
Normal file
53
src/camps/migrations/0034_add_team_permission_sets.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# Generated by Django 3.0.3 on 2020-02-19 19:08
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("camps", "0033_camp_show_schedule"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="permission",
|
||||||
|
options={
|
||||||
|
"default_permissions": (),
|
||||||
|
"managed": False,
|
||||||
|
"permissions": (
|
||||||
|
("backoffice_permission", "BackOffice access"),
|
||||||
|
("badgeteam_permission", "Badge Team permissions set"),
|
||||||
|
("barteam_permission", "Bar Team permissions set"),
|
||||||
|
("certteam_permission", "CERT Team permissions set"),
|
||||||
|
(
|
||||||
|
"constructionteam_permission",
|
||||||
|
"Construction Team permissions set",
|
||||||
|
),
|
||||||
|
("contentteam_permission", "Content Team permissions set"),
|
||||||
|
("economyteam_permission", "Economy Team permissions set"),
|
||||||
|
("foodareateam_permission", "Foodarea Team permissions set"),
|
||||||
|
("infoteam_permission", "Info Team permissions set"),
|
||||||
|
("lightteam_permission", "Light Team permissions set"),
|
||||||
|
("logisticsteam_permission", "Logistics Team permissions set"),
|
||||||
|
("metricsteam_permission", "Metrics Team permissions set"),
|
||||||
|
("nocteam_permission", "NOC Team permissions set"),
|
||||||
|
("orgateam_permission", "Orga Team permissions set"),
|
||||||
|
("pocteam_permission", "POC Team permissions set"),
|
||||||
|
("prteam_permission", "PR Team permissions set"),
|
||||||
|
("phototeam_permission", "Photo Team permissions set"),
|
||||||
|
("powerteam_permission", "Power Team permissions set"),
|
||||||
|
("rocteam_permission", "ROC Team permissions set"),
|
||||||
|
("sanitationteam_permission", "Sanitation Team permissions set"),
|
||||||
|
("shuttleteam_permission", "Shuttle Team permissions set"),
|
||||||
|
("sponsorsteam_permission", "Sponsors Team permissions set"),
|
||||||
|
("sysadminteam_permission", "Sysadmin Team permissions set"),
|
||||||
|
("videoteam_permission", "Video Team permissions set"),
|
||||||
|
("websiteteam_permission", "Website Team permissions set"),
|
||||||
|
("wellnessteam_permission", "Wellness Team permissions set"),
|
||||||
|
("expense_create_permission", "Expense Create permission"),
|
||||||
|
("revenue_create_permission", "Revenue Create permission"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,5 @@
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
|
|
||||||
from camps.models import Camp
|
from camps.models import Camp
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
|
||||||
class CampViewMixin:
|
class CampViewMixin:
|
||||||
|
|
|
@ -5,9 +5,8 @@ from django.contrib.postgres.fields import DateTimeRangeField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from psycopg2.extras import DateTimeTZRange
|
|
||||||
|
|
||||||
from program.models import EventLocation, EventType
|
from program.models import EventLocation, EventType
|
||||||
|
from psycopg2.extras import DateTimeTZRange
|
||||||
from utils.models import CreatedUpdatedModel, UUIDModel
|
from utils.models import CreatedUpdatedModel, UUIDModel
|
||||||
|
|
||||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
@ -23,10 +22,31 @@ class Permission(models.Model):
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (
|
permissions = (
|
||||||
("backoffice_permission", "BackOffice access"),
|
("backoffice_permission", "BackOffice access"),
|
||||||
("orgateam_permission", "Orga Team permissions set"),
|
("badgeteam_permission", "Badge Team permissions set"),
|
||||||
("infoteam_permission", "Info Team permissions set"),
|
("barteam_permission", "Bar Team permissions set"),
|
||||||
("economyteam_permission", "Economy Team permissions set"),
|
("certteam_permission", "CERT Team permissions set"),
|
||||||
|
("constructionteam_permission", "Construction Team permissions set"),
|
||||||
("contentteam_permission", "Content Team permissions set"),
|
("contentteam_permission", "Content Team permissions set"),
|
||||||
|
("economyteam_permission", "Economy Team permissions set"),
|
||||||
|
("foodareateam_permission", "Foodarea Team permissions set"),
|
||||||
|
("infoteam_permission", "Info Team permissions set"),
|
||||||
|
("lightteam_permission", "Light Team permissions set"),
|
||||||
|
("logisticsteam_permission", "Logistics Team permissions set"),
|
||||||
|
("metricsteam_permission", "Metrics Team permissions set"),
|
||||||
|
("nocteam_permission", "NOC Team permissions set"),
|
||||||
|
("orgateam_permission", "Orga Team permissions set"),
|
||||||
|
("pocteam_permission", "POC Team permissions set"),
|
||||||
|
("prteam_permission", "PR Team permissions set"),
|
||||||
|
("phototeam_permission", "Photo Team permissions set"),
|
||||||
|
("powerteam_permission", "Power Team permissions set"),
|
||||||
|
("rocteam_permission", "ROC Team permissions set"),
|
||||||
|
("sanitationteam_permission", "Sanitation Team permissions set"),
|
||||||
|
("shuttleteam_permission", "Shuttle Team permissions set"),
|
||||||
|
("sponsorsteam_permission", "Sponsors Team permissions set"),
|
||||||
|
("sysadminteam_permission", "Sysadmin Team permissions set"),
|
||||||
|
("videoteam_permission", "Video Team permissions set"),
|
||||||
|
("websiteteam_permission", "Website Team permissions set"),
|
||||||
|
("wellnessteam_permission", "Wellness Team permissions set"),
|
||||||
("expense_create_permission", "Expense Create permission"),
|
("expense_create_permission", "Expense Create permission"),
|
||||||
("revenue_create_permission", "Revenue Create permission"),
|
("revenue_create_permission", "Revenue Create permission"),
|
||||||
)
|
)
|
||||||
|
|
0
src/facilities/__init__.py
Normal file
0
src/facilities/__init__.py
Normal file
65
src/facilities/admin.py
Normal file
65
src/facilities/admin.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from leaflet.admin import LeafletGeoAdmin
|
||||||
|
|
||||||
|
from .models import Facility, FacilityFeedback, FacilityQuickFeedback, FacilityType
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(FacilityQuickFeedback)
|
||||||
|
class FacilityQuickFeedbackAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(FacilityType)
|
||||||
|
class FacilityTypeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "description", "responsible_team", "camp"]
|
||||||
|
list_filter = ["responsible_team__camp", "responsible_team"]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Facility)
|
||||||
|
class FacilityAdmin(LeafletGeoAdmin):
|
||||||
|
list_display = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"facility_type",
|
||||||
|
"camp",
|
||||||
|
"team",
|
||||||
|
"location",
|
||||||
|
"feedback_url",
|
||||||
|
"feedback_qrcode",
|
||||||
|
]
|
||||||
|
list_filter = [
|
||||||
|
"facility_type__responsible_team__camp",
|
||||||
|
"facility_type",
|
||||||
|
"facility_type__responsible_team",
|
||||||
|
]
|
||||||
|
|
||||||
|
def feedback_qrcode(self, obj):
|
||||||
|
return format_html("<img src='{}'>", obj.get_feedback_qr(self.request))
|
||||||
|
|
||||||
|
def feedback_url(self, obj):
|
||||||
|
return obj.get_feedback_url(self.request)
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
self.request = request
|
||||||
|
return super().get_queryset(request)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(FacilityFeedback)
|
||||||
|
class FacilityFeedbackAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
"user",
|
||||||
|
"facility",
|
||||||
|
"quick_feedback",
|
||||||
|
"comment",
|
||||||
|
"urgent",
|
||||||
|
"handled",
|
||||||
|
]
|
||||||
|
list_filter = [
|
||||||
|
"facility__facility_type__responsible_team__camp",
|
||||||
|
"urgent",
|
||||||
|
"handled",
|
||||||
|
"facility__facility_type",
|
||||||
|
"facility__facility_type__responsible_team",
|
||||||
|
"facility",
|
||||||
|
]
|
5
src/facilities/apps.py
Normal file
5
src/facilities/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FacilitiesConfig(AppConfig):
|
||||||
|
name = "facilities"
|
216
src/facilities/migrations/0001_initial.py
Normal file
216
src/facilities/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
# Generated by Django 3.0.3 on 2020-02-19 19:18
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("teams", "0052_team_permission_set"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Facility",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
help_text="Name or description of this facility", max_length=100
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
models.TextField(help_text="Description of this facility"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"abstract": False,},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FacilityQuickFeedback",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("feedback", models.CharField(max_length=100)),
|
||||||
|
(
|
||||||
|
"icon",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
default="fas fa-exclamation",
|
||||||
|
help_text="Name of the fontawesome icon to use, including the 'fab fa-' or 'fas fa-' part. Defaults to an exclamation mark icon.",
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FacilityType",
|
||||||
|
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)),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
help_text="The name of this facility type", max_length=100
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"slug",
|
||||||
|
models.SlugField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The url slug for this facility type. Leave blank to autogenerate one.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"description",
|
||||||
|
models.TextField(help_text="Description of this facility type"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"icon",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
default="fas fa-list",
|
||||||
|
help_text="Name of the fontawesome icon to use, including the 'fab fa-' or 'fas fa-' part.",
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"quickfeedback_options",
|
||||||
|
models.ManyToManyField(
|
||||||
|
help_text="Pick the quick feedback options the user should be presented with when submitting Feedback for a Facility of this type. Pick at least the 'N/A' option if none of the other applies.",
|
||||||
|
to="facilities.FacilityQuickFeedback",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"responsible_team",
|
||||||
|
models.ForeignKey(
|
||||||
|
help_text="The Team responsible for this type of facility. This team will get the notification when we get a new FacilityFeedback for a Facility of this type.",
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="teams.Team",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"unique_together": {("slug", "responsible_team")},},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FacilityFeedback",
|
||||||
|
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)),
|
||||||
|
(
|
||||||
|
"comment",
|
||||||
|
models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Any comments or feedback about this facility? (optional)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"urgent",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Check if this is an urgent issue. Will trigger immediate notifications to the responsible team.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"handled",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="True if this feedback has been handled by the responsible team, False if not",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"facility",
|
||||||
|
models.ForeignKey(
|
||||||
|
help_text="The Facility this feeback is about",
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="feedbacks",
|
||||||
|
to="facilities.Facility",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"handled_by",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="The User who handled this feedback",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="facility_feebacks_handled",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"quick_feedback",
|
||||||
|
models.ForeignKey(
|
||||||
|
help_text="Quick feedback options. Elaborate in comment field as needed.",
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="feedbacks",
|
||||||
|
to="facilities.FacilityQuickFeedback",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="The User this feedback came from, empty if the user submits anonymously",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="facility_feebacks",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"abstract": False,},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="facility",
|
||||||
|
name="facility_type",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="facilities",
|
||||||
|
to="facilities.FacilityType",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
21
src/facilities/migrations/0002_facility_location.py
Normal file
21
src/facilities/migrations/0002_facility_location.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 3.0.3 on 2020-02-20 08:59
|
||||||
|
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("facilities", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="facility",
|
||||||
|
name="location",
|
||||||
|
field=django.contrib.gis.db.models.fields.PointField(
|
||||||
|
help_text="The location of this facility", null=True, srid=4326
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
21
src/facilities/migrations/0003_location_not_null.py
Normal file
21
src/facilities/migrations/0003_location_not_null.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 3.0.3 on 2020-02-20 08:59
|
||||||
|
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("facilities", "0002_facility_location"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="facility",
|
||||||
|
name="location",
|
||||||
|
field=django.contrib.gis.db.models.fields.PointField(
|
||||||
|
help_text="The location of this facility", srid=4326
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
0
src/facilities/migrations/__init__.py
Normal file
0
src/facilities/migrations/__init__.py
Normal file
42
src/facilities/mixins.py
Normal file
42
src/facilities/mixins.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
from camps.mixins import CampViewMixin
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
from .models import Facility, FacilityType
|
||||||
|
|
||||||
|
|
||||||
|
class FacilityTypeViewMixin(CampViewMixin):
|
||||||
|
"""
|
||||||
|
A mixin to get the FacilityType object based on facility_type_slug in url kwargs
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup(self, *args, **kwargs):
|
||||||
|
super().setup(*args, **kwargs)
|
||||||
|
self.facility_type = get_object_or_404(
|
||||||
|
FacilityType,
|
||||||
|
responsible_team__camp=self.camp,
|
||||||
|
slug=self.kwargs["facility_type_slug"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context["facilitytype"] = self.facility_type
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class FacilityViewMixin(FacilityTypeViewMixin):
|
||||||
|
"""
|
||||||
|
A mixin to get the Facility object based on facility_uuid in url kwargs
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup(self, *args, **kwargs):
|
||||||
|
super().setup(*args, **kwargs)
|
||||||
|
self.facility = get_object_or_404(
|
||||||
|
Facility,
|
||||||
|
facility_type=self.facility_type,
|
||||||
|
uuid=self.kwargs["facility_uuid"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context["facility"] = self.facility
|
||||||
|
return context
|
201
src/facilities/models.py
Normal file
201
src/facilities/models.py
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import qrcode
|
||||||
|
from django.contrib.gis.db.models import PointField
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
from django.utils.text import slugify
|
||||||
|
from utils.models import CampRelatedModel, UUIDModel
|
||||||
|
|
||||||
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FacilityQuickFeedback(models.Model):
|
||||||
|
"""
|
||||||
|
This model contains the various options for giving quick feedback which we present to the user
|
||||||
|
when giving feedback on facilities. Think "Needs cleaning" or "Doesn't work" and such.
|
||||||
|
This model is not Camp specific.
|
||||||
|
"""
|
||||||
|
|
||||||
|
feedback = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
icon = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
default="fas fa-exclamation",
|
||||||
|
blank=True,
|
||||||
|
help_text="Name of the fontawesome icon to use, including the 'fab fa-' or 'fas fa-' part. Defaults to an exclamation mark icon.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.feedback
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [("slug", "responsible_team")]
|
||||||
|
|
||||||
|
name = models.CharField(max_length=100, help_text="The name of this facility type")
|
||||||
|
|
||||||
|
slug = models.SlugField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The url slug for this facility type. Leave blank to autogenerate one.",
|
||||||
|
)
|
||||||
|
|
||||||
|
description = models.TextField(help_text="Description of this facility type")
|
||||||
|
|
||||||
|
icon = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
default="fas fa-list",
|
||||||
|
blank=True,
|
||||||
|
help_text="Name of the fontawesome icon to use, including the 'fab fa-' or 'fas fa-' part.",
|
||||||
|
)
|
||||||
|
|
||||||
|
responsible_team = models.ForeignKey(
|
||||||
|
"teams.Team",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
help_text="The Team responsible for this type of facility. This team will get the notification when we get a new FacilityFeedback for a Facility of this type.",
|
||||||
|
)
|
||||||
|
|
||||||
|
quickfeedback_options = models.ManyToManyField(
|
||||||
|
to="facilities.FacilityQuickFeedback",
|
||||||
|
help_text="Pick the quick feedback options the user should be presented with when submitting Feedback for a Facility of this type. Pick at least the 'N/A' option if none of the other applies.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def camp(self):
|
||||||
|
return self.responsible_team.camp
|
||||||
|
|
||||||
|
camp_filter = "responsible_team__camp"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.camp})"
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify(self.name)
|
||||||
|
if not self.slug:
|
||||||
|
raise ValidationError("Unable to slugify")
|
||||||
|
super().save(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Facility(CampRelatedModel, UUIDModel):
|
||||||
|
"""
|
||||||
|
Facilities are toilets, thrashcans, cooking and dishwashing areas, and any other part of the event which could need attention or maintenance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
facility_type = models.ForeignKey(
|
||||||
|
"facilities.FacilityType", related_name="facilities", on_delete=models.PROTECT
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100, help_text="Name or description of this facility",
|
||||||
|
)
|
||||||
|
|
||||||
|
description = models.TextField(help_text="Description of this facility")
|
||||||
|
|
||||||
|
location = PointField(help_text="The location of this facility")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def team(self):
|
||||||
|
return self.facility_type.responsible_team
|
||||||
|
|
||||||
|
@property
|
||||||
|
def camp(self):
|
||||||
|
return self.facility_type.camp
|
||||||
|
|
||||||
|
camp_filter = "facility_type__responsible_team__camp"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_feedback_url(self, request):
|
||||||
|
return request.build_absolute_uri(
|
||||||
|
reverse(
|
||||||
|
"facilities:facility_feedback",
|
||||||
|
kwargs={
|
||||||
|
"camp_slug": self.facility_type.responsible_team.camp.slug,
|
||||||
|
"facility_type_slug": self.facility_type.slug,
|
||||||
|
"facility_uuid": self.uuid,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_feedback_qr(self, request):
|
||||||
|
qr = qrcode.make(
|
||||||
|
self.get_feedback_url(request),
|
||||||
|
version=1,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_H,
|
||||||
|
).resize((250, 250))
|
||||||
|
file_like = io.BytesIO()
|
||||||
|
qr.save(file_like, format="png")
|
||||||
|
qrcode_base64 = base64.b64encode(file_like.getvalue()).decode("utf-8")
|
||||||
|
return f"data:image/png;base64,{qrcode_base64}"
|
||||||
|
|
||||||
|
|
||||||
|
class FacilityFeedback(CampRelatedModel):
|
||||||
|
"""
|
||||||
|
This model contains participant feedback for Facilities.
|
||||||
|
It is linked to the user and the facility, and to the
|
||||||
|
quick_feedback choice the user picked (if any).
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"auth.User",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="facility_feebacks",
|
||||||
|
help_text="The User this feedback came from, empty if the user submits anonymously",
|
||||||
|
)
|
||||||
|
|
||||||
|
facility = models.ForeignKey(
|
||||||
|
"facilities.Facility",
|
||||||
|
related_name="feedbacks",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
help_text="The Facility this feeback is about",
|
||||||
|
)
|
||||||
|
|
||||||
|
quick_feedback = models.ForeignKey(
|
||||||
|
"facilities.FacilityQuickFeedback",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="feedbacks",
|
||||||
|
help_text="Quick feedback options. Elaborate in comment field as needed.",
|
||||||
|
)
|
||||||
|
|
||||||
|
comment = models.TextField(
|
||||||
|
blank=True, help_text="Any comments or feedback about this facility? (optional)"
|
||||||
|
)
|
||||||
|
|
||||||
|
urgent = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Check if this is an urgent issue. Will trigger immediate notifications to the responsible team.",
|
||||||
|
)
|
||||||
|
|
||||||
|
handled = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="True if this feedback has been handled by the responsible team, False if not",
|
||||||
|
)
|
||||||
|
|
||||||
|
handled_by = models.ForeignKey(
|
||||||
|
"auth.User",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="facility_feebacks_handled",
|
||||||
|
help_text="The User who handled this feedback",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def camp(self):
|
||||||
|
return self.facility.camp
|
||||||
|
|
||||||
|
camp_filter = "facility__facility_type__responsible_team__camp"
|
41
src/facilities/templates/facility_detail.html
Normal file
41
src/facilities/templates/facility_detail.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
{% load leaflet_tags %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
{% leaflet_js %}
|
||||||
|
{% leaflet_css %}
|
||||||
|
{% endblock extra_head %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{{ facility.name }} - Facilities | {{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">{{ facility.facility_type.name }}: {{ facility.name }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p class="lead">{{ facility.description }}</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>{% leaflet_map "facility_detail" %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// add a listener to add the marker for the facility on the leaflet map after it inits
|
||||||
|
window.addEventListener("map:init", function (e) {
|
||||||
|
var detail = e.detail;
|
||||||
|
{% url "facilities:facility_feedback" camp_slug=facility.camp.slug facility_type_slug=facility.facility_type.slug facility_uuid=facility.uuid as feedback %}
|
||||||
|
marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}]);
|
||||||
|
marker.addTo(detail.map);
|
||||||
|
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 %}");
|
||||||
|
detail.map.setView([{{ facility_list.0.location.y }}, {{ facility_list.0.location.x }}], 15);
|
||||||
|
}, false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
27
src/facilities/templates/facility_feedback.html
Normal file
27
src/facilities/templates/facility_feedback.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load bornhack %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{{ facility.name }} Feedback</h2>
|
||||||
|
{% if unhandled_feedbacks %}
|
||||||
|
<div class="alert alert-info" role="alert">Note: We already have <b>{{ unhandled_feedbacks }}</b> unhandled feedback submissions for facility <b>{{ facility.name }}</b>. This means your issue could have been reported already, but you are welcome to submit your feedback anyway!</div>
|
||||||
|
{% endif %}
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
{% if field.name == "quick_feedback" %}
|
||||||
|
<div class="list-group">
|
||||||
|
{% for radio, option in field|zip:facility.facility_type.quickfeedback_options.all %}
|
||||||
|
<div class="list-group-item">{{ radio }} <i class="{{ option.icon }} fa-2x"></i></div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% bootstrap_field field %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-success"><i class="fas fa-check"></i> Submit Feedback</button>
|
||||||
|
<a href="{% url 'facilities:facility_detail' camp_slug=camp.slug facility_type_slug=facilitytype.slug facility_uuid=facility.uuid %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
|
|
58
src/facilities/templates/facility_list.html
Normal file
58
src/facilities/templates/facility_list.html
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
{% load leaflet_tags %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
{% leaflet_js %}
|
||||||
|
{% leaflet_css %}
|
||||||
|
{% endblock extra_head %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Facilities of type {{ facilitytype }} | {{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Facilities of type {{ facilitytype }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="list-group">
|
||||||
|
{% for facility in facility_list %}
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<a href="{% url 'facilities:facility_detail' camp_slug=camp.slug facility_type_slug=facility.facility_type.slug facility_uuid=facility.uuid %}" class="list-group-item">
|
||||||
|
{% else %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
{% endif %}
|
||||||
|
<h4 class="list-group-item-heading">
|
||||||
|
<i class="{{ facility.facility_type.icon }} fa-2x fa-pull-left fa-fw"></i> {{ facility.name }}
|
||||||
|
</h4>
|
||||||
|
<p class="list-group-item-text">{{ facility.description }}</p>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<p>{% leaflet_map "facility_list" %}</p>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// add a listener to add the marker for each facility on the leaflet map after it inits
|
||||||
|
window.addEventListener("map:init", function (e) {
|
||||||
|
var detail = e.detail;
|
||||||
|
{% for facility in facility_list %}
|
||||||
|
{% url "facilities:facility_detail" camp_slug=facility.camp.slug facility_type_slug=facility.facility_type.slug facility_uuid=facility.uuid as detail %}
|
||||||
|
{% url "facilities:facility_feedback" camp_slug=facility.camp.slug facility_type_slug=facility.facility_type.slug facility_uuid=facility.uuid as feedback %}
|
||||||
|
marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}]);
|
||||||
|
marker.addTo(detail.map);
|
||||||
|
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='{{ detail }}' class='btn btn-primary' style='color: white;'><i class='fas fa-search'></i> Details</a><a href='{{ feedback }}' class='btn btn-primary' style='color: white;'><i class='fas fa-comment-dots'></i> Feedback</a></p>{% endif %}");
|
||||||
|
{% endfor %}
|
||||||
|
detail.map.setView([{{ facility_list.0.location.y }}, {{ facility_list.0.location.x }}], 15);
|
||||||
|
}, false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
26
src/facilities/templates/facility_type_list.html
Normal file
26
src/facilities/templates/facility_type_list.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{% extends 'program_base.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Facility Types | {{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Facility Types</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="list-group">
|
||||||
|
{% for ft in facilitytype_list %}
|
||||||
|
<a href="{% url 'facilities:facility_list' camp_slug=camp.slug facility_type_slug=ft.slug %}" class="list-group-item">
|
||||||
|
<h4 class="list-group-item-heading">
|
||||||
|
<i class="{{ ft.icon }} fa-2x fa-pull-left fa-fw"></i> {{ ft.name }}
|
||||||
|
</h4>
|
||||||
|
<p class="list-group-item-text">{{ ft.description }}</p>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
36
src/facilities/urls.py
Normal file
36
src/facilities/urls.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from .views import (
|
||||||
|
FacilityDetailView,
|
||||||
|
FacilityFeedbackView,
|
||||||
|
FacilityListView,
|
||||||
|
FacilityTypeListView,
|
||||||
|
)
|
||||||
|
|
||||||
|
app_name = "facilities"
|
||||||
|
urlpatterns = [
|
||||||
|
path("", FacilityTypeListView.as_view(), name="facility_type_list"),
|
||||||
|
path(
|
||||||
|
"<slug:facility_type_slug>/",
|
||||||
|
include(
|
||||||
|
[
|
||||||
|
path("", FacilityListView.as_view(), name="facility_list"),
|
||||||
|
path(
|
||||||
|
"<uuid:facility_uuid>/",
|
||||||
|
include(
|
||||||
|
[
|
||||||
|
path(
|
||||||
|
"", FacilityDetailView.as_view(), name="facility_detail"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"feedback/",
|
||||||
|
FacilityFeedbackView.as_view(),
|
||||||
|
name="facility_feedback",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
86
src/facilities/views.py
Normal file
86
src/facilities/views.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
from camps.mixins import CampViewMixin
|
||||||
|
from django import forms
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.views.generic import DetailView, ListView
|
||||||
|
from django.views.generic.edit import CreateView
|
||||||
|
|
||||||
|
from .mixins import FacilityTypeViewMixin, FacilityViewMixin
|
||||||
|
from .models import Facility, FacilityFeedback, FacilityType
|
||||||
|
|
||||||
|
|
||||||
|
class FacilityTypeListView(CampViewMixin, ListView):
|
||||||
|
model = FacilityType
|
||||||
|
template_name = "facility_type_list.html"
|
||||||
|
|
||||||
|
|
||||||
|
class FacilityListView(FacilityTypeViewMixin, ListView):
|
||||||
|
model = Facility
|
||||||
|
template_name = "facility_list.html"
|
||||||
|
|
||||||
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
qs = super().get_queryset(*args, **kwargs)
|
||||||
|
return qs.filter(facility_type=self.facility_type)
|
||||||
|
|
||||||
|
|
||||||
|
class FacilityDetailView(FacilityTypeViewMixin, DetailView):
|
||||||
|
model = Facility
|
||||||
|
template_name = "facility_detail.html"
|
||||||
|
pk_url_kwarg = "facility_uuid"
|
||||||
|
|
||||||
|
|
||||||
|
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 anon option to the form
|
||||||
|
"""
|
||||||
|
form = super().get_form(form_class)
|
||||||
|
|
||||||
|
form.fields["quick_feedback"] = forms.ModelChoiceField(
|
||||||
|
queryset=self.facility_type.quickfeedback_options.all(),
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
empty_label=None,
|
||||||
|
help_text=form.fields["quick_feedback"].help_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
form.fields["anonymous"] = forms.BooleanField(
|
||||||
|
label="Anonymous",
|
||||||
|
required=False,
|
||||||
|
help_text="Check if you prefer to submit this feedback without associating it with your bornhack.dk username",
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
context["unhandled_feedbacks"] = FacilityFeedback.objects.filter(
|
||||||
|
facility=self.facility, handled=False
|
||||||
|
).count()
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
feedback = form.save(commit=False)
|
||||||
|
feedback.facility = self.facility
|
||||||
|
if self.request.user.is_authenticated and not form.cleaned_data["anonymous"]:
|
||||||
|
feedback.user = self.request.user
|
||||||
|
feedback.save()
|
||||||
|
messages.success(self.request, "Your feedback has been submitted. Thank you!")
|
||||||
|
return redirect(
|
||||||
|
reverse(
|
||||||
|
"facilities:facility_feedback",
|
||||||
|
kwargs={
|
||||||
|
"camp_slug": self.camp.slug,
|
||||||
|
"facility_type_slug": self.facility_type.slug,
|
||||||
|
"facility_uuid": self.facility.uuid,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<p>
|
<p>
|
||||||
BornHack can always improve, but we need your feedback to know how. So write you thoughts on what was good and what wasn't, it's highly appreciated!
|
BornHack can always improve, but we need your feedback to know how. So write your thoughts on what was good and what wasn't at {{ camp.title }}, it's highly appreciated!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -7,4 +7,5 @@ codecov==2.0.15
|
||||||
black==19.10b0
|
black==19.10b0
|
||||||
pre-commit==2.1.0
|
pre-commit==2.1.0
|
||||||
isort==4.3.21
|
isort==4.3.21
|
||||||
|
flake8==3.7.9
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ django-bootstrap3==12.0.3
|
||||||
django-cors-headers==3.2.1
|
django-cors-headers==3.2.1
|
||||||
django-extensions==2.2.8
|
django-extensions==2.2.8
|
||||||
django-filter==2.2.0
|
django-filter==2.2.0
|
||||||
|
django-leaflet==0.26.0
|
||||||
django-reversion==3.0.7
|
django-reversion==3.0.7
|
||||||
django-wkhtmltopdf==3.3.0
|
django-wkhtmltopdf==3.3.0
|
||||||
future==0.18.2
|
future==0.18.2
|
||||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 608 KiB After Width: | Height: | Size: 699 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 141 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 829 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
23
src/teams/migrations/0052_team_permission_set.py
Normal file
23
src/teams/migrations/0052_team_permission_set.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 3.0.3 on 2020-02-19 19:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("teams", "0051_auto_20190312_1129"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="team",
|
||||||
|
name="permission_set",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
help_text="The name of this Teams set of permissions. Must be a value from camps.models.Permission.Meta.permissions.",
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -6,7 +6,6 @@ from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel
|
from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel
|
||||||
|
|
||||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
@ -55,6 +54,13 @@ class Team(CampRelatedModel):
|
||||||
|
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
|
|
||||||
|
permission_set = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
|
help_text="The name of this Teams set of permissions. Must be a value from camps.models.Permission.Meta.permissions.",
|
||||||
|
)
|
||||||
|
|
||||||
needs_members = models.BooleanField(
|
needs_members = models.BooleanField(
|
||||||
default=True, help_text="Check to indicate that this team needs more members"
|
default=True, help_text="Check to indicate that this team needs more members"
|
||||||
)
|
)
|
||||||
|
@ -268,7 +274,6 @@ class Team(CampRelatedModel):
|
||||||
|
|
||||||
|
|
||||||
class TeamMember(CampRelatedModel):
|
class TeamMember(CampRelatedModel):
|
||||||
|
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
"auth.User",
|
"auth.User",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from camps.mixins import CampViewMixin
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
@ -8,8 +9,6 @@ from django.urls import reverse_lazy
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
from django.views.generic.edit import UpdateView
|
from django.views.generic.edit import UpdateView
|
||||||
|
|
||||||
from camps.mixins import CampViewMixin
|
|
||||||
|
|
||||||
from ..models import Team, TeamMember
|
from ..models import Team, TeamMember
|
||||||
from .mixins import EnsureTeamResponsibleMixin
|
from .mixins import EnsureTeamResponsibleMixin
|
||||||
|
|
||||||
|
@ -21,6 +20,16 @@ class TeamListView(CampViewMixin, ListView):
|
||||||
model = Team
|
model = Team
|
||||||
context_object_name = "teams"
|
context_object_name = "teams"
|
||||||
|
|
||||||
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
qs = super().get_queryset(*args, **kwargs)
|
||||||
|
qs = qs.prefetch_related("members")
|
||||||
|
qs = qs.prefetch_related("members__profile")
|
||||||
|
# FIXME: there is more to be gained here but the templatetag we use to see if
|
||||||
|
# the logged-in user is a member of the current team does not benefit from the prefetching,
|
||||||
|
# also the getting of team responsible members and their profiles do not use the prefetching
|
||||||
|
# :( /tyk
|
||||||
|
return qs
|
||||||
|
|
||||||
def get_context_data(self, *, object_list=None, **kwargs):
|
def get_context_data(self, *, object_list=None, **kwargs):
|
||||||
context = super().get_context_data(object_list=object_list, **kwargs)
|
context = super().get_context_data(object_list=object_list, **kwargs)
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
<li><a href="{% url 'contact' %}">Contact</a></li>
|
<li><a href="{% url 'contact' %}">Contact</a></li>
|
||||||
<li><a href="{% url 'people' %}">People</a></li>
|
<li><a href="{% url 'people' %}">People</a></li>
|
||||||
<li><a href="{% url 'wish_list_redirect' %}">Wishlist</a></li>
|
<li><a href="{% url 'wish_list_redirect' %}">Wishlist</a></li>
|
||||||
|
|
||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
<li><a href="{% url 'admin:index' %}">Django Admin</a></li>
|
<li><a href="{% url 'admin:index' %}">Django Admin</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -11,11 +11,16 @@
|
||||||
<a class="btn {% menubuttonclass 'feedback' %}" href="{% url 'feedback' camp_slug=camp.slug %}">Feedback</a>
|
<a class="btn {% menubuttonclass 'feedback' %}" href="{% url 'feedback' camp_slug=camp.slug %}">Feedback</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">More <span class="caret"></span></button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="{% url 'facilities:facility_type_list' camp_slug=camp.slug %}">Facilities</a></li>
|
||||||
{% if perms.camps.expense_create_permission or perms.camps.revenue_create_permission %}
|
{% if perms.camps.expense_create_permission or perms.camps.revenue_create_permission %}
|
||||||
<a class="btn {% menubuttonclass 'economy' %}" href="{% url 'economy:dashboard' camp_slug=camp.slug %}">Economy</a>
|
<li><a href="{% url 'economy:dashboard' camp_slug=camp.slug %}">Economy</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if perms.camps.backoffice_permission %}
|
{% if perms.camps.backoffice_permission %}
|
||||||
<a class="btn {% menubuttonclass 'backoffice' %}" href="{% url 'backoffice:index' camp_slug=camp.slug %}">Backoffice</a>
|
<li><a href="{% url 'backoffice:index' camp_slug=camp.slug %}">Backoffice</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
import factory
|
import factory
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
|
from camps.models import Camp
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from faker import Faker
|
|
||||||
|
|
||||||
from camps.models import Camp
|
|
||||||
from events.models import Routing, Type
|
from events.models import Routing, Type
|
||||||
|
from facilities.models import (
|
||||||
|
Facility,
|
||||||
|
FacilityFeedback,
|
||||||
|
FacilityQuickFeedback,
|
||||||
|
FacilityType,
|
||||||
|
)
|
||||||
|
from faker import Faker
|
||||||
from feedback.models import Feedback
|
from feedback.models import Feedback
|
||||||
from info.models import InfoCategory, InfoItem
|
from info.models import InfoCategory, InfoItem
|
||||||
from news.models import NewsItem
|
from news.models import NewsItem
|
||||||
|
@ -169,6 +175,126 @@ class Command(BaseCommand):
|
||||||
title="unpublished news item", content="unpublished news body here"
|
title="unpublished news item", content="unpublished news body here"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def create_quickfeedback_options(self):
|
||||||
|
options = {}
|
||||||
|
self.output("Creating quickfeedback options")
|
||||||
|
options["na"] = FacilityQuickFeedback.objects.create(
|
||||||
|
feedback="N/A", icon="fas fa-times"
|
||||||
|
)
|
||||||
|
options["attention"] = FacilityQuickFeedback.objects.create(
|
||||||
|
feedback="Needs attention"
|
||||||
|
)
|
||||||
|
options["toiletpaper"] = FacilityQuickFeedback.objects.create(
|
||||||
|
feedback="Needs more toiletpaper", icon="fas fa-toilet-paper"
|
||||||
|
)
|
||||||
|
options["cleaning"] = FacilityQuickFeedback.objects.create(
|
||||||
|
feedback="Needs cleaning", icon="fas fa-broom"
|
||||||
|
)
|
||||||
|
options["power"] = FacilityQuickFeedback.objects.create(
|
||||||
|
feedback="No power", icon="fas fa-bolt"
|
||||||
|
)
|
||||||
|
return options
|
||||||
|
|
||||||
|
def create_facility_types(self, camp, teams, options):
|
||||||
|
types = {}
|
||||||
|
self.output("Creating facility types...")
|
||||||
|
types["toilet"] = FacilityType.objects.create(
|
||||||
|
name="Toilets",
|
||||||
|
description="All the toilets",
|
||||||
|
icon="fas fa-toilet",
|
||||||
|
responsible_team=teams["shit"],
|
||||||
|
)
|
||||||
|
types["toilet"].quickfeedback_options.add(options["na"])
|
||||||
|
types["toilet"].quickfeedback_options.add(options["attention"])
|
||||||
|
types["toilet"].quickfeedback_options.add(options["toiletpaper"])
|
||||||
|
types["toilet"].quickfeedback_options.add(options["cleaning"])
|
||||||
|
|
||||||
|
types["power"] = FacilityType.objects.create(
|
||||||
|
name="Power Infrastructure",
|
||||||
|
description="Power related infrastructure, distribution points, distribution cables, and so on.",
|
||||||
|
icon="fas fa-plug",
|
||||||
|
responsible_team=teams["power"],
|
||||||
|
)
|
||||||
|
types["power"].quickfeedback_options.add(options["attention"])
|
||||||
|
types["power"].quickfeedback_options.add(options["power"])
|
||||||
|
return types
|
||||||
|
|
||||||
|
def create_facilities(self, facility_types):
|
||||||
|
facilities = {}
|
||||||
|
self.output("Creating facilities...")
|
||||||
|
facilities["toilet1"] = Facility.objects.create(
|
||||||
|
facility_type=facility_types["toilet"],
|
||||||
|
name="Toilet A1",
|
||||||
|
description="Toilet on the left side in the NOC building",
|
||||||
|
location=Point(1, 2),
|
||||||
|
)
|
||||||
|
facilities["toilet2"] = Facility.objects.create(
|
||||||
|
facility_type=facility_types["toilet"],
|
||||||
|
name="Toilet A2",
|
||||||
|
description="Toilet on the right side in the NOC building",
|
||||||
|
location=Point(3, 4),
|
||||||
|
)
|
||||||
|
facilities["pdp1"] = Facility.objects.create(
|
||||||
|
facility_type=facility_types["power"],
|
||||||
|
name="PDP1",
|
||||||
|
description="In orga area",
|
||||||
|
location=Point(5, 6),
|
||||||
|
)
|
||||||
|
facilities["pdp2"] = Facility.objects.create(
|
||||||
|
facility_type=facility_types["power"],
|
||||||
|
name="PDP2",
|
||||||
|
description="In bar area",
|
||||||
|
location=Point(7, 8),
|
||||||
|
)
|
||||||
|
facilities["pdp3"] = Facility.objects.create(
|
||||||
|
facility_type=facility_types["power"],
|
||||||
|
name="PDP3",
|
||||||
|
description="In speaker tent",
|
||||||
|
location=Point(9, 10),
|
||||||
|
)
|
||||||
|
facilities["pdp4"] = Facility.objects.create(
|
||||||
|
facility_type=facility_types["power"],
|
||||||
|
name="PDP4",
|
||||||
|
description="In food area",
|
||||||
|
location=Point(11, 12),
|
||||||
|
)
|
||||||
|
return facilities
|
||||||
|
|
||||||
|
def create_facility_feedbacks(self, facilities, options, users):
|
||||||
|
self.output("Creating facility feedbacks...")
|
||||||
|
FacilityFeedback.objects.create(
|
||||||
|
user=users[1],
|
||||||
|
facility=facilities["toilet1"],
|
||||||
|
quick_feedback=options["attention"],
|
||||||
|
comment="Something smells wrong",
|
||||||
|
urgent=True,
|
||||||
|
)
|
||||||
|
FacilityFeedback.objects.create(
|
||||||
|
user=users[2],
|
||||||
|
facility=facilities["toilet1"],
|
||||||
|
quick_feedback=options["toiletpaper"],
|
||||||
|
urgent=False,
|
||||||
|
)
|
||||||
|
FacilityFeedback.objects.create(
|
||||||
|
facility=facilities["toilet2"],
|
||||||
|
quick_feedback=options["cleaning"],
|
||||||
|
comment="This place needs cleaning please. Anonymous feedback.",
|
||||||
|
urgent=False,
|
||||||
|
)
|
||||||
|
FacilityFeedback.objects.create(
|
||||||
|
facility=facilities["pdp1"],
|
||||||
|
quick_feedback=options["attention"],
|
||||||
|
comment="Rain cover needs some work, and we need more free plugs! This feedback is submitted anonymously.",
|
||||||
|
urgent=False,
|
||||||
|
)
|
||||||
|
FacilityFeedback.objects.create(
|
||||||
|
user=users[5],
|
||||||
|
facility=facilities["pdp2"],
|
||||||
|
quick_feedback=options["power"],
|
||||||
|
comment="No power, please help",
|
||||||
|
urgent=True,
|
||||||
|
)
|
||||||
|
|
||||||
def create_event_types(self):
|
def create_event_types(self):
|
||||||
types = {}
|
types = {}
|
||||||
self.output("Creating event types...")
|
self.output("Creating event types...")
|
||||||
|
@ -1144,21 +1270,37 @@ class Command(BaseCommand):
|
||||||
description="The Orga team are the main organisers. All tasks are Orga responsibility until they are delegated to another team",
|
description="The Orga team are the main organisers. All tasks are Orga responsibility until they are delegated to another team",
|
||||||
camp=camp,
|
camp=camp,
|
||||||
needs_members=False,
|
needs_members=False,
|
||||||
|
permission_set="orgateam_permission",
|
||||||
)
|
)
|
||||||
teams["noc"] = Team.objects.create(
|
teams["noc"] = Team.objects.create(
|
||||||
name="NOC",
|
name="NOC",
|
||||||
description="The NOC team is in charge of establishing and running a network onsite.",
|
description="The NOC team is in charge of establishing and running a network onsite.",
|
||||||
camp=camp,
|
camp=camp,
|
||||||
|
permission_set="nocteam_permission",
|
||||||
)
|
)
|
||||||
teams["bar"] = Team.objects.create(
|
teams["bar"] = Team.objects.create(
|
||||||
name="Bar",
|
name="Bar",
|
||||||
description="The Bar team plans, builds and run the IRL bar!",
|
description="The Bar team plans, builds and run the IRL bar!",
|
||||||
camp=camp,
|
camp=camp,
|
||||||
|
permission_set="barteam_permission",
|
||||||
)
|
)
|
||||||
teams["shuttle"] = Team.objects.create(
|
teams["shuttle"] = Team.objects.create(
|
||||||
name="Shuttle",
|
name="Shuttle",
|
||||||
description="The shuttle team drives people to and from the trainstation or the supermarket",
|
description="The shuttle team drives people to and from the trainstation or the supermarket",
|
||||||
camp=camp,
|
camp=camp,
|
||||||
|
permission_set="shuttleteam_permission",
|
||||||
|
)
|
||||||
|
teams["power"] = Team.objects.create(
|
||||||
|
name="Power",
|
||||||
|
description="The power team makes sure we have power all over the venue",
|
||||||
|
camp=camp,
|
||||||
|
permission_set="powerteam_permission",
|
||||||
|
)
|
||||||
|
teams["shit"] = Team.objects.create(
|
||||||
|
name="Sanitation",
|
||||||
|
description="Team shit takes care of the toilets",
|
||||||
|
camp=camp,
|
||||||
|
permission_set="sanitationteam_permission",
|
||||||
)
|
)
|
||||||
|
|
||||||
return teams
|
return teams
|
||||||
|
@ -1588,6 +1730,8 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
global_products = self.create_global_products(product_categories)
|
global_products = self.create_global_products(product_categories)
|
||||||
|
|
||||||
|
quickfeedback_options = self.create_quickfeedback_options()
|
||||||
|
|
||||||
for (camp, read_only) in camps:
|
for (camp, read_only) in camps:
|
||||||
year = camp.camp.lower.year
|
year = camp.camp.lower.year
|
||||||
|
|
||||||
|
@ -1626,6 +1770,14 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
self.create_camp_team_shifts(camp, teams, team_memberships)
|
self.create_camp_team_shifts(camp, teams, team_memberships)
|
||||||
|
|
||||||
|
facility_types = self.create_facility_types(
|
||||||
|
camp, teams, quickfeedback_options
|
||||||
|
)
|
||||||
|
|
||||||
|
facilities = self.create_facilities(facility_types)
|
||||||
|
|
||||||
|
self.create_facility_feedbacks(facilities, quickfeedback_options, users)
|
||||||
|
|
||||||
info_categories = self.create_camp_info_categories(camp, teams)
|
info_categories = self.create_camp_info_categories(camp, teams)
|
||||||
|
|
||||||
self.create_camp_info_items(camp, info_categories)
|
self.create_camp_info_items(camp, info_categories)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
||||||
|
|
||||||
from .models import Wish
|
from .models import Wish
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Wish)
|
@admin.register(Wish)
|
||||||
class WishAdmin(admin.ModelAdmin):
|
class WishAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 3.0.3 on 2020-02-22 14:48
|
# Generated by Django 3.0.3 on 2020-02-19 19:08
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
|
@ -1,5 +1,4 @@
|
||||||
from django.shortcuts import render
|
from django.views.generic import DetailView, ListView
|
||||||
from django.views.generic import ListView, DetailView
|
|
||||||
|
|
||||||
from .models import Wish
|
from .models import Wish
|
||||||
|
|
||||||
|
@ -13,4 +12,3 @@ class WishDetailView(DetailView):
|
||||||
model = Wish
|
model = Wish
|
||||||
template_name = "wish_detail.html"
|
template_name = "wish_detail.html"
|
||||||
slug_url_kwarg = "wish_slug"
|
slug_url_kwarg = "wish_slug"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue