Exemple #1
0
class LocationGroupCategory(SerializableMixin, models.Model):
    name = models.SlugField(_('Name'), unique=True, max_length=50)
    single = models.BooleanField(_('single selection'), default=False)
    title = I18nField(_('Title'), plural_name='titles', fallback_any=True)
    title_plural = I18nField(_('Title (Plural)'),
                             plural_name='titles_plural',
                             fallback_any=True)
    allow_levels = models.BooleanField(_('allow levels'),
                                       db_index=True,
                                       default=True)
    allow_spaces = models.BooleanField(_('allow spaces'),
                                       db_index=True,
                                       default=True)
    allow_areas = models.BooleanField(_('allow areas'),
                                      db_index=True,
                                      default=True)
    allow_pois = models.BooleanField(_('allow pois'),
                                     db_index=True,
                                     default=True)
    priority = models.IntegerField(default=0, db_index=True)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.orig_priority = self.priority

    class Meta:
        verbose_name = _('Location Group Category')
        verbose_name_plural = _('Location Group Categories')
        default_related_name = 'locationgroupcategories'
        ordering = ('-priority', )

    def _serialize(self, detailed=True, **kwargs):
        result = super()._serialize(detailed=detailed, **kwargs)
        result['name'] = self.name
        if detailed:
            result['titles'] = self.titles
        result['title'] = self.title
        return result

    def register_changed_geometries(self):
        from c3nav.mapdata.models.geometry.space import SpaceGeometryMixin
        query = self.groups.all()
        for model in get_submodels(SpecificLocation):
            related_name = model._meta.default_related_name
            subquery = model.objects.all()
            if issubclass(model, SpaceGeometryMixin):
                subquery = subquery.select_related('space')
            query.prefetch_related(
                Prefetch('groups__' + related_name, subquery))

        for group in query:
            group.register_changed_geometries(do_query=False)

    def save(self, *args, **kwargs):
        if self.pk and self.priority != self.orig_priority:
            self.register_changed_geometries()
        super().save(*args, **kwargs)
Exemple #2
0
class Space(LevelGeometryMixin, SpecificLocation, models.Model):
    """
    An accessible space. Shouldn't overlap with spaces on the same level.
    """
    geometry = GeometryField('polygon')
    height = models.DecimalField(_('height'), max_digits=6, decimal_places=2, null=True, blank=True,
                                 validators=[MinValueValidator(Decimal('0'))])
    outside = models.BooleanField(default=False, verbose_name=_('only outside of building'))
    enter_description = I18nField(_('Enter description'), blank=True, fallback_language=None)

    class Meta:
        verbose_name = _('Space')
        verbose_name_plural = _('Spaces')
        default_related_name = 'spaces'

    def _serialize(self, geometry=True, **kwargs):
        result = super()._serialize(geometry=geometry, **kwargs)
        result['outside'] = self.outside
        result['height'] = None if self.height is None else float(str(self.height))
        return result

    def details_display(self, editor_url=True, **kwargs):
        result = super().details_display(**kwargs)
        result['display'].extend([
            (_('height'), self.height),
            (_('outside only'), _('Yes') if self.outside else _('No')),
        ])
        if editor_url:
            result['editor_url'] = reverse('editor.spaces.detail', kwargs={'level': self.level_id, 'pk': self.pk})
        return result
Exemple #3
0
class LeaveDescription(SerializableMixin):
    """
    A description for leaving a space to another space
    """
    space = models.ForeignKey('mapdata.Space', on_delete=models.CASCADE, verbose_name=_('space'))
    target_space = models.ForeignKey('mapdata.Space', on_delete=models.CASCADE, verbose_name=_('target space'),
                                     related_name='enter_descriptions')
    description = I18nField(_('description'))

    class Meta:
        verbose_name = _('Leave description')
        verbose_name_plural = _('Leave descriptions')
        default_related_name = 'leave_descriptions'
        unique_together = (
            ('space', 'target_space')
        )

    def _serialize(self, **kwargs):
        result = super()._serialize(**kwargs)
        result['space'] = self.space_id
        result['target_space'] = self.target_space_id
        result['description_i18n'] = self.description_i18n
        result['description'] = self.description
        return result

    @cached_property
    def title(self):
        return self.target_space.title

    @classmethod
    def q_for_request(cls, request, prefix='', allow_none=False):
        return (
            Space.q_for_request(request, prefix='space__', allow_none=allow_none) &
            Space.q_for_request(request, prefix='target_space__', allow_none=allow_none)
        )
Exemple #4
0
class TitledMixin(SerializableMixin, models.Model):
    title = I18nField(_('Title'),
                      plural_name='titles',
                      blank=True,
                      fallback_any=True,
                      fallback_value='{model} {pk}')

    class Meta:
        abstract = True

    def serialize(self, **kwargs):
        result = super().serialize(**kwargs)
        return result

    def _serialize(self, detailed=True, **kwargs):
        result = super()._serialize(detailed=detailed, **kwargs)
        if detailed:
            result['titles'] = self.titles
        result['title'] = self.title
        return result

    def details_display(self, **kwargs):
        result = super().details_display(**kwargs)
        for lang, title in sorted(self.titles.items(),
                                  key=lambda item: item[0] != get_language()):
            language = _('Title ({lang})').format(
                lang=get_language_info(lang)['name_translated'])
            result['display'].append((language, title))
        return result
Exemple #5
0
class WayType(SerializableMixin, models.Model):
    """
    A special way type
    """
    title = I18nField(_('Title'), plural_name='titles', fallback_any=True)
    title_plural = I18nField(_('Title (Plural)'),
                             plural_name='titles_plural',
                             fallback_any=True)
    join_edges = models.BooleanField(_('join consecutive edges'), default=True)
    up_separate = models.BooleanField(_('upwards separately'), default=True)
    walk = models.BooleanField(_('walking'), default=False)
    color = models.CharField(max_length=32, verbose_name=_('edge color'))
    icon_name = models.CharField(_('icon name'),
                                 max_length=32,
                                 null=True,
                                 blank=True)
    extra_seconds = models.PositiveSmallIntegerField(
        _('extra seconds per edge'), default=0)
    speed = models.DecimalField(_('speed (m/s)'),
                                max_digits=3,
                                decimal_places=1,
                                default=1,
                                validators=[MinValueValidator(Decimal('0.1'))])
    description = I18nField(_('description'), fallback_any=True)
    speed_up = models.DecimalField(
        _('speed upwards (m/s)'),
        max_digits=3,
        decimal_places=1,
        default=1,
        validators=[MinValueValidator(Decimal('0.1'))])
    description_up = I18nField(_('description upwards'), fallback_any=True)
    level_change_description = I18nField(_('level change description'))

    class Meta:
        verbose_name = _('Way Type')
        verbose_name_plural = _('Way Types')
        default_related_name = 'waytypes'
Exemple #6
0
class Announcement(models.Model):
    created = models.DateTimeField(auto_now_add=True,
                                   verbose_name=_('created'))
    active_until = models.DateTimeField(null=True,
                                        verbose_name=_('active until'))
    active = models.BooleanField(default=True, verbose_name=_('active'))
    author = models.ForeignKey(settings.AUTH_USER_MODEL,
                               null=True,
                               on_delete=models.PROTECT,
                               verbose_name=_('author'))
    text = I18nField(_('Text'), fallback_any=True)

    class Meta:
        verbose_name = _('Announcement')
        verbose_name_plural = _('Announcements')
        default_related_name = 'announcements'
        get_latest_by = 'created'

    @classmethod
    def get_current(cls):
        result = cache.get('site:announcement', False)
        if result is not False:
            return result

        try:
            result = cls.objects.filter(
                Q(active=True)
                & (Q(active_until__isnull=True)
                   | Q(active_until__gt=timezone.now()))).latest()
        except cls.DoesNotExist:
            result = None

        timeout = 300
        if result and result.active_until:
            timeout = max(
                0,
                min(timeout,
                    (result.active_until - timezone.now()).total_seconds()))
        cache.set('site:announcement', result, timeout)

        return result

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        cache.delete('site:announcement')
Exemple #7
0
class LabelSettings(SerializableMixin, models.Model):
    title = I18nField(_('Title'), plural_name='titles', fallback_any=True)
    min_zoom = models.DecimalField(_('min zoom'),
                                   max_digits=3,
                                   decimal_places=1,
                                   default=-10,
                                   validators=[
                                       MinValueValidator(Decimal('-10')),
                                       MaxValueValidator(Decimal('10'))
                                   ])
    max_zoom = models.DecimalField(_('max zoom'),
                                   max_digits=3,
                                   decimal_places=1,
                                   default=10,
                                   validators=[
                                       MinValueValidator(Decimal('-10')),
                                       MaxValueValidator(Decimal('10'))
                                   ])
    font_size = models.IntegerField(
        _('font size'),
        default=12,
        validators=[MinValueValidator(12),
                    MaxValueValidator(30)])

    def _serialize(self, detailed=True, **kwargs):
        result = super()._serialize(detailed=detailed, **kwargs)
        if detailed:
            result['titles'] = self.titles
        if self.min_zoom > -10:
            result['min_zoom'] = self.min_zoom
        if self.max_zoom < 10:
            result['max_zoom'] = self.max_zoom
        result['font_size'] = self.font_size
        return result

    class Meta:
        verbose_name = _('Label Settings')
        verbose_name_plural = _('Label Settings')
        default_related_name = 'labelsettings'
        ordering = ('min_zoom', '-font_size')
Exemple #8
0
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.PROTECT, 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,
                                            limit_choices_to={'can_report_missing': True},
                                            help_text=_('select all groups that apply, if any'), related_name='+')
    secret = models.CharField(_('secret'), max_length=32, default=get_report_secret)

    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 ReportMissingLocationForm, ReportIssueForm
        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,
                                       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()
Exemple #9
0
class SpecificLocation(Location, models.Model):
    groups = models.ManyToManyField('mapdata.LocationGroup',
                                    verbose_name=_('Location Groups'),
                                    blank=True)
    label_settings = models.ForeignKey('mapdata.LabelSettings',
                                       null=True,
                                       blank=True,
                                       on_delete=models.PROTECT,
                                       verbose_name=_('label settings'))
    label_override = I18nField(_('Label override'),
                               plural_name='label_overrides',
                               blank=True,
                               fallback_any=True)

    class Meta:
        abstract = True

    def _serialize(self, detailed=True, **kwargs):
        result = super()._serialize(detailed=detailed, **kwargs)
        if grid.enabled:
            grid_square = self.grid_square
            if grid_square is not None:
                result['grid_square'] = grid_square or None
        if detailed:
            groups = {}
            for group in self.groups.all():
                groups.setdefault(group.category, []).append(group.pk)
            groups = {
                category.name:
                (items[0] if items else None) if category.single else items
                for category, items in groups.items() if getattr(
                    category, 'allow_' +
                    self.__class__._meta.default_related_name)
            }
            result['groups'] = groups

        label_settings = self.get_label_settings()
        if label_settings:
            result['label_settings'] = label_settings.serialize(detailed=False)
        if self.label_overrides:
            # todo: what if only one language is set?
            result['label_override'] = self.label_override
        return result

    def get_label_settings(self):
        if self.label_settings:
            return self.label_settings
        for group in self.groups.all():
            if group.label_settings:
                return group.label_settings
        return None

    def details_display(self, **kwargs):
        result = super().details_display(**kwargs)

        groupcategories = {}
        for group in self.groups.all():
            groupcategories.setdefault(group.category, []).append(group)

        if grid.enabled:
            grid_square = self.grid_square
            if grid_square is not None:
                grid_square_title = (_('Grid Squares')
                                     if grid_square and '-' in grid_square else
                                     _('Grid Square'))
                result['display'].insert(
                    3, (grid_square_title, grid_square or None))

        for category, groups in sorted(groupcategories.items(),
                                       key=lambda item: item[0].priority):
            result['display'].insert(
                3,
                (category.title if category.single else category.title_plural,
                 tuple({
                     'id': group.pk,
                     'slug': group.get_slug(),
                     'title': group.title,
                     'can_search': group.can_search,
                 } for group in sorted(
                     groups, key=attrgetter('priority'), reverse=True))))

        return result

    @cached_property
    def describing_groups(self):
        groups = tuple(self.groups.all() if 'groups' in
                       getattr(self, '_prefetched_objects_cache', ()) else ())
        groups = tuple(group for group in groups if group.can_describe)
        return groups

    @property
    def subtitle(self):
        subtitle = self.describing_groups[
            0].title if self.describing_groups else self.__class__._meta.verbose_name
        if self.grid_square:
            return '%s, %s' % (subtitle, self.grid_square)
        return subtitle

    @cached_property
    def order(self):
        groups = tuple(self.groups.all())
        if not groups:
            return (0, 0, 0)
        return (0, groups[0].category.priority, groups[0].priority)

    def get_icon(self):
        icon = super().get_icon()
        if icon:
            return icon
        for group in self.groups.all():
            if group.icon and getattr(
                    group.category,
                    'allow_' + self.__class__._meta.default_related_name):
                return group.icon
        return None