Django 2.0
And update other deps
This commit is contained in:
parent
1921ad8615
commit
e55d03bd5e
30 changed files with 824 additions and 25 deletions
3
Makefile
3
Makefile
|
@ -1,2 +1,5 @@
|
||||||
test:
|
test:
|
||||||
py.test
|
py.test
|
||||||
|
|
||||||
|
develop:
|
||||||
|
python setup.py develop
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
Django==1.11
|
Django==2.0.5
|
||||||
django-compressor==2.1.1
|
django-compressor==2.1.1
|
||||||
django-leaflet-storage==0.8.2
|
Pillow==5.1.0
|
||||||
Pillow==4.1.0
|
psycopg2==2.7.4
|
||||||
psycopg2==2.7.1
|
requests==2.18.4
|
||||||
requests==2.13.0
|
social-auth-app-django==2.1.0
|
||||||
social-auth-app-django==1.1.0
|
|
||||||
social-auth-core==1.7.0
|
social-auth-core==1.7.0
|
||||||
|
|
13
umap/admin.py
Normal file
13
umap/admin.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from django.contrib.gis import admin
|
||||||
|
from .models import Map, DataLayer, Pictogram, TileLayer, Licence
|
||||||
|
|
||||||
|
|
||||||
|
class TileLayerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'rank', )
|
||||||
|
list_editable = ('rank', )
|
||||||
|
|
||||||
|
admin.site.register(Map, admin.OSMGeoAdmin)
|
||||||
|
admin.site.register(DataLayer)
|
||||||
|
admin.site.register(Pictogram)
|
||||||
|
admin.site.register(TileLayer, TileLayerAdmin)
|
||||||
|
admin.site.register(Licence)
|
57
umap/decorators.py
Normal file
57
umap/decorators.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.http import HttpResponseForbidden
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .views import simple_json_response
|
||||||
|
from .models import Map
|
||||||
|
|
||||||
|
|
||||||
|
LOGIN_URL = getattr(settings, "LOGIN_URL", "login")
|
||||||
|
LOGIN_URL = (reverse_lazy(LOGIN_URL) if not LOGIN_URL.startswith("/")
|
||||||
|
else LOGIN_URL)
|
||||||
|
|
||||||
|
|
||||||
|
def login_required_if_not_anonymous_allowed(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
if (not getattr(settings, "LEAFLET_STORAGE_ALLOW_ANONYMOUS", False)
|
||||||
|
and not request.user.is_authenticated):
|
||||||
|
return simple_json_response(login_required=str(LOGIN_URL))
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def map_permissions_check(view_func):
|
||||||
|
"""
|
||||||
|
Used for URLs dealing with the map.
|
||||||
|
"""
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
map_inst = get_object_or_404(Map, pk=kwargs['map_id'])
|
||||||
|
user = request.user
|
||||||
|
kwargs['map_inst'] = map_inst # Avoid rerequesting the map in the view
|
||||||
|
if map_inst.edit_status >= map_inst.EDITORS:
|
||||||
|
can_edit = map_inst.can_edit(user=user, request=request)
|
||||||
|
if not can_edit:
|
||||||
|
if map_inst.owner and not user.is_authenticated:
|
||||||
|
return simple_json_response(login_required=str(LOGIN_URL))
|
||||||
|
else:
|
||||||
|
return HttpResponseForbidden('Action not allowed for user.')
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def jsonize_view(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
response = view_func(request, *args, **kwargs)
|
||||||
|
response_kwargs = {}
|
||||||
|
if hasattr(response, 'rendered_content'):
|
||||||
|
response_kwargs['html'] = response.rendered_content
|
||||||
|
if response.has_header('location'):
|
||||||
|
response_kwargs['redirect'] = response['location']
|
||||||
|
return simple_json_response(**response_kwargs)
|
||||||
|
return wrapper
|
33
umap/fields.py
Normal file
33
umap/fields.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.utils import six
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.encoding import smart_text
|
||||||
|
|
||||||
|
|
||||||
|
class DictField(models.TextField):
|
||||||
|
"""
|
||||||
|
A very simple field to store JSON in db.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
if not value:
|
||||||
|
value = {}
|
||||||
|
if not isinstance(value, six.string_types):
|
||||||
|
value = json.dumps(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def from_db_value(self, value, expression, connection, context):
|
||||||
|
return self.to_python(value)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if not value:
|
||||||
|
value = {}
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
return json.loads(value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def value_to_string(self, obj):
|
||||||
|
"""Return value from object converted to string properly"""
|
||||||
|
return smart_text(self.get_prep_value(self.value_from_object(obj)))
|
90
umap/forms.py
Normal file
90
umap/forms.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
|
from django.conf import settings
|
||||||
|
from django.forms.utils import ErrorList
|
||||||
|
|
||||||
|
from .models import Map, DataLayer
|
||||||
|
|
||||||
|
DEFAULT_LATITUDE = settings.LEAFLET_LATITUDE if hasattr(settings, "LEAFLET_LATITUDE") else 51
|
||||||
|
DEFAULT_LONGITUDE = settings.LEAFLET_LONGITUDE if hasattr(settings, "LEAFLET_LONGITUDE") else 2
|
||||||
|
DEFAULT_CENTER = Point(DEFAULT_LONGITUDE, DEFAULT_LATITUDE)
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class FlatErrorList(ErrorList):
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.flat()
|
||||||
|
|
||||||
|
def flat(self):
|
||||||
|
if not self:
|
||||||
|
return u''
|
||||||
|
return u' — '.join([e for e in self])
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateMapPermissionsForm(forms.ModelForm):
|
||||||
|
owner = forms.ModelChoiceField(User.objects, required=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Map
|
||||||
|
fields = ('edit_status', 'editors', 'share_status', 'owner')
|
||||||
|
|
||||||
|
|
||||||
|
class AnonymousMapPermissionsForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(AnonymousMapPermissionsForm, self).__init__(*args, **kwargs)
|
||||||
|
full_secret_link = "%s%s" % (settings.SITE_URL, self.instance.get_anonymous_edit_url())
|
||||||
|
help_text = _('Secret edit link is %s') % full_secret_link
|
||||||
|
self.fields['edit_status'].help_text = _(help_text)
|
||||||
|
|
||||||
|
STATUS = (
|
||||||
|
(Map.ANONYMOUS, _('Everyone can edit')),
|
||||||
|
(Map.OWNER, _('Only editable with secret edit link'))
|
||||||
|
)
|
||||||
|
|
||||||
|
edit_status = forms.ChoiceField(choices=STATUS)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Map
|
||||||
|
fields = ('edit_status', )
|
||||||
|
|
||||||
|
|
||||||
|
class DataLayerForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = DataLayer
|
||||||
|
fields = ('geojson', 'name', 'display_on_load', 'rank')
|
||||||
|
|
||||||
|
|
||||||
|
class MapSettingsForm(forms.ModelForm):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(MapSettingsForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["slug"].required = False
|
||||||
|
|
||||||
|
def clean_slug(self):
|
||||||
|
slug = self.cleaned_data.get('slug', None)
|
||||||
|
name = self.cleaned_data.get('name', None)
|
||||||
|
if not slug and name:
|
||||||
|
# If name is empty, don't do nothing, validation will raise
|
||||||
|
# later on the process because name is required
|
||||||
|
self.cleaned_data['slug'] = slugify(name) or "map"
|
||||||
|
return self.cleaned_data['slug'][:50]
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def clean_center(self):
|
||||||
|
if not self.cleaned_data['center']:
|
||||||
|
point = DEFAULT_CENTER
|
||||||
|
self.cleaned_data['center'] = point
|
||||||
|
return self.cleaned_data['center']
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = ('settings', 'name', 'center', 'slug')
|
||||||
|
model = Map
|
28
umap/management/commands/anonymous_edit_url.py
Normal file
28
umap/management/commands/anonymous_edit_url.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from umap.models import Map
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = ('Retrieve anonymous edit url of a map. '
|
||||||
|
'Eg.: python manage.py anonymous_edit_url 1234')
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('pk', help='PK of the map to retrieve.')
|
||||||
|
|
||||||
|
def abort(self, msg):
|
||||||
|
self.stderr.write(msg)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
pk = options['pk']
|
||||||
|
try:
|
||||||
|
map_ = Map.objects.get(pk=pk)
|
||||||
|
except Map.DoesNotExist:
|
||||||
|
self.abort('Map with pk {} not found'.format(pk))
|
||||||
|
if map_.owner:
|
||||||
|
self.abort('Map is not anonymous (owner: {})'.format(map_.owner))
|
||||||
|
print(settings.SITE_URL + map_.get_anonymous_edit_url())
|
42
umap/management/commands/generate_js_locale.py
Normal file
42
umap/management/commands/generate_js_locale.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils.translation import to_locale
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.verbosity = options['verbosity']
|
||||||
|
for code, name in settings.LANGUAGES:
|
||||||
|
code = to_locale(code)
|
||||||
|
if self.verbosity > 0:
|
||||||
|
print("Processing", name)
|
||||||
|
path = finders.find('storage/src/locale/{code}.json'.format(
|
||||||
|
code=code))
|
||||||
|
if not path:
|
||||||
|
print("No file for", code, "Skipping")
|
||||||
|
else:
|
||||||
|
with io.open(path, "r", encoding="utf-8") as f:
|
||||||
|
if self.verbosity > 1:
|
||||||
|
print("Found file", path)
|
||||||
|
self.render(code, f.read())
|
||||||
|
|
||||||
|
def render(self, code, json):
|
||||||
|
path = os.path.join(
|
||||||
|
settings.STATIC_ROOT,
|
||||||
|
"storage/src/locale/",
|
||||||
|
"{code}.js".format(code=code)
|
||||||
|
)
|
||||||
|
with io.open(path, "w", encoding="utf-8") as f:
|
||||||
|
content = render_to_string('umap/locale.js', {
|
||||||
|
"locale": json,
|
||||||
|
"locale_code": code
|
||||||
|
})
|
||||||
|
if self.verbosity > 1:
|
||||||
|
print("Exporting to", path)
|
||||||
|
f.write(content)
|
|
@ -3,7 +3,7 @@ import os
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from leaflet_storage.models import Pictogram
|
from umap.models import Pictogram
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
8
umap/managers.py
Normal file
8
umap/managers.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.db.models import Manager
|
||||||
|
|
||||||
|
|
||||||
|
class PublicManager(Manager):
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super(PublicManager, self).get_queryset().filter(
|
||||||
|
share_status=self.model.PUBLIC)
|
356
umap/models.py
Normal file
356
umap/models.py
Normal file
|
@ -0,0 +1,356 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from django.contrib.gis.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.core.signing import Signer
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
|
from django.core.files.base import File
|
||||||
|
|
||||||
|
from .fields import DictField
|
||||||
|
from .managers import PublicManager
|
||||||
|
|
||||||
|
|
||||||
|
class NamedModel(models.Model):
|
||||||
|
name = models.CharField(max_length=200, verbose_name=_("name"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
ordering = ('name', )
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_licence():
|
||||||
|
"""
|
||||||
|
Returns a default Licence, creates it if it doesn't exist.
|
||||||
|
Needed to prevent a licence deletion from deleting all the linked
|
||||||
|
maps.
|
||||||
|
"""
|
||||||
|
return Licence.objects.get_or_create(
|
||||||
|
# can't use ugettext_lazy for database storage, see #13965
|
||||||
|
name=getattr(settings, "LEAFLET_STORAGE_DEFAULT_LICENCE_NAME",
|
||||||
|
'No licence set')
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
|
||||||
|
class Licence(NamedModel):
|
||||||
|
"""
|
||||||
|
The licence one map is published on.
|
||||||
|
"""
|
||||||
|
details = models.URLField(
|
||||||
|
verbose_name=_('details'),
|
||||||
|
help_text=_('Link to a page where the licence is detailed.')
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'url': self.details
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TileLayer(NamedModel):
|
||||||
|
url_template = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
help_text=_("URL template using OSM tile format")
|
||||||
|
)
|
||||||
|
minZoom = models.IntegerField(default=0)
|
||||||
|
maxZoom = models.IntegerField(default=18)
|
||||||
|
attribution = models.CharField(max_length=300)
|
||||||
|
rank = models.SmallIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text=_('Order of the tilelayers in the edit box')
|
||||||
|
)
|
||||||
|
# See https://wiki.openstreetmap.org/wiki/TMS#The_Y_coordinate
|
||||||
|
tms = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def json(self):
|
||||||
|
return dict((field.name, getattr(self, field.name))
|
||||||
|
for field in self._meta.fields)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default(cls):
|
||||||
|
"""
|
||||||
|
Returns the default tile layer (used for a map when no layer is set).
|
||||||
|
"""
|
||||||
|
return cls.objects.order_by('rank')[0] # FIXME, make it administrable
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_list(cls, selected=None):
|
||||||
|
l = []
|
||||||
|
for t in cls.objects.all():
|
||||||
|
fields = t.json
|
||||||
|
if selected and selected.pk == t.pk:
|
||||||
|
fields['selected'] = True
|
||||||
|
l.append(fields)
|
||||||
|
return l
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('rank', 'name', )
|
||||||
|
|
||||||
|
|
||||||
|
class Map(NamedModel):
|
||||||
|
"""
|
||||||
|
A single thematical map.
|
||||||
|
"""
|
||||||
|
ANONYMOUS = 1
|
||||||
|
EDITORS = 2
|
||||||
|
OWNER = 3
|
||||||
|
PUBLIC = 1
|
||||||
|
OPEN = 2
|
||||||
|
PRIVATE = 3
|
||||||
|
EDIT_STATUS = (
|
||||||
|
(ANONYMOUS, _('Everyone can edit')),
|
||||||
|
(EDITORS, _('Only editors can edit')),
|
||||||
|
(OWNER, _('Only owner can edit')),
|
||||||
|
)
|
||||||
|
SHARE_STATUS = (
|
||||||
|
(PUBLIC, _('everyone (public)')),
|
||||||
|
(OPEN, _('anyone with link')),
|
||||||
|
(PRIVATE, _('editors only')),
|
||||||
|
)
|
||||||
|
slug = models.SlugField(db_index=True)
|
||||||
|
description = models.TextField(blank=True, null=True, verbose_name=_("description"))
|
||||||
|
center = models.PointField(geography=True, verbose_name=_("center"))
|
||||||
|
zoom = models.IntegerField(default=7, verbose_name=_("zoom"))
|
||||||
|
locate = models.BooleanField(default=False, verbose_name=_("locate"), help_text=_("Locate user on load?"))
|
||||||
|
licence = models.ForeignKey(
|
||||||
|
Licence,
|
||||||
|
help_text=_("Choose the map licence."),
|
||||||
|
verbose_name=_('licence'),
|
||||||
|
on_delete=models.SET_DEFAULT,
|
||||||
|
default=get_default_licence
|
||||||
|
)
|
||||||
|
modified_at = models.DateTimeField(auto_now=True)
|
||||||
|
tilelayer = models.ForeignKey(TileLayer, blank=True, null=True, related_name="maps", verbose_name=_("background"), on_delete=models.PROTECT)
|
||||||
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name="owned_maps", verbose_name=_("owner"), on_delete=models.PROTECT)
|
||||||
|
editors = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, verbose_name=_("editors"))
|
||||||
|
edit_status = models.SmallIntegerField(choices=EDIT_STATUS, default=OWNER, verbose_name=_("edit status"))
|
||||||
|
share_status = models.SmallIntegerField(choices=SHARE_STATUS, default=PUBLIC, verbose_name=_("share status"))
|
||||||
|
settings = DictField(blank=True, null=True, verbose_name=_("settings"))
|
||||||
|
|
||||||
|
objects = models.Manager()
|
||||||
|
public = PublicManager()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("map", kwargs={'slug': self.slug or "map", 'pk': self.pk})
|
||||||
|
|
||||||
|
def get_anonymous_edit_url(self):
|
||||||
|
signer = Signer()
|
||||||
|
signature = signer.sign(self.pk)
|
||||||
|
return reverse('map_anonymous_edit_url', kwargs={'signature': signature})
|
||||||
|
|
||||||
|
def is_anonymous_owner(self, request):
|
||||||
|
if self.owner:
|
||||||
|
# edit cookies are only valid while map hasn't owner
|
||||||
|
return False
|
||||||
|
key, value = self.signed_cookie_elements
|
||||||
|
try:
|
||||||
|
has_anonymous_cookie = int(request.get_signed_cookie(key, False)) == value
|
||||||
|
except ValueError:
|
||||||
|
has_anonymous_cookie = False
|
||||||
|
return has_anonymous_cookie
|
||||||
|
|
||||||
|
def can_edit(self, user=None, request=None):
|
||||||
|
"""
|
||||||
|
Define if a user can edit or not the instance, according to his account
|
||||||
|
or the request.
|
||||||
|
"""
|
||||||
|
can = False
|
||||||
|
if request and not self.owner:
|
||||||
|
if (getattr(settings, "LEAFLET_STORAGE_ALLOW_ANONYMOUS", False)
|
||||||
|
and self.is_anonymous_owner(request)):
|
||||||
|
can = True
|
||||||
|
if user and user.is_authenticated:
|
||||||
|
# if user is authenticated, attach as owner
|
||||||
|
self.owner = user
|
||||||
|
self.save()
|
||||||
|
msg = _("Your anonymous map has been attached to your account %s" % user)
|
||||||
|
messages.info(request, msg)
|
||||||
|
if self.edit_status == self.ANONYMOUS:
|
||||||
|
can = True
|
||||||
|
elif not user.is_authenticated:
|
||||||
|
pass
|
||||||
|
elif user == self.owner:
|
||||||
|
can = True
|
||||||
|
elif self.edit_status == self.EDITORS and user in self.editors.all():
|
||||||
|
can = True
|
||||||
|
return can
|
||||||
|
|
||||||
|
def can_view(self, request):
|
||||||
|
if self.owner is None:
|
||||||
|
can = True
|
||||||
|
elif self.share_status in [self.PUBLIC, self.OPEN]:
|
||||||
|
can = True
|
||||||
|
elif request.user == self.owner:
|
||||||
|
can = True
|
||||||
|
else:
|
||||||
|
can = not (self.share_status == self.PRIVATE and request.user not in self.editors.all())
|
||||||
|
return can
|
||||||
|
|
||||||
|
@property
|
||||||
|
def signed_cookie_elements(self):
|
||||||
|
return ('anonymous_owner|%s' % self.pk, self.pk)
|
||||||
|
|
||||||
|
def get_tilelayer(self):
|
||||||
|
return self.tilelayer or TileLayer.get_default()
|
||||||
|
|
||||||
|
def clone(self, **kwargs):
|
||||||
|
new = self.__class__.objects.get(pk=self.pk)
|
||||||
|
new.pk = None
|
||||||
|
new.name = u"%s %s" % (_("Clone of"), self.name)
|
||||||
|
if "owner" in kwargs:
|
||||||
|
# can be None in case of anonymous cloning
|
||||||
|
new.owner = kwargs["owner"]
|
||||||
|
new.save()
|
||||||
|
for editor in self.editors.all():
|
||||||
|
new.editors.add(editor)
|
||||||
|
for datalayer in self.datalayer_set.all():
|
||||||
|
datalayer.clone(map_inst=new)
|
||||||
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
class Pictogram(NamedModel):
|
||||||
|
"""
|
||||||
|
An image added to an icon of the map.
|
||||||
|
"""
|
||||||
|
attribution = models.CharField(max_length=300)
|
||||||
|
pictogram = models.ImageField(upload_to="pictogram")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"id": self.pk,
|
||||||
|
"attribution": self.attribution,
|
||||||
|
"name": self.name,
|
||||||
|
"src": self.pictogram.url
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Must be out of Datalayer for Django migration to run, because of python 2
|
||||||
|
# serialize limitations.
|
||||||
|
def upload_to(instance, filename):
|
||||||
|
if instance.pk:
|
||||||
|
return instance.upload_to()
|
||||||
|
name = "%s.geojson" % slugify(instance.name)[:50] or "untitled"
|
||||||
|
return os.path.join(instance.storage_root(), name)
|
||||||
|
|
||||||
|
|
||||||
|
class DataLayer(NamedModel):
|
||||||
|
"""
|
||||||
|
Layer to store Features in.
|
||||||
|
"""
|
||||||
|
map = models.ForeignKey(Map, on_delete=models.CASCADE)
|
||||||
|
description = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_("description")
|
||||||
|
)
|
||||||
|
geojson = models.FileField(upload_to=upload_to, blank=True, null=True)
|
||||||
|
display_on_load = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("display on load"),
|
||||||
|
help_text=_("Display this layer on load.")
|
||||||
|
)
|
||||||
|
rank = models.SmallIntegerField(default=0)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('rank',)
|
||||||
|
|
||||||
|
def save(self, force_insert=False, force_update=False, **kwargs):
|
||||||
|
is_new = not bool(self.pk)
|
||||||
|
super(DataLayer, self).save(force_insert, force_update, **kwargs)
|
||||||
|
|
||||||
|
if is_new:
|
||||||
|
force_insert, force_update = False, True
|
||||||
|
filename = self.upload_to()
|
||||||
|
old_name = self.geojson.name
|
||||||
|
new_name = self.geojson.storage.save(filename, self.geojson)
|
||||||
|
self.geojson.storage.delete(old_name)
|
||||||
|
self.geojson.name = new_name
|
||||||
|
super(DataLayer, self).save(force_insert, force_update, **kwargs)
|
||||||
|
self.purge_old_versions()
|
||||||
|
|
||||||
|
def upload_to(self):
|
||||||
|
root = self.storage_root()
|
||||||
|
name = '%s_%s.geojson' % (self.pk, int(time.time() * 1000))
|
||||||
|
return os.path.join(root, name)
|
||||||
|
|
||||||
|
def storage_root(self):
|
||||||
|
path = ["datalayer", str(self.map.pk)[-1]]
|
||||||
|
if len(str(self.map.pk)) > 1:
|
||||||
|
path.append(str(self.map.pk)[-2])
|
||||||
|
path.append(str(self.map.pk))
|
||||||
|
return os.path.join(*path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metadata(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"id": self.pk,
|
||||||
|
"displayOnLoad": self.display_on_load
|
||||||
|
}
|
||||||
|
|
||||||
|
def clone(self, map_inst=None):
|
||||||
|
new = self.__class__.objects.get(pk=self.pk)
|
||||||
|
new.pk = None
|
||||||
|
if map_inst:
|
||||||
|
new.map = map_inst
|
||||||
|
new.geojson = File(new.geojson.file.file)
|
||||||
|
new.save()
|
||||||
|
return new
|
||||||
|
|
||||||
|
def is_valid_version(self, name):
|
||||||
|
return name.startswith('%s_' % self.pk) and name.endswith('.geojson')
|
||||||
|
|
||||||
|
def version_metadata(self, name):
|
||||||
|
els = name.split('.')[0].split('_')
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"at": els[1],
|
||||||
|
"size": self.geojson.storage.size(self.get_version_path(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_versions(self):
|
||||||
|
root = self.storage_root()
|
||||||
|
names = self.geojson.storage.listdir(root)[1]
|
||||||
|
names = [name for name in names if self.is_valid_version(name)]
|
||||||
|
names.sort(reverse=True) # Recent first.
|
||||||
|
return names
|
||||||
|
|
||||||
|
@property
|
||||||
|
def versions(self):
|
||||||
|
names = self.get_versions()
|
||||||
|
return [self.version_metadata(name) for name in names]
|
||||||
|
|
||||||
|
def get_version(self, name):
|
||||||
|
path = self.get_version_path(name)
|
||||||
|
with self.geojson.storage.open(path, 'r') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def get_version_path(self, name):
|
||||||
|
return '{root}/{name}'.format(root=self.storage_root(), name=name)
|
||||||
|
|
||||||
|
def purge_old_versions(self):
|
||||||
|
root = self.storage_root()
|
||||||
|
names = self.get_versions()[settings.LEAFLET_STORAGE_KEEP_VERSIONS:]
|
||||||
|
for name in names:
|
||||||
|
for ext in ['', '.gz']:
|
||||||
|
path = os.path.join(root, name + ext)
|
||||||
|
try:
|
||||||
|
self.geojson.storage.delete(path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
|
@ -132,7 +132,7 @@ TEMPLATES = [
|
||||||
# Middleware
|
# Middleware
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
MIDDLEWARE = (
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
@ -141,6 +141,7 @@ MIDDLEWARE_CLASSES = (
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Auth / security
|
# Auth / security
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="map_list row">
|
<div class="map_list row">
|
||||||
{% if maps %}
|
{% if maps %}
|
||||||
{% include "leaflet_storage/map_list.html" %}
|
{% include "umap/map_list.html" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div>
|
<div>
|
||||||
{% blocktrans %}{{ current_user }} has no maps.{% endblocktrans %}
|
{% blocktrans %}{{ current_user }} has no maps.{% endblocktrans %}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% load leaflet_storage_tags compress i18n %}
|
{% load umap_tags compress i18n %}
|
||||||
|
|
||||||
{% block body_class %}content{% endblock %}
|
{% block body_class %}content{% endblock %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
{% compress css %}
|
{% compress css %}
|
||||||
{% leaflet_storage_css %}
|
{% umap_css %}
|
||||||
{% endcompress css %}
|
{% endcompress css %}
|
||||||
{% leaflet_storage_js %}
|
{% umap_js %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
|
|
10
umap/templates/umap/css.html
Normal file
10
umap/templates/umap/css.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_URL }}storage/reqs/leaflet/leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_URL }}storage/reqs/markercluster/MarkerCluster.css" />
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_URL }}storage/reqs/markercluster/MarkerCluster.Default.css" />
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_URL }}storage/reqs/editinosm/Leaflet.EditInOSM.css" />
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_URL }}storage/reqs/minimap/Control.MiniMap.css" />
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_URL }}storage/reqs/contextmenu/leaflet.contextmenu.css" />
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_URL }}storage/reqs/toolbar/leaflet.toolbar.css" />
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_URL }}storage/reqs/measurable/Leaflet.Measurable.css" />
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_URL }}storage/reqs/fullscreen/leaflet.fullscreen.css" />
|
||||||
|
<link rel="stylesheet" href="{{ STATIC_URL }}storage/src/css/storage.css" />
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "umap/content.html" %}
|
{% extends "umap/content.html" %}
|
||||||
|
|
||||||
{% load leaflet_storage_tags i18n %}
|
{% load umap_tags i18n %}
|
||||||
|
|
||||||
{% block maincontent %}
|
{% block maincontent %}
|
||||||
{% if DEMO_SITE %}
|
{% if DEMO_SITE %}
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<h2 class="section">{% blocktrans %}Get inspired, browse maps{% endblocktrans %}</h2>
|
<h2 class="section">{% blocktrans %}Get inspired, browse maps{% endblocktrans %}</h2>
|
||||||
<div class="map_list row">
|
<div class="map_list row">
|
||||||
{% include "leaflet_storage/map_list.html" %}
|
{% include "umap/map_list.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
19
umap/templates/umap/login_popup_end.html
Normal file
19
umap/templates/umap/login_popup_end.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<h3>{% trans "You are logged in. Continuing..." %}</h3>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function proceed()
|
||||||
|
{
|
||||||
|
if (window.opener) {
|
||||||
|
console.log(window.opener);
|
||||||
|
window.opener.storage_proceed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proceed();
|
||||||
|
|
||||||
|
// To handle errors, this template should be integrated into your authentication error message page
|
||||||
|
// Note that you can call any window.opener function like window.opener.func
|
||||||
|
|
||||||
|
</script>
|
|
@ -1,13 +1,13 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load leaflet_storage_tags compress i18n %}
|
{% load umap_tags compress i18n %}
|
||||||
{% block head_title %}{{ map.name }} - {{ SITE_NAME }}{% endblock %}
|
{% block head_title %}{{ map.name }} - {{ SITE_NAME }}{% endblock %}
|
||||||
{% block body_class %}map_detail{% endblock %}
|
{% block body_class %}map_detail{% endblock %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
{% compress css %}
|
{% compress css %}
|
||||||
{% leaflet_storage_css %}
|
{% umap_css %}
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
{% leaflet_storage_js locale=locale %}
|
{% umap_js locale=locale %}
|
||||||
{% if object.share_status != object.PUBLIC %}
|
{% if object.share_status != object.PUBLIC %}
|
||||||
<meta name="robots" content="noindex">
|
<meta name="robots" content="noindex">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -15,9 +15,9 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% block map_init %}
|
{% block map_init %}
|
||||||
{% include "leaflet_storage/map_init.html" %}
|
{% include "umap/map_init.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% include "leaflet_storage/map_messages.html" %}
|
{% include "umap/map_messages.html" %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
MAP.ui.on('panel:ready', function () {
|
MAP.ui.on('panel:ready', function () {
|
||||||
L.S.AutoComplete.multiSelect('id_editors', {
|
L.S.AutoComplete.multiSelect('id_editors', {
|
5
umap/templates/umap/map_fragment.html
Normal file
5
umap/templates/umap/map_fragment.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% load umap_tags %}
|
||||||
|
<div id="{{ prefix }}{{ map.pk }}" class="map_fragment"></div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
new L.Storage.Map("{{ prefix }}{{ map.pk }}", {{ map_settings|notag|safe }});
|
||||||
|
</script>
|
5
umap/templates/umap/map_init.html
Normal file
5
umap/templates/umap/map_init.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% load umap_tags %}
|
||||||
|
<div id="map"></div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var MAP = new L.Storage.Map("map", {{ map_settings|notag|safe }});
|
||||||
|
</script>
|
|
@ -1,4 +1,4 @@
|
||||||
{% load leaflet_storage_tags umap_tags i18n %}
|
{% load umap_tags umap_tags i18n %}
|
||||||
|
|
||||||
{% for map_inst in maps %}
|
{% for map_inst in maps %}
|
||||||
<hr />
|
<hr />
|
11
umap/templates/umap/map_messages.html
Normal file
11
umap/templates/umap/map_messages.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<script type="text/javascript">
|
||||||
|
{% for m in messages %}
|
||||||
|
"loooping"
|
||||||
|
{# We have just one, but we need to loop, as for messages API #}
|
||||||
|
L.Storage.fire('ui:alert', {
|
||||||
|
content: "{{ m }}",
|
||||||
|
level: "{{ m.tags }}",
|
||||||
|
duration: 100000
|
||||||
|
});
|
||||||
|
{% endfor %}
|
||||||
|
</script>
|
7
umap/templates/umap/map_update_permissions.html
Normal file
7
umap/templates/umap/map_update_permissions.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<h3>{% trans "Map permissions" %}</h3>
|
||||||
|
<form action="{% url 'map_update_permissions' map.pk %}" method="post" id="map_edit">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" />
|
||||||
|
</form>
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="map_list row">
|
<div class="map_list row">
|
||||||
{% if maps %}
|
{% if maps %}
|
||||||
{% include "leaflet_storage/map_list.html" with prefix='search_map_' %}
|
{% include "umap/map_list.html" with prefix='search_map_' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "Not map found." %}
|
{% trans "Not map found." %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
1
umap/templates/umap/success.html
Normal file
1
umap/templates/umap/success.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ok
|
|
@ -3,7 +3,7 @@ import socket
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user, get_user_model
|
from django.contrib.auth import get_user, get_user_model
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.test import RequestFactory
|
from django.test import RequestFactory
|
||||||
|
|
||||||
from umap.views import validate_url
|
from umap.views import validate_url
|
||||||
|
@ -105,4 +105,4 @@ def test_can_login_with_username_and_password_if_enabled(client, settings):
|
||||||
user.save()
|
user.save()
|
||||||
client.post(reverse('login'), {'username': 'test', 'password': 'test'})
|
client.post(reverse('login'), {'username': 'test', 'password': 'test'})
|
||||||
user = get_user(client)
|
user = get_user(client)
|
||||||
assert user.is_authenticated()
|
assert user.is_authenticated
|
||||||
|
|
|
@ -14,7 +14,7 @@ from . import views
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', admin.site.urls),
|
||||||
url('', include('social_django.urls', namespace='social')),
|
url('', include('social_django.urls', namespace='social')),
|
||||||
url(r'^m/(?P<pk>\d+)/$', MapShortUrl.as_view(), name='umap_short_url'),
|
url(r'^m/(?P<pk>\d+)/$', MapShortUrl.as_view(), name='umap_short_url'),
|
||||||
url(r'^ajax-proxy/$', cache_page(180)(views.ajax_proxy),
|
url(r'^ajax-proxy/$', cache_page(180)(views.ajax_proxy),
|
||||||
|
|
111
umap/utils.py
Normal file
111
umap/utils.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import gzip
|
||||||
|
|
||||||
|
from django.urls import get_resolver
|
||||||
|
from django.urls import URLPattern, URLResolver
|
||||||
|
|
||||||
|
|
||||||
|
def get_uri_template(urlname, args=None, prefix=""):
|
||||||
|
'''
|
||||||
|
Utility function to return an URI Template from a named URL in django
|
||||||
|
Copied from django-digitalpaper.
|
||||||
|
|
||||||
|
Restrictions:
|
||||||
|
- Only supports named urls! i.e. url(... name="toto")
|
||||||
|
- Only support one namespace level
|
||||||
|
- Only returns the first URL possibility.
|
||||||
|
- Supports multiple pattern possibilities (i.e., patterns with
|
||||||
|
non-capturing parenthesis in them) by trying to find a pattern
|
||||||
|
whose optional parameters match those you specified (a parameter
|
||||||
|
is considered optional if it doesn't appear in every pattern possibility)
|
||||||
|
'''
|
||||||
|
def _convert(template, args=None):
|
||||||
|
"""URI template converter"""
|
||||||
|
if not args:
|
||||||
|
args = []
|
||||||
|
paths = template % dict([p, "{%s}" % p] for p in args)
|
||||||
|
return u'%s/%s' % (prefix, paths)
|
||||||
|
|
||||||
|
resolver = get_resolver(None)
|
||||||
|
parts = urlname.split(':')
|
||||||
|
if len(parts) > 1 and parts[0] in resolver.namespace_dict:
|
||||||
|
namespace = parts[0]
|
||||||
|
urlname = parts[1]
|
||||||
|
nprefix, resolver = resolver.namespace_dict[namespace]
|
||||||
|
prefix = prefix + '/' + nprefix.rstrip('/')
|
||||||
|
possibilities = resolver.reverse_dict.getlist(urlname)
|
||||||
|
for tmp in possibilities:
|
||||||
|
possibility, pattern = tmp[:2]
|
||||||
|
if not args:
|
||||||
|
# If not args are specified, we only consider the first pattern
|
||||||
|
# django gives us
|
||||||
|
result, params = possibility[0]
|
||||||
|
return _convert(result, params)
|
||||||
|
else:
|
||||||
|
# If there are optionnal arguments passed, use them to try to find
|
||||||
|
# the correct pattern.
|
||||||
|
# First, we need to build a list with all the arguments
|
||||||
|
seen_params = []
|
||||||
|
for result, params in possibility:
|
||||||
|
seen_params.append(params)
|
||||||
|
# Then build a set to find the common ones, and use it to build the
|
||||||
|
# list of all the expected params
|
||||||
|
common_params = reduce(lambda x, y: set(x) & set(y), seen_params)
|
||||||
|
expected_params = sorted(common_params.union(args))
|
||||||
|
# Then loop again over the pattern possibilities and return
|
||||||
|
# the first one that strictly match expected params
|
||||||
|
for result, params in possibility:
|
||||||
|
if sorted(params) == expected_params:
|
||||||
|
return _convert(result, params)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DecoratedURLPattern(URLPattern):
|
||||||
|
|
||||||
|
def resolve(self, *args, **kwargs):
|
||||||
|
result = URLPattern.resolve(self, *args, **kwargs)
|
||||||
|
if result:
|
||||||
|
for func in self._decorate_with:
|
||||||
|
result.func = func(result.func)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def decorated_patterns(func, *urls):
|
||||||
|
"""
|
||||||
|
Utility function to decorate a group of url in urls.py
|
||||||
|
|
||||||
|
Taken from http://djangosnippets.org/snippets/532/ + comments
|
||||||
|
See also http://friendpaste.com/6afByRiBB9CMwPft3a6lym
|
||||||
|
|
||||||
|
Example:
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^language/(?P<lang_code>[a-z]+)$', views.MyView, name='name'),
|
||||||
|
] + decorated_patterns(login_required, url(r'^', include('cms.urls')),
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorate(urls, func):
|
||||||
|
for url in urls:
|
||||||
|
if isinstance(url, URLPattern):
|
||||||
|
url.__class__ = DecoratedURLPattern
|
||||||
|
if not hasattr(url, "_decorate_with"):
|
||||||
|
setattr(url, "_decorate_with", [])
|
||||||
|
url._decorate_with.append(func)
|
||||||
|
elif isinstance(url, URLResolver):
|
||||||
|
for pp in url.url_patterns:
|
||||||
|
if isinstance(pp, URLPattern):
|
||||||
|
pp.__class__ = DecoratedURLPattern
|
||||||
|
if not hasattr(pp, "_decorate_with"):
|
||||||
|
setattr(pp, "_decorate_with", [])
|
||||||
|
pp._decorate_with.append(func)
|
||||||
|
if func:
|
||||||
|
if not isinstance(func, (list, tuple)):
|
||||||
|
func = [func]
|
||||||
|
for f in func:
|
||||||
|
decorate(urls, f)
|
||||||
|
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
|
def gzip_file(from_path, to_path):
|
||||||
|
with open(from_path, 'rb') as f_in:
|
||||||
|
with gzip.open(to_path, 'wb') as f_out:
|
||||||
|
f_out.writelines(f_in)
|
|
@ -22,7 +22,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest
|
from django.http import HttpResponse, HttpResponseBadRequest
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.encoding import smart_bytes
|
from django.utils.encoding import smart_bytes
|
||||||
from django.core.urlresolvers import reverse
|
from django.urls import reverse
|
||||||
from django.core.validators import URLValidator, ValidationError
|
from django.core.validators import URLValidator, ValidationError
|
||||||
|
|
||||||
from leaflet_storage.models import Map
|
from leaflet_storage.models import Map
|
||||||
|
|
Loading…
Reference in a new issue