first stuff for custom positions

This commit is contained in:
Laura Klünder 2019-12-27 14:13:40 +01:00
parent e144156b8a
commit 407e3ba06b
12 changed files with 655 additions and 267 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,56 @@
# Generated by Django 2.2.8 on 2019-12-27 12:51
import c3nav.mapdata.fields
import c3nav.mapdata.models.locations
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('mapdata', '0081_auto_20191225_1015'),
]
operations = [
migrations.CreateModel(
name='Position',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=32, verbose_name='name')),
('secret', models.CharField(default=c3nav.mapdata.models.locations.get_position_secret, max_length=32, unique=True, verbose_name='secret')),
('last_location_update', models.DateTimeField(null=True, verbose_name='last location update')),
('location_id', models.CharField(max_length=48, null=True, verbose_name='location')),
('api_secret', models.CharField(default=c3nav.mapdata.models.locations.get_position_api_secret, max_length=64, verbose_name='api secret')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dynamic_positions', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Dynamic position',
'verbose_name_plural': 'Dynamic position',
'default_related_name': 'dynamic_positions',
},
),
migrations.CreateModel(
name='DynamicLocation',
fields=[
('locationslug_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='dynamic_locations', serialize=False, to='mapdata.LocationSlug')),
('title', c3nav.mapdata.fields.I18nField(blank=True, fallback_any=True, fallback_value='{model} {pk}', plural_name='titles', verbose_name='Title')),
('can_search', models.BooleanField(default=True, verbose_name='can be searched')),
('can_describe', models.BooleanField(default=True, verbose_name='can describe')),
('icon', models.CharField(blank=True, help_text='any material icons name', max_length=32, null=True, verbose_name='icon')),
('label_override', c3nav.mapdata.fields.I18nField(blank=True, fallback_any=True, plural_name='label_overrides', verbose_name='Label override')),
('position_secret', models.CharField(blank=True, max_length=32, null=True, verbose_name='position secret')),
('access_restriction', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='dynamic_locations', to='mapdata.AccessRestriction', verbose_name='Access Restriction')),
('groups', models.ManyToManyField(blank=True, related_name='dynamic_locations', to='mapdata.LocationGroup', verbose_name='Location Groups')),
('label_settings', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='dynamic_locations', to='mapdata.LabelSettings', verbose_name='label settings')),
],
options={
'verbose_name': 'Dynamic location',
'verbose_name_plural': 'Dynamic locations',
'default_related_name': 'dynamic_locations',
},
bases=('mapdata.locationslug', models.Model),
),
]

View file

@ -1,11 +1,14 @@
import string
from contextlib import suppress
from decimal import Decimal
from operator import attrgetter
from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.db import models
from django.db.models import FieldDoesNotExist, Prefetch
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.text import format_lazy
from django.utils.translation import ugettext_lazy as _
@ -15,6 +18,7 @@ from c3nav.mapdata.fields import I18nField
from c3nav.mapdata.grid import grid
from c3nav.mapdata.models.access import AccessRestrictionMixin
from c3nav.mapdata.models.base import SerializableMixin, TitledMixin
from c3nav.mapdata.utils.fields import LocationById
from c3nav.mapdata.utils.models import get_submodels
@ -452,3 +456,52 @@ class LabelSettings(SerializableMixin, models.Model):
verbose_name_plural = _('Label Settings')
default_related_name = 'labelsettings'
ordering = ('min_zoom', '-font_size')
class DynamicLocation(SpecificLocation, models.Model):
position_secret = models.CharField(_('position secret'), max_length=32, null=True, blank=True)
class Meta:
verbose_name = _('Dynamic location')
verbose_name_plural = _('Dynamic locations')
default_related_name = 'dynamic_locations'
"""
def _serialize(self, **kwargs):
result = super()._serialize(**kwargs)
return result
@property
def grid_square(self):
return grid.get_squares_for_bounds(self.geometry.bounds) or ''
def details_display(self, editor_url=True, **kwargs):
result = super().details_display(**kwargs)
if editor_url:
result['editor_url'] = reverse('editor.areas.edit', kwargs={'space': self.space_id, 'pk': self.pk})
return result
"""
def get_position_secret():
return get_random_string(32, string.ascii_letters+string.digits)
def get_position_api_secret():
return get_random_string(64, string.ascii_letters+string.digits)
class Position(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
name = models.CharField(_('name'), max_length=32)
secret = models.CharField(_('secret'), unique=True, max_length=32, default=get_position_secret)
last_location_update = models.DateTimeField(_('last location update'), null=True)
location_id = models.CharField(_('location'), null=True, max_length=48)
api_secret = models.CharField(_('api secret'), max_length=64, default=get_position_api_secret)
coordinates = LocationById()
class Meta:
verbose_name = _('Dynamic position')
verbose_name_plural = _('Dynamic position')
default_related_name = 'dynamic_positions'

View file

@ -3,7 +3,6 @@ import string
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models import Q
from django.urls import reverse
@ -15,7 +14,7 @@ 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.locations import get_location_by_id_for_request
from c3nav.mapdata.utils.fields import LocationById
from c3nav.mapdata.utils.models import get_submodels
from c3nav.site.tasks import send_report_notification
@ -24,39 +23,6 @@ def get_report_secret():
return get_random_string(32, string.ascii_letters)
class LocationById():
def __init__(self):
super().__init__()
self.name = None
self.cached_id = None
self.cached_value = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner=None):
value_id = getattr(instance, self.name+'_id')
if value_id is None:
self.cached_pk = None
self.cached_value = None
return None
if value_id == self.cached_id:
return self.cached_value
value = get_location_by_id_for_request(value_id, getattr(instance, 'request', None))
if value is None:
raise ObjectDoesNotExist
self.cached_id = value_id
self.cached_value = value
return value
def __set__(self, instance, value):
self.cached_id = value.pk
self.cached_value = value
setattr(instance, self.name+'_id', value.pk)
class Report(models.Model):
CATEGORIES = (
('location-issue', _('location issue')),

View file

@ -0,0 +1,35 @@
from django.core.exceptions import ObjectDoesNotExist
class LocationById():
def __init__(self):
super().__init__()
self.name = None
self.cached_id = None
self.cached_value = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner=None):
value_id = getattr(instance, self.name+'_id')
if value_id is None:
self.cached_pk = None
self.cached_value = None
return None
if value_id == self.cached_id:
return self.cached_value
from c3nav.mapdata.utils.locations import get_location_by_id_for_request
value = get_location_by_id_for_request(value_id, getattr(instance, 'request', None))
if value is None:
raise ObjectDoesNotExist
self.cached_id = value_id
self.cached_value = value
return value
def __set__(self, instance, value):
self.cached_id = value.pk
self.cached_value = value
setattr(instance, self.name+'_id', value.pk)

View file

@ -3,6 +3,7 @@ from django.forms import ModelForm
from django.utils.translation import ugettext_lazy as _
from c3nav.mapdata.forms import I18nModelFormMixin
from c3nav.mapdata.models.locations import Position
from c3nav.mapdata.models.report import Report, ReportUpdate
@ -46,3 +47,9 @@ class ReportUpdateForm(ModelForm):
class Meta:
model = ReportUpdate
fields = ['open', 'comment', 'public']
class PositionForm(ModelForm):
class Meta:
model = Position
fields = ['name']

View file

@ -41,6 +41,15 @@
</p>
{% endif %}
<hr>
<p>
{% trans 'You can create custom positions.' %}
</p>
<p>
<a class="button" href="{% url 'site.position_list' %}">{% trans 'Manage your positions' %}</a>
</p>
<hr>
<p>
<a class="button" href="{% url 'site.logout' %}">{% trans 'Log out' %}</a>

View file

@ -0,0 +1,16 @@
{% extends 'site/base.html' %}
{% load i18n %}
{% block content %}
<main class="account">
<h2>{% trans 'Create position' %}</h2>
{% include 'site/fragment_messages.html' %}
<p><a href="{% url 'site.position_list' %}">&laquo; {% trans 'back to overview' %}</a></p>
<form method="post" action="{{ request.path_info }}">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">{% trans 'Submit' %}</button>
</form>
</main>
{% endblock %}

View file

@ -0,0 +1,30 @@
{% extends 'site/base.html' %}
{% load i18n %}
{% block content %}
<main class="account">
{% include 'site/fragment_messages.html' %}
<h2>{% trans 'Position:' %} {{ position.name }}</h2>
<p><a href="{% url 'site.position_list' %}">&laquo; {% trans 'back to overview' %}</a></p>
<p>
<strong>{% trans 'Secret' %}:</strong>
<code>{{ position.secret }}</code>
</p>
<p>
<strong>{% trans 'API secret' %}:</strong>
<code>{{ position.api_secret }}</code>
</p>
<hr>
<form method="post" action="{{ request.path_info }}">
{% csrf_token %}
{{ form.as_p }}
<label><input type="checkbox" name="reset_secret" value="1"> {% trans 'reset secret' %}</label>
<label><input type="checkbox" name="reset_api_secret" value="1"> {% trans 'reset API secret' %}</label>
<label><input type="checkbox" name="delete" value="1"> {% trans 'delete this position' %}</label>
<button type="submit">{% trans 'Update position' %}</button>
</form>
</main>
{% endblock %}

View file

@ -0,0 +1,18 @@
{% extends 'site/base.html' %}
{% load i18n %}
{% block content %}
<main class="account">
{% include 'site/fragment_messages.html' %}
<h2>{% trans 'Your positions' %}</h2>
<p>(still work in progress, stay tuned for more information)</p>
<ul>
{% for position in positions %}
<li><a href="{% url 'site.position_detail' pk=position.pk %}">{{ position.name }}</a></li>
{% endfor %}
</ul>
<a class="button" href="{% url 'site.position_create' %}">{% trans 'Create position' %}</a>
</main>
{% endblock %}

View file

@ -1,8 +1,8 @@
from django.conf.urls import url
from c3nav.site.views import (about_view, access_redeem_view, account_view, change_password_view, choose_language,
login_view, logout_view, map_index, qr_code, register_view, report_create, report_detail,
report_list)
login_view, logout_view, map_index, position_create, position_detail, position_list,
qr_code, register_view, report_create, report_detail, report_list)
slug = r'(?P<slug>[a-z0-9-_.:]+)'
coordinates = r'(?P<coordinates>[a-z0-9-_:]+:-?\d+(\.\d+)?:-?\d+(\.\d+)?)'
@ -35,4 +35,7 @@ urlpatterns = [
url(r'^report/l/(?P<location>\d+)/$', report_create, name='site.report_create'),
url(r'^report/r/(?P<origin>[^/]+)/(?P<destination>[^/]+)/(?P<options>[^/]+)/$',
report_create, name='site.report_create'),
url(r'^positions/$', position_list, name='site.position_list'),
url(r'^positions/create/$', position_create, name='site.position_create'),
url(r'^positions/(?P<pk>\d+)/$', position_detail, name='site.position_detail'),
]

View file

@ -28,14 +28,15 @@ from c3nav.control.forms import AccessPermissionForm, SignedPermissionDataError
from c3nav.mapdata.grid import grid
from c3nav.mapdata.models import Location, Source
from c3nav.mapdata.models.access import AccessPermissionToken
from c3nav.mapdata.models.locations import LocationRedirect, SpecificLocation
from c3nav.mapdata.models.locations import (LocationRedirect, Position, SpecificLocation, get_position_api_secret,
get_position_secret)
from c3nav.mapdata.models.report import Report, ReportUpdate
from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, get_location_by_slug_for_request,
levels_by_short_label_for_request)
from c3nav.mapdata.utils.user import can_access_editor, get_user_data
from c3nav.mapdata.views import set_tile_access_cookie
from c3nav.routing.models import RouteOptions
from c3nav.site.forms import ReportUpdateForm
from c3nav.site.forms import PositionForm, ReportUpdateForm
from c3nav.site.models import Announcement, SiteUpdate
@ -433,6 +434,7 @@ def report_create(request, coordinates=None, location=None, origin=None, destina
})
@login_required(login_url='site.login')
def report_list(request, filter):
page = request.GET.get('page', 1)
@ -481,3 +483,66 @@ def report_detail(request, pk, secret=None):
'form': form,
'update_form': update_form,
})
@login_required(login_url='site.login')
def position_list(request):
return render(request, 'site/position_list.html', {
'positions': Position.objects.filter(owner=request.user),
})
@login_required(login_url='site.login')
def position_create(request):
if Position.objects.filter(owner=request.user).count() >= 20:
messages.error(request, _('You can\'t create more than 20 positions.'))
position = Position()
position.owner = request.user
if request.method == 'POST':
form = PositionForm(instance=position, data=request.POST)
if form.is_valid():
form.save()
messages.success(request, _('Position created.'))
return redirect(reverse('site.position_detail', kwargs={'pk': position.pk}))
else:
form = PositionForm(instance=position)
return render(request, 'site/position_create.html', {
'form': form,
})
pass
@login_required(login_url='site.login')
def position_detail(request, pk):
position = get_object_or_404(Position.objects.filter(owner=request.user), pk=pk)
if request.method == 'POST':
with transaction.atomic():
if request.POST.get('delete', None):
position.delete()
messages.success(request, _('Position deleted.'))
return redirect(reverse('site.position_list'))
if request.POST.get('reset_secret', None):
position.secret = get_position_secret()
if request.POST.get('reset_api_secret', None):
position.api_secret = get_position_api_secret()
form = PositionForm(instance=position, data=request.POST)
if form.is_valid():
form.save()
messages.success(request, _('Position updated.'))
return redirect(reverse('site.position_detail', kwargs={'pk': position.pk}))
else:
form = PositionForm(instance=position)
return render(request, 'site/position_detail.html', {
'position': position,
'form': form,
})
pass