#65 KML Import

This commit is contained in:
Leonhard Strohmidel 2022-10-16 10:14:04 +02:00
parent e60a6ea9be
commit 3982db1375
12 changed files with 240 additions and 8 deletions

View File

@ -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"

View File

@ -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
)

View File

@ -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

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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')
] ]

View File

@ -6,3 +6,4 @@ 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 *

View File

@ -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

View 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)

View File

@ -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,