diff --git a/Pipfile b/Pipfile
index a828b33..2e75ede 100644
--- a/Pipfile
+++ b/Pipfile
@@ -22,6 +22,7 @@ easy-thumbnails = "*"
image = "*"
django-widget-tweaks = "*"
django-taggit = "*"
+pykml = "*"
[scripts]
test = "django_lostplaces/manage.py test lostplaces"
diff --git a/django_lostplaces/lostplaces/forms.py b/django_lostplaces/lostplaces/forms.py
index baa2b11..9c56906 100644
--- a/django_lostplaces/lostplaces/forms.py
+++ b/django_lostplaces/lostplaces/forms.py
@@ -127,3 +127,9 @@ class TagSubmitForm(forms.Form):
required=False,
widget=forms.TextInput(attrs={'autocomplete':'off'})
)
+
+class UploadMapFileForm(forms.Form):
+ map_file = forms.FileField()
+ description = forms.CharField(
+ widget=forms.Textarea
+ )
diff --git a/django_lostplaces/lostplaces/models/place.py b/django_lostplaces/lostplaces/models/place.py
index f4aa7f9..705bd2c 100644
--- a/django_lostplaces/lostplaces/models/place.py
+++ b/django_lostplaces/lostplaces/models/place.py
@@ -27,9 +27,39 @@ PLACE_MODES = (
('live', 'live'),
('draft', 'draft'),
('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):
"""
Place defines a lost place (location, name, description etc.).
@@ -63,6 +93,16 @@ class Place(Submittable, Taggable, Mapable):
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):
if self.hero:
return self.hero
@@ -239,4 +279,4 @@ class PlaceVoting(PlaceAsset):
return PLACE_LEVELS[self.vote - 1][1]
def get_all_choices(self):
- return reversed(PLACE_LEVELS)
\ No newline at end of file
+ return reversed(PLACE_LEVELS)
diff --git a/django_lostplaces/lostplaces/templates/global.html b/django_lostplaces/lostplaces/templates/global.html
index 78a207c..74ce821 100644
--- a/django_lostplaces/lostplaces/templates/global.html
+++ b/django_lostplaces/lostplaces/templates/global.html
@@ -59,6 +59,10 @@
+
+ {% if user.is_superuser %}
+
+ {% endif %}
diff --git a/django_lostplaces/lostplaces/templates/import/import_detail_view.html b/django_lostplaces/lostplaces/templates/import/import_detail_view.html
new file mode 100644
index 0000000..4a9f588
--- /dev/null
+++ b/django_lostplaces/lostplaces/templates/import/import_detail_view.html
@@ -0,0 +1,33 @@
+{% extends 'global.html'%}
+{% load static %}
+{% load i18n %}
+
+{% load svg_icon %}
+
+{% block maincontent %}
+
+ {{ import_type}} from {{import.imported_when|date:"d F Y"}} by {{import.explorer.user.username}}
+
+
+
+ {{import.place_list.all|length}} places where import in this import
+
+
+ {{import.description}}
+
+
+
+
{% translate 'Places imported' %}
+
+ {% for place in paginated_places %}
+ -
+ {% include 'partials/place_teaser.html' with place=place extended=True %}
+
+ {% endfor %}
+
+
+ {% include 'partials/nav/pagination.html' with page_obj=paginated_places is_paginated=True %}
+
+
+
+{% endblock maincontent %}
\ No newline at end of file
diff --git a/django_lostplaces/lostplaces/templates/import/upload_map_file.html b/django_lostplaces/lostplaces/templates/import/upload_map_file.html
new file mode 100644
index 0000000..20e546e
--- /dev/null
+++ b/django_lostplaces/lostplaces/templates/import/upload_map_file.html
@@ -0,0 +1,33 @@
+{% extends 'global.html'%}
+{% load static %}
+{% load i18n %}
+
+{% load svg_icon %}
+
+{% block maincontent %}
+
+{% endblock maincontent %}
\ No newline at end of file
diff --git a/django_lostplaces/lostplaces/templates/place/place_detail.html b/django_lostplaces/lostplaces/templates/place/place_detail.html
index 7a0beeb..02d3f8f 100644
--- a/django_lostplaces/lostplaces/templates/place/place_detail.html
+++ b/django_lostplaces/lostplaces/templates/place/place_detail.html
@@ -49,7 +49,6 @@
{% include '../partials/tagging.html' with config=tagging_config %}
- {{votingplace.vote}}
{% include '../partials/voting.html' with voting=placevoting %}
diff --git a/django_lostplaces/lostplaces/urls.py b/django_lostplaces/lostplaces/urls.py
index 6cc6f38..788a107 100644
--- a/django_lostplaces/lostplaces/urls.py
+++ b/django_lostplaces/lostplaces/urls.py
@@ -24,7 +24,9 @@ from lostplaces.views import (
ExplorerProfileView,
ExplorerProfileUpdateView,
ExplorerDraftsView,
- PlaceVoteView
+ PlaceVoteView,
+ UploadMapFileView,
+ ImportDetailView
)
urlpatterns = [
@@ -54,5 +56,8 @@ urlpatterns = [
path('place/vote//', PlaceVoteView.as_view(), name='place_vote'),
path('photo_album/create//', PhotoAlbumCreateView.as_view(), name='photo_album_create'),
- path('photo_album/delete//', PhotoAlbumDeleteView.as_view(), name='photo_album_delete')
+ path('photo_album/delete//', PhotoAlbumDeleteView.as_view(), name='photo_album_delete'),
+
+ path('import/upload', UploadMapFileView.as_view(), name='import_upload'),
+ path('import/', ImportDetailView.as_view(), name='import_detail')
]
diff --git a/django_lostplaces/lostplaces/views/__init__.py b/django_lostplaces/lostplaces/views/__init__.py
index 734abcd..6533390 100644
--- a/django_lostplaces/lostplaces/views/__init__.py
+++ b/django_lostplaces/lostplaces/views/__init__.py
@@ -5,4 +5,5 @@ from lostplaces.views.base_views import *
from lostplaces.views.views import *
from lostplaces.views.place_views import *
from lostplaces.views.place_image_views import *
-from lostplaces.views.explorer_views import *
\ No newline at end of file
+from lostplaces.views.explorer_views import *
+from lostplaces.views.imports import *
\ No newline at end of file
diff --git a/django_lostplaces/lostplaces/views/base_views.py b/django_lostplaces/lostplaces/views/base_views.py
index 7490b56..735d676 100644
--- a/django_lostplaces/lostplaces/views/base_views.py
+++ b/django_lostplaces/lostplaces/views/base_views.py
@@ -31,6 +31,21 @@ class IsAuthenticatedMixin(LoginRequiredMixin, View):
messages.error(self.request, self.permission_denied_message)
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):
'''
A view mixin that checks wether a user is the submitter
diff --git a/django_lostplaces/lostplaces/views/imports.py b/django_lostplaces/lostplaces/views/imports.py
new file mode 100644
index 0000000..99aabd1
--- /dev/null
+++ b/django_lostplaces/lostplaces/views/imports.py
@@ -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)
+
diff --git a/django_lostplaces/lostplaces/views/place_views.py b/django_lostplaces/lostplaces/views/place_views.py
index df11d12..2c37055 100644
--- a/django_lostplaces/lostplaces/views/place_views.py
+++ b/django_lostplaces/lostplaces/views/place_views.py
@@ -12,7 +12,6 @@ from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.utils.translation import gettext as _
from django.utils import timezone
-from django.utils.translation import gettext as _
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse_lazy, reverse
@@ -37,7 +36,7 @@ from lostplaces.common import redirect_referer_or
from taggit.models import Tag
class PlaceListView(IsAuthenticatedMixin, LevelCapPlaceListView):
- paginate_by = 5
+ paginate_by = 18
template_name = 'place/place_list.html'
ordering = [Lower('name')]
@@ -65,6 +64,11 @@ class PlaceDetailView(IsAuthenticatedMixin, IsEligibleToSeePlaceMixin, View):
self.request,
_('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 = {
'place': place,