diff --git a/Pipfile b/Pipfile index 8ce2a9d..5325444 100644 --- a/Pipfile +++ b/Pipfile @@ -13,6 +13,7 @@ twine = "*" pandoc = "*" pylint-django = "*" setuptools = "*" +django-nose = "*" [packages] django = "*" diff --git a/django_lostplaces/django_lostplaces/settings.py b/django_lostplaces/django_lostplaces/settings.py index 93a097c..44c6948 100644 --- a/django_lostplaces/django_lostplaces/settings.py +++ b/django_lostplaces/django_lostplaces/settings.py @@ -49,7 +49,15 @@ INSTALLED_APPS = [ 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', - 'django.contrib.staticfiles' + 'django.contrib.staticfiles', + 'django_nose' +] + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +NOSE_ARGS = [ + '--with-coverage', + '--cover-package=lostplaces', ] MIDDLEWARE = [ diff --git a/django_lostplaces/lostplaces/__init__.py b/django_lostplaces/lostplaces/__init__.py index c2d76d6..534c09a 100644 --- a/django_lostplaces/lostplaces/__init__.py +++ b/django_lostplaces/lostplaces/__init__.py @@ -5,7 +5,7 @@ from django.conf import settings settings.THUMBNAIL_ALIASES = { '': { - 'thumbnail': {'size': (300, 200), 'sharpen': True, 'crop': True}, + 'thumbnail': {'size': (300, 200), 'sharpen': True, 'crop': True, 'upscale': True}, 'hero': {'size': (700, 466), 'sharpen': True, 'crop': True}, 'large': {'size': (1920, 1920), 'sharpen': True, 'crop': False}, }, diff --git a/django_lostplaces/lostplaces/apps.py b/django_lostplaces/lostplaces/apps.py index bd8e0fc..71c4108 100644 --- a/django_lostplaces/lostplaces/apps.py +++ b/django_lostplaces/lostplaces/apps.py @@ -4,4 +4,5 @@ from django.apps import AppConfig class LostplacesAppConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' name = 'lostplaces' diff --git a/django_lostplaces/lostplaces/models/models.py b/django_lostplaces/lostplaces/models/models.py index 5f0fd44..286f540 100644 --- a/django_lostplaces/lostplaces/models/models.py +++ b/django_lostplaces/lostplaces/models/models.py @@ -82,6 +82,18 @@ class Explorer(models.Model): choices=EXPLORER_LEVELS ) + def get_places_eligible_to_see(self): + if self.user.is_superuser: + return Place.objects.all() + return Place.objects.all().filter(level__lte=self.level) | self.places.all() + + def is_eligible_to_see(self, place): + return ( + self.user.is_superuser or + place.submitted_by == self or + place in self.get_places_eligible_to_see() + ) + def __str__(self): return self.user.username diff --git a/django_lostplaces/lostplaces/tests/views/test_place_views.py b/django_lostplaces/lostplaces/tests/views/test_place_views.py index 8bebd2d..ba70250 100644 --- a/django_lostplaces/lostplaces/tests/views/test_place_views.py +++ b/django_lostplaces/lostplaces/tests/views/test_place_views.py @@ -37,9 +37,36 @@ class TestPlaceListView(GlobalTemplateTestCaseMixin, ViewTestCase): def setUpTestData(cls): user = User.objects.create_user( username='testpeter', + password='Develop123', + ) + user.explorer.level = 3 + user.explorer.save() + + # default level should be 1, not setting required + other_user = User.objects.create_user( + username='blubberbernd', password='Develop123' ) + superuser = User.objects.create_user( + username='toor', + password='Develop123' + ) + + superuser.is_superuser = True + superuser.save() + + Place.objects.create( + name='Im a own place', + submitted_when=timezone.now(), + submitted_by=other_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 + ) + for i in range(12): place = Place.objects.create( name='Im a place %d' % i, @@ -48,7 +75,8 @@ class TestPlaceListView(GlobalTemplateTestCaseMixin, ViewTestCase): location='Test %d town' % i, latitude=50.5 + i/10, longitude=7.0 - i/10, - description='This is just a test, do not worry %d' % i + description='This is just a test, do not worry %d' % i, + level=3 ) place.tags.add('I a tag', 'testlocation') place.save() @@ -90,6 +118,42 @@ class TestPlaceListView(GlobalTemplateTestCaseMixin, ViewTestCase): ), msg='Expecting the place list to be paginated like [first] [previous] [item] at least 2 times [next] [last]' ) + + def test_not_eligible_to_see_because_of_low_level(self): + self.client.login(username='blubberbernd', password='Develop123') + response = self.client.get(reverse('place_list')) + + self.assertFalse( + 'Im a place' in response.content.decode(), + msg='Expecting the user to not see any places' + ) + + def test_not_eligible_to_see_because_of_low_level_superuser(self): + self.client.login(username='toor', password='Develop123') + response = self.client.get(reverse('place_list')) + + self.assertTrue( + 'Im a place' in response.content.decode(), + msg='Expecting the superuser to see all places' + ) + + def test_not_eligible_to_see_because_of_low_level_own_place(self): + self.client.login(username='blubberbernd', password='Develop123') + response = self.client.get(reverse('place_list')) + + self.assertTrue( + 'Im a own place' in response.content.decode(), + msg='Expecting the user to see it\'s own places' + ) + + def test_eligible_to_see(self): + self.client.login(username='testpeter', password='Develop123') + response = self.client.get(reverse('place_list')) + + self.assertTrue( + 'Im a own place' in response.content.decode(), + msg='Expecting the user to see places where their level is high enough' + ) class TestPlaceCreateView(ViewTestCase): view = PlaceCreateView @@ -308,6 +372,8 @@ class PlaceDetailViewTestCase(TaggableViewTestCaseMixin, MapableViewTestCaseMixi username='testpeter', password='Develop123' ) + user.explorer.level = 3 + user.explorer.save() place = Place.objects.create( name='Im a place', @@ -316,11 +382,72 @@ class PlaceDetailViewTestCase(TaggableViewTestCaseMixin, MapableViewTestCaseMixi location='Testtown', latitude=50.5, longitude=7.0, - description='This is just a test, do not worry' + description='This is just a test, do not worry', + level=3 ) place.tags.add('I a tag', 'testlocation') place.save() + other_user = User.objects.create_user( + username='blubberbernd', + password='Develop123' + ) + + superuser = User.objects.create_user( + username='toor', + password='Develop123' + ) + + superuser.is_superuser = True + superuser.save() + + Place.objects.create( + name='Im a own place', + submitted_when=timezone.now(), + submitted_by=other_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 + ) + + def test_not_eligible_to_see_because_of_low_level(self): + self.client.login(username='blubberbernd', password='Develop123') + response = self.client.get(reverse('place_detail', kwargs={'pk': 1})) + + self.assertFalse( + 'Im a place' in response.content.decode(), + msg='Expecting the user to not see the places' + ) + + def test_not_eligible_to_see_because_of_low_level_superuser(self): + self.client.login(username='toor', password='Develop123') + response = self.client.get(reverse('place_detail', kwargs={'pk': 1})) + + self.assertTrue( + 'Im a place' in response.content.decode(), + msg='Expecting the superuser to see all places' + ) + + def test_not_eligible_to_see_because_of_low_level_own_place(self): + self.client.login(username='blubberbernd', password='Develop123') + response = self.client.get(reverse('place_detail', kwargs={'pk': 2})) + + self.assertTrue( + 'Im a own place' in response.content.decode(), + msg='Expecting the user to see it\'s own places' + ) + + def test_eligible_to_see(self): + self.client.login(username='testpeter', password='Develop123') + response = self.client.get(reverse('place_detail', kwargs={'pk': 2})) + + self.assertTrue( + 'Im a own place' in response.content.decode(), + msg='Expecting the user to see places where their level is high enough' + ) + def test_not_authenticated(self): response = self.client.get(reverse('place_detail', kwargs={'pk': 1})) self.assertHttpRedirect(response) diff --git a/django_lostplaces/lostplaces/views/base_views.py b/django_lostplaces/lostplaces/views/base_views.py index 3f07af2..bc8d8e4 100644 --- a/django_lostplaces/lostplaces/views/base_views.py +++ b/django_lostplaces/lostplaces/views/base_views.py @@ -4,6 +4,7 @@ from django.views import View from django.views.generic.edit import CreateView from django.views.generic.detail import SingleObjectMixin +from django.views.generic import ListView from django.contrib import messages from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin @@ -26,7 +27,8 @@ class IsAuthenticatedMixin(LoginRequiredMixin, View): permission_denied_message = _('Please login to proceed') def handle_no_permission(self): - messages.error(self.request, self.permission_denied_message) + if not self.request.user.is_authenticated: + messages.error(self.request, self.permission_denied_message) return super().handle_no_permission() class IsPlaceSubmitterMixin(UserPassesTestMixin, View): @@ -60,6 +62,23 @@ class IsPlaceSubmitterMixin(UserPassesTestMixin, View): messages.error(self.request, self.place_submitter_error_message) return False +class IsEligibleToSeePlaceMixin(UserPassesTestMixin): + not_eligible_to_see_message = None + + def get_place(self): + pass + + def test_func(self): + if not hasattr(self.request, 'user'): + return False + + if self.request.user.explorer.is_eligible_to_see(self.get_place()): + return True + + if self.not_eligible_to_see_message: + messages.error(self.request, self.not_eligible_to_see_message) + return False + class PlaceAssetCreateView(IsAuthenticatedMixin, SuccessMessageMixin, CreateView): """ Abstract View for creating a place asset (i.e. PlaceImage) @@ -113,3 +132,10 @@ class PlaceAssetDeleteView(IsAuthenticatedMixin, IsPlaceSubmitterMixin, SingleOb self.get_object().delete() messages.success(self.request, self.success_message) return redirect_referer_or(request, reverse('place_detail', kwargs={'pk': place_id})) + + +class LevelCapPlaceListView(ListView): + model = Place + + def get_queryset(self): + return self.request.user.explorer.get_places_eligible_to_see() \ No newline at end of file diff --git a/django_lostplaces/lostplaces/views/place_image_views.py b/django_lostplaces/lostplaces/views/place_image_views.py index dc04c37..fcb82dd 100644 --- a/django_lostplaces/lostplaces/views/place_image_views.py +++ b/django_lostplaces/lostplaces/views/place_image_views.py @@ -18,7 +18,10 @@ class MultiplePlaceImageUploadMixin: submitted_by=submitted_by ) place_image.save() - + if place.hero is None: + place.hero = place.placeimages.all()[0] + place.save() + class PlaceImageCreateView(MultiplePlaceImageUploadMixin, PlaceAssetCreateView): model = PlaceImage form_class = PlaceImageForm diff --git a/django_lostplaces/lostplaces/views/place_views.py b/django_lostplaces/lostplaces/views/place_views.py index d391300..bb63435 100644 --- a/django_lostplaces/lostplaces/views/place_views.py +++ b/django_lostplaces/lostplaces/views/place_views.py @@ -6,7 +6,6 @@ from django.db.models.functions import Lower from django.views import View from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic.detail import SingleObjectMixin -from django.views.generic import ListView from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin @@ -16,16 +15,20 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.urls import reverse_lazy, reverse from lostplaces.models import Place, PlaceImage -from lostplaces.views.base_views import IsAuthenticatedMixin, IsPlaceSubmitterMixin +from lostplaces.views.base_views import ( + IsAuthenticatedMixin, + IsPlaceSubmitterMixin, + LevelCapPlaceListView, + IsEligibleToSeePlaceMixin +) from lostplaces.views.place_image_views import MultiplePlaceImageUploadMixin from lostplaces.forms import PlaceForm, PlaceImageForm, TagSubmitForm from lostplaces.common import redirect_referer_or from taggit.models import Tag -class PlaceListView(IsAuthenticatedMixin, ListView): +class PlaceListView(IsAuthenticatedMixin, LevelCapPlaceListView): paginate_by = 5 - model = Place template_name = 'place/place_list.html' ordering = [Lower('name')] @@ -37,9 +40,15 @@ class PlaceListView(IsAuthenticatedMixin, ListView): } return context -class PlaceDetailView(IsAuthenticatedMixin, View): - def get(self, request, pk): - place = get_object_or_404(Place, pk=pk) +class PlaceDetailView(IsAuthenticatedMixin, IsEligibleToSeePlaceMixin, View): + not_eligible_to_see_message = _('You\'r not allowed to see this place') + + def get_place(self): + return get_object_or_404(Place, pk=self.kwargs['pk']) + + def get(self, request, pk): + place = self.get_place() + context = { 'place': place, 'mapping_config': { @@ -129,10 +138,14 @@ class PlaceDeleteView(IsAuthenticatedMixin, IsPlaceSubmitterMixin, DeleteView): def get_place(self): return self.get_object() -class PlaceFavoriteView(IsAuthenticatedMixin, View): - +class PlaceFavoriteView(IsAuthenticatedMixin, IsEligibleToSeePlaceMixin, View): + not_eligible_to_see_message = _('You\'r not allowed to favorite this place') + + def get_place(self): + return get_object_or_404(Place, pk=self.kwargs['place_id']) + def get(self, request, place_id): - place = get_object_or_404(Place, id=place_id) + place = self.get_place() if request.user is not None: request.user.explorer.favorite_places.add(place) request.user.explorer.save() @@ -140,7 +153,7 @@ class PlaceFavoriteView(IsAuthenticatedMixin, View): return redirect_referer_or(request, reverse('place_detail', kwargs={'pk': place.pk})) class PlaceUnfavoriteView(IsAuthenticatedMixin, View): - + def get(self, request, place_id): place = get_object_or_404(Place, id=place_id) if request.user is not None: @@ -149,10 +162,14 @@ class PlaceUnfavoriteView(IsAuthenticatedMixin, View): return redirect_referer_or(request, reverse('place_detail', kwargs={'pk': place.pk})) -class PlaceVisitCreateView(IsAuthenticatedMixin, View): - +class PlaceVisitCreateView(IsAuthenticatedMixin, IsEligibleToSeePlaceMixin, View): + not_eligible_to_see_message = _('You\'r not allowed to visit this place :P (Now please stop trying out URL\'s)') + + def get_place(self): + return get_object_or_404(Place, pk=self.kwargs['place_id']) + def get(self, request, place_id): - place = get_object_or_404(Place, id=place_id) + place = self.get_place() if request.user is not None: request.user.explorer.visited_places.add(place) request.user.explorer.save() diff --git a/django_lostplaces/lostplaces/views/views.py b/django_lostplaces/lostplaces/views/views.py index 74d16a2..a72fd8f 100644 --- a/django_lostplaces/lostplaces/views/views.py +++ b/django_lostplaces/lostplaces/views/views.py @@ -31,7 +31,7 @@ class SignUpView(SuccessMessageMixin, CreateView): class HomeView(IsAuthenticatedMixin, View): def get(self, request, *args, **kwargs): - place_list = Place.objects.all().order_by('-submitted_when')[:10] + place_list = request.user.explorer.get_places_eligible_to_see() context = { 'place_list': place_list, 'mapping_config': {