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 @@
  • {% translate 'Create place' %}
  • {% translate 'All places' %}
  • + + {% if user.is_superuser %} +
  • {% translate 'Import KML File' %}
  • + {% 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' %}

    + + + {% 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 %} +
    +
    + {% translate 'Import Places from KML File' %} + {% csrf_token %} + +
    +
    + {% include 'partials/form/inputField.html' with field=upload_form.description %} +
    +
    + +
    +
    + {% include 'partials/form/inputField.html' with field=upload_form.map_file %} +
    +
    + + + {% translate 'Create' as action %} +
    + {% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER action=action %} +
    +
    + +
    +{% 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,