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:
Thomas Steen Rasmussen 2020-02-24 23:28:52 +01:00 committed by GitHub
parent df1a6564b7
commit 12a1c9a0ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 10398 additions and 3864 deletions

View file

@ -1,4 +1,4 @@
[flake8] [flake8]
max-line-length = 88 max-line-length = 88
ignore = E501 W503 ignore = E501 W503 E231

View file

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

View file

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

View 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 %}

View file

@ -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 %}"

View file

@ -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/",

View file

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

View file

@ -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 }}',

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

65
src/facilities/admin.py Normal file
View 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
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class FacilitiesConfig(AppConfig):
name = "facilities"

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

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

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

View file

42
src/facilities/mixins.py Normal file
View 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
View 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"

View 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 %}

View 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 %}

View 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 %}

View 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
View 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
View 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,
},
)
)

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 608 KiB

After

Width:  |  Height:  |  Size: 699 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 141 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 579 KiB

After

Width:  |  Height:  |  Size: 829 KiB

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

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