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