#65 KML Import
This commit is contained in:
		@@ -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
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        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_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>
 | 
			
		||||
            </nav>
 | 
			
		||||
        </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 %}
 | 
			
		||||
		</section>
 | 
			
		||||
		
 | 
			
		||||
		{{votingplace.vote}}
 | 
			
		||||
		<section class="LP-Section">
 | 
			
		||||
			{% include '../partials/voting.html' with voting=placevoting %}
 | 
			
		||||
		</section>
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,9 @@ from lostplaces.views import (
 | 
			
		||||
    ExplorerProfileView,
 | 
			
		||||
    ExplorerProfileUpdateView,
 | 
			
		||||
    ExplorerDraftsView,
 | 
			
		||||
    PlaceVoteView
 | 
			
		||||
    PlaceVoteView,
 | 
			
		||||
    UploadMapFileView,
 | 
			
		||||
    ImportDetailView
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
@@ -54,5 +56,8 @@ urlpatterns = [
 | 
			
		||||
    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/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.place_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)
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.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,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user