270 lines
8.6 KiB
Python
270 lines
8.6 KiB
Python
import base64
|
|
import io
|
|
import logging
|
|
|
|
import qrcode
|
|
from django.contrib.gis.db.models import PointField
|
|
from django.contrib.gis.geos import Point
|
|
from django.contrib.postgres.constraints import ExclusionConstraint
|
|
from django.contrib.postgres.fields import DateTimeRangeField, RangeOperators
|
|
from django.db import models
|
|
from django.shortcuts import reverse
|
|
from maps.utils import LeafletMarkerChoices
|
|
from utils.models import CampRelatedModel, UUIDModel
|
|
from utils.slugs import unique_slugify
|
|
|
|
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 facilities of this type
|
|
"""
|
|
|
|
class Meta:
|
|
# we need a unique slug for each team due to the url structure in backoffice
|
|
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.",
|
|
)
|
|
|
|
marker = models.CharField(
|
|
max_length=10,
|
|
choices=LeafletMarkerChoices.choices,
|
|
default=LeafletMarkerChoices.BLUE,
|
|
help_text="The name/colour of the Leaflet marker to use for this facility type.",
|
|
)
|
|
|
|
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 = unique_slugify(
|
|
self.name,
|
|
slugs_in_use=self.__class__.objects.filter(
|
|
responsible_team=self.responsible_team
|
|
).values_list("slug", flat=True),
|
|
)
|
|
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")
|
|
|
|
# default to near the workshop rooms / cabins
|
|
location = PointField(
|
|
default=Point(9.93891, 55.38562), help_text="The location of this facility."
|
|
)
|
|
|
|
@property
|
|
def team(self):
|
|
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}"
|
|
|
|
def unhandled_feedbacks(self):
|
|
return self.feedbacks.filter(handled=False)
|
|
|
|
|
|
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"
|
|
|
|
|
|
class FacilityOpeningHours(CampRelatedModel):
|
|
"""
|
|
This model contains opening hours for facilities which are not always open.
|
|
If a facility has zero entries in this model it means is always open.
|
|
If a facility has one or more periods of opening hours defined in this model
|
|
it is considered closed outside of the period(s) defined in this model.
|
|
"""
|
|
|
|
class Meta:
|
|
ordering = ["when"]
|
|
constraints = [
|
|
# we do not want overlapping hours for the same Facility
|
|
ExclusionConstraint(
|
|
name="prevent_facility_opening_hours_overlaps",
|
|
expressions=[
|
|
("when", RangeOperators.OVERLAPS),
|
|
("facility", RangeOperators.EQUAL),
|
|
],
|
|
),
|
|
]
|
|
|
|
facility = models.ForeignKey(
|
|
"facilities.Facility",
|
|
related_name="opening_hours",
|
|
on_delete=models.PROTECT,
|
|
help_text="The Facility to which these opening hours belong.",
|
|
)
|
|
|
|
when = DateTimeRangeField(
|
|
db_index=True, help_text="The period when this facility is open.",
|
|
)
|
|
|
|
notes = models.TextField(
|
|
blank=True,
|
|
help_text="Any notes for this period like 'no hot food after 20' or 'no alcohol sale after 02'. Optional.",
|
|
)
|
|
|
|
@property
|
|
def camp(self):
|
|
return self.facility.camp
|
|
|
|
camp_filter = "facility__facility_type__responsible_team__camp"
|
|
|
|
@property
|
|
def duration(self):
|
|
return self.when.upper - self.when.lower
|