4 Commits

Author SHA1 Message Date
Leonhard Strohmidel
3982db1375 #65 KML Import 2022-10-16 10:14:04 +02:00
Leonhard Strohmidel
e60a6ea9be Fixing warning when testing (unordered list in pagination) 2022-09-25 18:03:48 +02:00
Leonhard Strohmidel
bc0ace7bf3 #64 Testing drafts in differents views 2022-09-25 18:03:22 +02:00
Leonhard Strohmidel
df67bcf639 #64 explorer draft view 2022-09-25 18:02:59 +02:00
18 changed files with 394 additions and 18 deletions

View File

@@ -22,6 +22,7 @@ easy-thumbnails = "*"
image = "*"
django-widget-tweaks = "*"
django-taggit = "*"
pykml = "*"
[scripts]
test = "django_lostplaces/manage.py test lostplaces"

View File

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

View File

@@ -88,15 +88,15 @@ class Explorer(models.Model):
and the list views
'''
if self.user.is_superuser:
return Place.objects.filter(mode='live')
return Place.objects.filter(mode='live').order_by('submitted_when')
return Place.objects.filter(
return (Place.objects.filter(
level__lte=self.level,
mode='live'
) | Place.objects.filter(
submitted_by=self,
mode='live'
)
)).order_by('submitted_when')
def get_places_eligible_to_see(self):
if self.user.is_superuser:
@@ -110,6 +110,12 @@ class Explorer(models.Model):
place in self.get_places_eligible_to_see()
)
def get_drafts(self):
return Place.objects.filter(
submitted_by=self,
mode='draft'
)
def __str__(self):
return self.user.username

View File

@@ -27,7 +27,37 @@ 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):
@@ -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

View File

@@ -0,0 +1,28 @@
{% extends 'global.html'%}
{% load static %}
{% load i18n %}
{% load svg_icon %}
{% block maincontent %}
<section class="LP-Section">
<div class="LP-PlaceList">
<h1 class="LP-Headline">
{% if user.username == explorer.user.username %}
{% translate 'Your drafts' %}
{% else %}
{{explorer.user.username}}{% translate '\'s drafts' %}
{% endif %}
</h1>
<ul class="LP-PlaceList__List">
{% for place in place_list %}
<li class="LP-PlaceList__Item">
<a href="{% url 'place_detail' pk=place.pk %}" class="LP-Link">
{% include 'partials/place_teaser.html' with place=place extended=True %}
</a>
</li>
{% endfor %}
</ul>
</div>
</section>
{% endblock maincontent %}

View File

@@ -32,7 +32,8 @@
{% if user.is_authenticated %}
Hi {{ user.username }}!
<a class="LP-Link" href="{% url 'logout' %}"><span class="LP-Link__Text">{% translate 'Logout' %}</span></a> |
<a class="LP-Link" href="{% url 'explorer_profile' explorer_id=user.pk%}"><span class="LP-Link__Text">{% translate 'Profile' %}</span></a>
<a class="LP-Link" href="{% url 'explorer_profile' explorer_id=user.pk%}"><span class="LP-Link__Text">{% translate 'Profile' %}</span></a> |
<a class="LP-Link" href="{% url 'explorer_drafts' explorer_id=user.pk%}"><span class="LP-Link__Text">{% translate 'Drafts' %}</span></a>
{% if user.is_superuser %}
| <a class="LP-Link" href="{% url 'admin:index' %}" target="_blank"><span class="LP-Link__Text">{% translate 'Admin' %}</span></a>
{% endif %}
@@ -58,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>

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 %}
</section>
{{votingplace.vote}}
<section class="LP-Section">
{% include '../partials/voting.html' with voting=placevoting %}
</section>

View File

@@ -1,4 +1,4 @@
23238#!/usr/bin/env python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django.test import TestCase, RequestFactory, Client
@@ -7,6 +7,7 @@ from django.db import models
from django.contrib.auth.models import User, AnonymousUser
from django.contrib.messages.storage.fallback import FallbackStorage
from django.utils import timezone
from django.utils.translation import gettext as _
from django.shortcuts import render
from lostplaces.models import Place
@@ -46,10 +47,6 @@ class TestIsAuthenticated(ViewTestCase):
self.assertHttpRedirect(response, '?'.join([str(reverse_lazy('login')), 'next=/']))
self.assertTrue(len(messages) > 0)
self.assertTrue(
_('Please login to proceed') in response.content.decode(),
msg='Expecting a message to tell the user to login'
)
class TestIsPlaceSubmitterMixin(TestCase):

View File

@@ -84,6 +84,34 @@ class TestExplorerProfileView(GlobalTemplateTestCaseMixin, ViewTestCase):
msg='Expecting the latest place to be visible on the submitters profile page'
)
def test_draft_in_explorer_places(self):
user = User.objects.get(username='testpeter')
Place.objects.create(
name='Im the latest place in draft 3671',
submitted_when=timezone.now(),
submitted_by=user.explorer,
location='Test %d town' % 5,
latitude=50.5 + 5/10,
longitude=7.0 - 5/10,
description='This is just a test, do not worry %d' % 5,
level=3,
mode='draft'
)
self.client.login(username='testpeter', password='Develop123')
response = self.client.get(
reverse('explorer_profile', kwargs={
'explorer_id': user.explorer.id
})
)
self.assertHttpOK(response)
self.assertFalse(
'Im the latest place in draft 3671' in response.content.decode(),
msg='Expecting a place in draft mode to not show up on the submitters profile page'
)
def test_explorer_image(self):
user = User.objects.get(username='testpeter')
place = Place.objects.create(

View File

@@ -678,5 +678,62 @@ class PlaceDetailViewTestCase(TaggableViewTestCaseMixin, MapableViewTestCaseMixi
msg='Expecting the explorer to not be in the reverse list of visits after deleting visit'
)
def test_accessing_place_in_draft(self):
user = User.objects.get(username='testpeter')
place = Place.objects.create(
name='Im a own place in draft 387948',
submitted_when=timezone.now(),
submitted_by=user.explorer,
location='Test %d town' % 5,
latitude=50.5 + 5/10,
longitude=7.0 - 5/10,
description='This is just a test, do not worry %d' % 5,
level=3,
mode='draft'
)
self.client.login(username='testpeter', password='Develop123')
response = self.client.get(
reverse('place_detail', kwargs={
'pk': place.pk
})
)
self.assertHttpOK(response)
self.assertTrue(
'Im a own place in draft 387948' in response.content.decode(),
msg='Expecting the user to see his own place in draft mode'
)
self.client.login(username='blubberbernd', password='Develop123')
response = self.client.get(
reverse('place_detail', kwargs={
'pk': place.pk
})
)
self.assertHttpForbidden(response)
self.client.login(username='toor', password='Develop123')
response = self.client.get(
reverse('place_detail', kwargs={
'pk': place.pk
})
)
self.assertHttpOK(response)
self.assertTrue(
'Im a own place in draft 387948' in response.content.decode(),
msg='Expecting a superuser to see all places in draft mode'
)

View File

@@ -23,7 +23,10 @@ from lostplaces.views import (
PhotoAlbumDeleteView,
ExplorerProfileView,
ExplorerProfileUpdateView,
PlaceVoteView
ExplorerDraftsView,
PlaceVoteView,
UploadMapFileView,
ImportDetailView
)
urlpatterns = [
@@ -33,6 +36,7 @@ urlpatterns = [
path('explorer/<int:explorer_id>/', ExplorerProfileView.as_view(), name='explorer_profile'),
path('explorer/update/', ExplorerProfileUpdateView.as_view(), name='explorer_profile_update'),
path('explorer/<int:explorer_id>/drafts', ExplorerDraftsView.as_view(), name='explorer_drafts'),
path('place/', PlaceListView.as_view(), name='place_list'),
path('place/<int:pk>/', PlaceDetailView.as_view(), name='place_detail'),
@@ -52,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')
]

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ from django.utils.translation import gettext as _
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse_lazy
from django.contrib import messages
from django.contrib.auth.mixins import UserPassesTestMixin
from lostplaces.common import get_all_subclasses
from lostplaces.views.base_views import IsAuthenticatedMixin
@@ -18,7 +19,7 @@ from lostplaces.forms import ExplorerChangeForm, ExplorerUserChangeForm
class ExplorerProfileView(IsAuthenticatedMixin, View):
def get(self, request, explorer_id):
explorer = get_object_or_404(Explorer, pk=explorer_id)
place_list = explorer.places.all()
place_list = explorer.get_place_list_to_display()
place_count = place_list.count()
context={
@@ -82,3 +83,20 @@ class ExplorerProfileUpdateView(IsAuthenticatedMixin, View):
)
return redirect(reverse_lazy('explorer_profile_update'))
class ExplorerDraftsView(IsAuthenticatedMixin, UserPassesTestMixin, View):
def get_explorer(self):
return get_object_or_404(Explorer, pk=self.kwargs['explorer_id'])
def test_func(self):
if not hasattr(self.request, 'user'):
return False
return self.request.user == self.get_explorer().user or self.request.user.is_superuser
def get(self, request, *args, **kwargs):
context = {
'explorer': self.get_explorer(),
'place_list': self.get_explorer().get_drafts()
}
return render(request, 'explorer/drafts.html', context)

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

@@ -36,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')]
@@ -59,6 +59,17 @@ class PlaceDetailView(IsAuthenticatedMixin, IsEligibleToSeePlaceMixin, View):
place.calculate_place_level()
explorer = request.user.explorer
if place.mode == 'draft':
messages.info(
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,
'mapping_config': {