import string from django.conf import settings from django.contrib.auth import get_user_model from django.core.cache import cache from django.db import models from django.db.models import Q from django.urls import reverse from django.utils.crypto import get_random_string from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from c3nav.mapdata.fields import I18nField from c3nav.mapdata.models.geometry.level import LevelGeometryMixin from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin from c3nav.mapdata.models.locations import SpecificLocation from c3nav.mapdata.utils.fields import LocationById from c3nav.mapdata.utils.models import get_submodels from c3nav.site.tasks import send_report_notification def get_report_secret(): return get_random_string(32, string.ascii_letters) class Report(models.Model): CATEGORIES = ( ('location-issue', _('location issue')), ('missing-location', _('missing location')), ('route-issue', _('route issue')), ) created = models.DateTimeField(auto_now_add=True, verbose_name=_('created')) category = models.CharField(max_length=20, db_index=True, choices=CATEGORIES, verbose_name=_('category')) author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL, verbose_name=_('author')) open = models.BooleanField(default=True, verbose_name=_('open')) last_update = models.DateTimeField(auto_now=True, verbose_name=_('last_update')) title = models.CharField(max_length=100, default='', verbose_name=_('title'), help_text=_('a short title for your report')) description = models.TextField(max_length=1000, default='', verbose_name=_('description'), help_text=_('tell us precisely what\'s wrong')) assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT, related_name='assigned_reports', verbose_name=_('assigned to')) location = models.ForeignKey('mapdata.LocationSlug', null=True, on_delete=models.SET_NULL, related_name='reports', verbose_name=_('location')) coordinates_id = models.CharField(_('coordinates'), null=True, max_length=48) origin_id = models.CharField(_('origin'), null=True, max_length=48) destination_id = models.CharField(_('destination'), null=True, max_length=48) route_options = models.CharField(_('route options'), null=True, max_length=128) created_title = I18nField(_('new location title'), plural_name='titles', blank=False, fallback_any=True, help_text=_('you have to supply a title in at least one language')) created_groups = models.ManyToManyField('mapdata.LocationGroup', verbose_name=_('location groups'), blank=True, help_text=_('select all groups that apply, if any'), related_name='+') secret = models.CharField(_('secret'), max_length=32, default=get_report_secret) import_tag = models.CharField(_('import tag'), null=True, blank=True, max_length=256) coordinates = LocationById() origin = LocationById() destination = LocationById() class Meta: verbose_name = _('Report') verbose_name_plural = _('Reports') default_related_name = 'reports' @property def form_cls(self): from c3nav.site.forms import ReportIssueForm, ReportMissingLocationForm return ReportMissingLocationForm if self.category == 'missing-location' else ReportIssueForm @cached_property def location_specific(self): if self.location is None: return None return self.location.get_child() @classmethod def qs_for_request(cls, request): if request.user_permissions.review_all_reports: return cls.objects.all() elif request.user.is_authenticated: location_ids = set() review_group_ids = request.user_permissions.review_group_ids for model in get_submodels(SpecificLocation): location_ids.update(set( model.objects.filter(groups__in=review_group_ids).values_list('pk', flat=True) )) return cls.objects.filter( Q(author=request.user) | Q(location_id__in=location_ids) | Q(created_groups__in=review_group_ids) ) else: return cls.objects.none() def get_affected_group_ids(self): if self.category == 'missing-location': return tuple(self.created_groups.values_list('pk', flat=True)) elif self.category == 'location-issue': return tuple(self.location.get_child().groups.values_list('pk', flat=True)) return () def get_reviewers_qs(self): return get_user_model().objects.filter( Q(permissions__review_all_reports=True) | Q(permissions__review_group_reports__in=self.get_affected_group_ids()) ) def request_can_review(self, request): return ( request.user_permissions.review_all_reports or set(request.user_permissions.review_group_ids) & set(self.get_affected_group_ids()) ) def notify_reviewers(self): reviewers = tuple(self.get_reviewers_qs().values_list('pk', flat=True)) send_report_notification.delay(pk=self.pk, title=self.title, author=self.author.username if self.author else "(none)", description=self.description, reviewers=reviewers) @classmethod def user_has_reports(cls, user): if not user.is_authenticated: return False result = cache.get('user:has-reports:%d' % user.pk, None) if result is None: result = user.reports.exists() cache.set('user:has-reports:%d' % user.pk, result, 900) return result @cached_property def editor_url(self): if self.category == 'missing-location': space = self.coordinates.space if space is not None: return reverse('editor.spaces.detail', kwargs={ 'pk': space.pk, 'level': space.level_id, })+'?x=%.2f&y=%.2f' % (self.coordinates.x, self.coordinates.y) return None elif self.category == 'location-issue': location = self.location_specific if location is None: return None url_name = 'editor.%s.edit' % location.__class__._meta.default_related_name if isinstance(location, SpaceGeometryMixin): return reverse(url_name, kwargs={ 'pk': location.pk, 'space': location.space.pk }) if isinstance(location, LevelGeometryMixin): return reverse(url_name, kwargs={ 'pk': location.pk, 'level': location.level.pk }) return reverse(url_name, kwargs={ 'pk': location.pk, }) def save(self, *args, **kwargs): created = self.pk is None if self.author: cache.delete('user:has-reports:%d' % self.author.pk) super().save(*args, **kwargs) if created: self.notify_reviewers() class ReportUpdate(models.Model): report = models.ForeignKey(Report, on_delete=models.CASCADE, related_name='updates') datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('datetime')) author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT, verbose_name=_('author')) open = models.BooleanField(null=True, verbose_name=_('open')) comment = models.TextField(verbose_name=_('comment'), blank=True) assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT, related_name='report_update_assigns', verbose_name=_('assigned to')) public = models.BooleanField(verbose_name=_('comment is public')) class Meta: verbose_name = _('Report update') verbose_name_plural = _('Report updates') default_related_name = 'reportupdates' ordering = ('datetime', )