#65 KML Import
This commit is contained in:
parent
e60a6ea9be
commit
3982db1375
1
Pipfile
1
Pipfile
@ -22,6 +22,7 @@ easy-thumbnails = "*"
|
|||||||
image = "*"
|
image = "*"
|
||||||
django-widget-tweaks = "*"
|
django-widget-tweaks = "*"
|
||||||
django-taggit = "*"
|
django-taggit = "*"
|
||||||
|
pykml = "*"
|
||||||
|
|
||||||
[scripts]
|
[scripts]
|
||||||
test = "django_lostplaces/manage.py test lostplaces"
|
test = "django_lostplaces/manage.py test lostplaces"
|
||||||
|
@ -127,3 +127,9 @@ class TagSubmitForm(forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=forms.TextInput(attrs={'autocomplete':'off'})
|
widget=forms.TextInput(attrs={'autocomplete':'off'})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class UploadMapFileForm(forms.Form):
|
||||||
|
map_file = forms.FileField()
|
||||||
|
description = forms.CharField(
|
||||||
|
widget=forms.Textarea
|
||||||
|
)
|
||||||
|
@ -27,9 +27,39 @@ PLACE_MODES = (
|
|||||||
('live', 'live'),
|
('live', 'live'),
|
||||||
('draft', 'draft'),
|
('draft', 'draft'),
|
||||||
('review', 'review'),
|
('review', 'review'),
|
||||||
('archive', 'archive')
|
('archive', 'archive'),
|
||||||
|
('imported', 'imported')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PLACE_IMPORT_TYPES = (
|
||||||
|
('kml', 'KML-File import'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class PlaceImport(models.Model):
|
||||||
|
imported_when = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name=_('When the imported has taken place')
|
||||||
|
)
|
||||||
|
|
||||||
|
description = models.TextField(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('Description of the import')
|
||||||
|
)
|
||||||
|
|
||||||
|
explorer = models.ForeignKey(
|
||||||
|
'Explorer',
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='place_imports'
|
||||||
|
)
|
||||||
|
|
||||||
|
import_type = models.TextField(
|
||||||
|
default='kml',
|
||||||
|
choices=PLACE_IMPORT_TYPES,
|
||||||
|
verbose_name=_('What kind of import this is')
|
||||||
|
)
|
||||||
|
|
||||||
class Place(Submittable, Taggable, Mapable):
|
class Place(Submittable, Taggable, Mapable):
|
||||||
"""
|
"""
|
||||||
Place defines a lost place (location, name, description etc.).
|
Place defines a lost place (location, name, description etc.).
|
||||||
@ -63,6 +93,16 @@ class Place(Submittable, Taggable, Mapable):
|
|||||||
verbose_name=_('Mode of Place Editing')
|
verbose_name=_('Mode of Place Editing')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
place_import = models.ForeignKey(
|
||||||
|
PlaceImport,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='place_list'
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_imported(self):
|
||||||
|
return self.place_import != None
|
||||||
|
|
||||||
def get_hero_image(self):
|
def get_hero_image(self):
|
||||||
if self.hero:
|
if self.hero:
|
||||||
return self.hero
|
return self.hero
|
||||||
@ -239,4 +279,4 @@ class PlaceVoting(PlaceAsset):
|
|||||||
return PLACE_LEVELS[self.vote - 1][1]
|
return PLACE_LEVELS[self.vote - 1][1]
|
||||||
|
|
||||||
def get_all_choices(self):
|
def get_all_choices(self):
|
||||||
return reversed(PLACE_LEVELS)
|
return reversed(PLACE_LEVELS)
|
||||||
|
@ -59,6 +59,10 @@
|
|||||||
|
|
||||||
<li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_create'%}" class="LP-Link"><span class="LP-Link__Text">{% translate 'Create place' %}</span></a></li>
|
<li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_create'%}" class="LP-Link"><span class="LP-Link__Text">{% translate 'Create place' %}</span></a></li>
|
||||||
<li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_list'%}" class="LP-Link"><span class="LP-Link__Text">{% translate 'All places' %}</span></a></li>
|
<li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_list'%}" class="LP-Link"><span class="LP-Link__Text">{% translate 'All places' %}</span></a></li>
|
||||||
|
|
||||||
|
{% if user.is_superuser %}
|
||||||
|
<li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'import_upload'%}" class="LP-Link"><span class="LP-Link__Text">{% translate 'Import KML File' %}</span></a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
{% extends 'global.html'%}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% load svg_icon %}
|
||||||
|
|
||||||
|
{% block maincontent %}
|
||||||
|
<h1 class="LP-Headline">
|
||||||
|
{{ import_type}} from {{import.imported_when|date:"d F Y"}} by {{import.explorer.user.username}}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="SP-Paragraph">
|
||||||
|
{{import.place_list.all|length}} places where import in this import
|
||||||
|
</p>
|
||||||
|
<p class="SP-Paragraph">
|
||||||
|
{{import.description}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="LP-PlaceList">
|
||||||
|
<h1 class="LP-Headline">{% translate 'Places imported' %}</h1>
|
||||||
|
<ul class="LP-PlaceList__List">
|
||||||
|
{% for place in paginated_places %}
|
||||||
|
<li class="LP-PlaceList__Item">
|
||||||
|
{% include 'partials/place_teaser.html' with place=place extended=True %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% include 'partials/nav/pagination.html' with page_obj=paginated_places is_paginated=True %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock maincontent %}
|
@ -0,0 +1,33 @@
|
|||||||
|
{% extends 'global.html'%}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% load svg_icon %}
|
||||||
|
|
||||||
|
{% block maincontent %}
|
||||||
|
<form class="LP-Form" method="POST" enctype="multipart/form-data">
|
||||||
|
<fieldset class="LP-Form__Fieldset">
|
||||||
|
<legend class="LP-Form__Legend">{% translate 'Import Places from KML File' %}</legend>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="LP-Form__Composition LP-Form__Composition--breakable">
|
||||||
|
<div class="LP-Form__Field">
|
||||||
|
{% include 'partials/form/inputField.html' with field=upload_form.description %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="LP-Form__Composition LP-Form__Composition--breakable">
|
||||||
|
<div class="LP-Form__Field">
|
||||||
|
{% include 'partials/form/inputField.html' with field=upload_form.map_file %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% translate 'Create' as action %}
|
||||||
|
<div class="LP-Form__Composition LP-Form__Composition--buttons">
|
||||||
|
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER action=action %}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{% endblock maincontent %}
|
@ -49,7 +49,6 @@
|
|||||||
{% include '../partials/tagging.html' with config=tagging_config %}
|
{% include '../partials/tagging.html' with config=tagging_config %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{votingplace.vote}}
|
|
||||||
<section class="LP-Section">
|
<section class="LP-Section">
|
||||||
{% include '../partials/voting.html' with voting=placevoting %}
|
{% include '../partials/voting.html' with voting=placevoting %}
|
||||||
</section>
|
</section>
|
||||||
|
@ -24,7 +24,9 @@ from lostplaces.views import (
|
|||||||
ExplorerProfileView,
|
ExplorerProfileView,
|
||||||
ExplorerProfileUpdateView,
|
ExplorerProfileUpdateView,
|
||||||
ExplorerDraftsView,
|
ExplorerDraftsView,
|
||||||
PlaceVoteView
|
PlaceVoteView,
|
||||||
|
UploadMapFileView,
|
||||||
|
ImportDetailView
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -54,5 +56,8 @@ urlpatterns = [
|
|||||||
path('place/vote/<int:place_id>/<int:vote>', PlaceVoteView.as_view(), name='place_vote'),
|
path('place/vote/<int:place_id>/<int:vote>', PlaceVoteView.as_view(), name='place_vote'),
|
||||||
|
|
||||||
path('photo_album/create/<int:place_id>/', PhotoAlbumCreateView.as_view(), name='photo_album_create'),
|
path('photo_album/create/<int:place_id>/', PhotoAlbumCreateView.as_view(), name='photo_album_create'),
|
||||||
path('photo_album/delete/<int:pk>/', PhotoAlbumDeleteView.as_view(), name='photo_album_delete')
|
path('photo_album/delete/<int:pk>/', PhotoAlbumDeleteView.as_view(), name='photo_album_delete'),
|
||||||
|
|
||||||
|
path('import/upload', UploadMapFileView.as_view(), name='import_upload'),
|
||||||
|
path('import/<int:pk>', ImportDetailView.as_view(), name='import_detail')
|
||||||
]
|
]
|
||||||
|
@ -5,4 +5,5 @@ from lostplaces.views.base_views import *
|
|||||||
from lostplaces.views.views import *
|
from lostplaces.views.views import *
|
||||||
from lostplaces.views.place_views import *
|
from lostplaces.views.place_views import *
|
||||||
from lostplaces.views.place_image_views import *
|
from lostplaces.views.place_image_views import *
|
||||||
from lostplaces.views.explorer_views import *
|
from lostplaces.views.explorer_views import *
|
||||||
|
from lostplaces.views.imports import *
|
@ -31,6 +31,21 @@ class IsAuthenticatedMixin(LoginRequiredMixin, View):
|
|||||||
messages.error(self.request, self.permission_denied_message)
|
messages.error(self.request, self.permission_denied_message)
|
||||||
return super().handle_no_permission()
|
return super().handle_no_permission()
|
||||||
|
|
||||||
|
class IsSuperUserMixin(UserPassesTestMixin, View):
|
||||||
|
'''
|
||||||
|
A view mixin that checks if the user is a superuser.
|
||||||
|
Users who are not logged in or users who are no superuser get
|
||||||
|
a permission deined message.
|
||||||
|
'''
|
||||||
|
permission_denied_message = _('You are not allowed to see this page')
|
||||||
|
|
||||||
|
def test_func(self):
|
||||||
|
return self.request.user.is_superuser
|
||||||
|
|
||||||
|
def handle_no_permission(self):
|
||||||
|
messages.error(self.request, self.permission_denied_message)
|
||||||
|
return super().handle_no_permission()
|
||||||
|
|
||||||
class IsPlaceSubmitterMixin(UserPassesTestMixin, View):
|
class IsPlaceSubmitterMixin(UserPassesTestMixin, View):
|
||||||
'''
|
'''
|
||||||
A view mixin that checks wether a user is the submitter
|
A view mixin that checks wether a user is the submitter
|
||||||
|
91
django_lostplaces/lostplaces/views/imports.py
Normal file
91
django_lostplaces/lostplaces/views/imports.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from django.views import View
|
||||||
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
|
||||||
|
from pykml import parser as kml_parser
|
||||||
|
|
||||||
|
from lostplaces.forms import UploadMapFileForm
|
||||||
|
from lostplaces.models import Place, PlaceImport
|
||||||
|
from lostplaces.views.base_views import (
|
||||||
|
IsSuperUserMixin
|
||||||
|
)
|
||||||
|
|
||||||
|
class UploadMapFileView(IsSuperUserMixin, View):
|
||||||
|
permission_denied_message = _('You are not allowed to import any places')
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
upload_form = UploadMapFileForm()
|
||||||
|
return render(request, 'import/upload_map_file.html', {'upload_form': upload_form})
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
upload_form = UploadMapFileForm(request.POST, request.FILES)
|
||||||
|
explorer = request.user.explorer
|
||||||
|
|
||||||
|
if upload_form.is_valid():
|
||||||
|
map_file = upload_form.cleaned_data['map_file']
|
||||||
|
parsed_kml = kml_parser.fromstring(map_file.read())
|
||||||
|
|
||||||
|
place_import = PlaceImport.objects.create(
|
||||||
|
explorer=request.user.explorer,
|
||||||
|
description=upload_form.cleaned_data['description']
|
||||||
|
)
|
||||||
|
|
||||||
|
for folder in parsed_kml.Document.Folder:
|
||||||
|
for place_kml in folder.Placemark:
|
||||||
|
lat_long = self.get_lat_long(place_kml)
|
||||||
|
|
||||||
|
name = str(place_kml.name) if hasattr(place_kml, 'name') else ''
|
||||||
|
description = str(place_kml.description) if hasattr(place_kml, 'description') else ''
|
||||||
|
|
||||||
|
place_model = Place.objects.create(
|
||||||
|
name=name.strip(),
|
||||||
|
latitude=lat_long[0],
|
||||||
|
longitude=lat_long[1],
|
||||||
|
description=description.strip(),
|
||||||
|
place_import=place_import,
|
||||||
|
mode='imported'
|
||||||
|
)
|
||||||
|
|
||||||
|
place_import.save()
|
||||||
|
|
||||||
|
return redirect(reverse('lostplaces_home'))
|
||||||
|
|
||||||
|
def get_lat_long(self, place_kml):
|
||||||
|
if hasattr(place_kml, 'Point') and len(place_kml.Point.coordinates) >= 1:
|
||||||
|
coordinates = str(place_kml.Point.coordinates[0]).strip()
|
||||||
|
splited = coordinates.split(',')
|
||||||
|
latitude = 0
|
||||||
|
longitude = 0
|
||||||
|
|
||||||
|
if len(splited) >= 1:
|
||||||
|
longitude = splited[0]
|
||||||
|
if len(splited) >= 2:
|
||||||
|
latitude = splited[1]
|
||||||
|
|
||||||
|
return (latitude, longitude)
|
||||||
|
else:
|
||||||
|
return (0, 0)
|
||||||
|
|
||||||
|
class ImportDetailView(IsSuperUserMixin, View):
|
||||||
|
permission_denied_message = _('You are not allowed to see this import\'s details')
|
||||||
|
|
||||||
|
def get_import(self):
|
||||||
|
return get_object_or_404(PlaceImport, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
place_import = self.get_import()
|
||||||
|
|
||||||
|
place_paginator = Paginator(place_import.place_list.all(), 18)
|
||||||
|
paginated_places = place_paginator.get_page(
|
||||||
|
request.GET.get('page')
|
||||||
|
)
|
||||||
|
context = {
|
||||||
|
'import': place_import,
|
||||||
|
'paginated_places': paginated_places,
|
||||||
|
'import_type': place_import.get_import_type_display()
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'import/import_detail_view.html', context)
|
||||||
|
|
@ -12,7 +12,6 @@ from django.contrib import messages
|
|||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
|
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.urls import reverse_lazy, reverse
|
from django.urls import reverse_lazy, reverse
|
||||||
@ -37,7 +36,7 @@ from lostplaces.common import redirect_referer_or
|
|||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
|
||||||
class PlaceListView(IsAuthenticatedMixin, LevelCapPlaceListView):
|
class PlaceListView(IsAuthenticatedMixin, LevelCapPlaceListView):
|
||||||
paginate_by = 5
|
paginate_by = 18
|
||||||
template_name = 'place/place_list.html'
|
template_name = 'place/place_list.html'
|
||||||
ordering = [Lower('name')]
|
ordering = [Lower('name')]
|
||||||
|
|
||||||
@ -65,6 +64,11 @@ class PlaceDetailView(IsAuthenticatedMixin, IsEligibleToSeePlaceMixin, View):
|
|||||||
self.request,
|
self.request,
|
||||||
_('This place is still in draft mode and only visible to the submitter and superusers')
|
_('This place is still in draft mode and only visible to the submitter and superusers')
|
||||||
)
|
)
|
||||||
|
elif place.mode == 'imported':
|
||||||
|
messages.info(
|
||||||
|
self.request,
|
||||||
|
_('This place was imported and not reviewed & correted yet. This place is visbible for superusers only and does not appear in the list views.')
|
||||||
|
)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'place': place,
|
'place': place,
|
||||||
|
Loading…
Reference in New Issue
Block a user