2019-12-24 17:28:41 +01:00
|
|
|
import string
|
|
|
|
|
|
|
|
from django.conf import settings
|
2019-12-24 18:20:39 +01:00
|
|
|
from django.contrib.auth import get_user_model
|
2019-12-24 19:03:22 +01:00
|
|
|
from django.core.cache import cache
|
2019-12-24 17:28:41 +01:00
|
|
|
from django.db import models
|
2019-12-24 17:56:56 +01:00
|
|
|
from django.db.models import Q
|
2019-12-25 10:32:51 +01:00
|
|
|
from django.urls import reverse
|
2019-12-24 17:28:41 +01:00
|
|
|
from django.utils.crypto import get_random_string
|
2019-12-25 10:32:51 +01:00
|
|
|
from django.utils.functional import cached_property
|
2022-04-03 16:33:43 +02:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2019-12-24 17:28:41 +01:00
|
|
|
|
|
|
|
from c3nav.mapdata.fields import I18nField
|
2019-12-25 10:32:51 +01:00
|
|
|
from c3nav.mapdata.models.geometry.level import LevelGeometryMixin
|
|
|
|
from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
|
2019-12-24 18:07:42 +01:00
|
|
|
from c3nav.mapdata.models.locations import SpecificLocation
|
2019-12-27 14:13:40 +01:00
|
|
|
from c3nav.mapdata.utils.fields import LocationById
|
2019-12-24 18:07:42 +01:00
|
|
|
from c3nav.mapdata.utils.models import get_submodels
|
2019-12-24 18:20:39 +01:00
|
|
|
from c3nav.site.tasks import send_report_notification
|
2019-12-24 17:28:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
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'))
|
2023-12-18 11:14:13 +01:00
|
|
|
author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL, verbose_name=_('author'))
|
2019-12-24 17:28:41 +01:00
|
|
|
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,
|
2019-12-24 22:04:10 +01:00
|
|
|
help_text=_('select all groups that apply, if any'), related_name='+')
|
2019-12-24 17:28:41 +01:00
|
|
|
secret = models.CharField(_('secret'), max_length=32, default=get_report_secret)
|
|
|
|
|
2023-12-25 23:38:23 +01:00
|
|
|
import_tag = models.CharField(_('import tag'), null=True, blank=True, max_length=256)
|
|
|
|
|
2019-12-24 17:28:41 +01:00
|
|
|
coordinates = LocationById()
|
|
|
|
origin = LocationById()
|
|
|
|
destination = LocationById()
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _('Report')
|
|
|
|
verbose_name_plural = _('Reports')
|
2019-12-24 19:03:22 +01:00
|
|
|
default_related_name = 'reports'
|
2019-12-24 17:28:41 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def form_cls(self):
|
2022-04-03 16:42:17 +02:00
|
|
|
from c3nav.site.forms import ReportIssueForm, ReportMissingLocationForm
|
2019-12-24 17:28:41 +01:00
|
|
|
return ReportMissingLocationForm if self.category == 'missing-location' else ReportIssueForm
|
|
|
|
|
2019-12-25 10:32:51 +01:00
|
|
|
@cached_property
|
|
|
|
def location_specific(self):
|
2019-12-27 14:18:13 +01:00
|
|
|
if self.location is None:
|
|
|
|
return None
|
2019-12-25 10:32:51 +01:00
|
|
|
return self.location.get_child()
|
|
|
|
|
2019-12-24 17:28:41 +01:00
|
|
|
@classmethod
|
|
|
|
def qs_for_request(cls, request):
|
2019-12-24 17:56:56 +01:00
|
|
|
if request.user_permissions.review_all_reports:
|
2019-12-24 17:28:41 +01:00
|
|
|
return cls.objects.all()
|
|
|
|
elif request.user.is_authenticated:
|
2019-12-24 18:07:42 +01:00
|
|
|
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)
|
|
|
|
))
|
2019-12-24 17:56:56 +01:00
|
|
|
return cls.objects.filter(
|
|
|
|
Q(author=request.user) |
|
2019-12-24 18:07:42 +01:00
|
|
|
Q(location_id__in=location_ids) |
|
|
|
|
Q(created_groups__in=review_group_ids)
|
2019-12-24 17:56:56 +01:00
|
|
|
)
|
2019-12-24 17:28:41 +01:00
|
|
|
else:
|
|
|
|
return cls.objects.none()
|
|
|
|
|
2019-12-24 18:20:39 +01:00
|
|
|
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())
|
|
|
|
)
|
|
|
|
|
2019-12-25 10:16:40 +01:00
|
|
|
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())
|
|
|
|
)
|
|
|
|
|
2019-12-24 18:20:39 +01:00
|
|
|
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,
|
2023-12-25 23:38:23 +01:00
|
|
|
author=self.author.username if self.author else "(none)",
|
2019-12-24 18:20:39 +01:00
|
|
|
description=self.description,
|
|
|
|
reviewers=reviewers)
|
|
|
|
|
2019-12-24 19:03:22 +01:00
|
|
|
@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
|
|
|
|
|
2019-12-25 10:32:51 +01:00
|
|
|
@cached_property
|
|
|
|
def editor_url(self):
|
|
|
|
if self.category == 'missing-location':
|
2019-12-25 10:46:27 +01:00
|
|
|
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)
|
2019-12-25 10:32:51 +01:00
|
|
|
return None
|
|
|
|
elif self.category == 'location-issue':
|
|
|
|
location = self.location_specific
|
2019-12-27 14:19:54 +01:00
|
|
|
if location is None:
|
|
|
|
return None
|
2019-12-25 10:32:51 +01:00
|
|
|
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,
|
2019-12-25 11:09:38 +01:00
|
|
|
'level': location.level.pk
|
2019-12-25 10:32:51 +01:00
|
|
|
})
|
|
|
|
return reverse(url_name, kwargs={
|
|
|
|
'pk': location.pk,
|
|
|
|
})
|
|
|
|
|
2019-12-24 18:20:39 +01:00
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
created = self.pk is None
|
2019-12-24 19:03:22 +01:00
|
|
|
if self.author:
|
|
|
|
cache.delete('user:has-reports:%d' % self.author.pk)
|
2019-12-24 18:20:39 +01:00
|
|
|
super().save(*args, **kwargs)
|
|
|
|
if created:
|
|
|
|
self.notify_reviewers()
|
|
|
|
|
2019-12-24 17:28:41 +01:00
|
|
|
|
|
|
|
class ReportUpdate(models.Model):
|
2019-12-25 10:16:40 +01:00
|
|
|
report = models.ForeignKey(Report, on_delete=models.CASCADE, related_name='updates')
|
2019-12-24 17:28:41 +01:00
|
|
|
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'))
|
2022-04-03 16:33:43 +02:00
|
|
|
open = models.BooleanField(null=True, verbose_name=_('open'))
|
2019-12-25 10:16:40 +01:00
|
|
|
comment = models.TextField(verbose_name=_('comment'), blank=True)
|
2019-12-24 17:28:41 +01:00
|
|
|
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT,
|
|
|
|
related_name='report_update_assigns', verbose_name=_('assigned to'))
|
2019-12-25 10:16:40 +01:00
|
|
|
public = models.BooleanField(verbose_name=_('comment is public'))
|
2019-12-24 17:28:41 +01:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _('Report update')
|
|
|
|
verbose_name_plural = _('Report updates')
|
2019-12-25 10:16:40 +01:00
|
|
|
default_related_name = 'reportupdates'
|
|
|
|
ordering = ('datetime', )
|