7 Commits

Author SHA1 Message Date
Leonhard Strohmidel
c9d83dfc2c #64 place mode and filtering of place modes in list views 2022-09-24 12:10:19 +02:00
Leonhard Strohmidel
49301afe51 Adapting django version 2022-09-20 12:00:27 +02:00
Leonhard Strohmidel
624878624f Testing place level and level acccuracy 2022-09-20 12:00:09 +02:00
Leonhard Strohmidel
a2ee323fa4 Removing Expireable from PlaceVote 2022-09-20 11:59:51 +02:00
Leonhard Strohmidel
86c9de3213 Fixing average latlon function 2022-09-20 11:58:20 +02:00
Leonhard Strohmidel
8597e53599 #63 Replacing vote expiration with vote accuarcy 2022-09-20 09:56:23 +02:00
Leonhard Strohmidel
d213b51a59 Fixing Typo 2022-09-20 09:53:00 +02:00
13 changed files with 308 additions and 49 deletions

View File

@@ -1,6 +1,6 @@
# lostplaces-backend # lostplaces-backend
lostplaces-backend is a django (3.x) based webproject. It once wants to become a software which allows a group of urban explorers to manage, document and share the locations of lost places while not exposing too much / any information to the public. lostplaces-backend is a django (4.x) based webproject. It once wants to become a software which allows a group of urban explorers to manage, document and share the locations of lost places while not exposing too much / any information to the public.
The software is currently in early development status, neither scope, datamodel(s) nor features are finalized yet. Therefore we would not recommend to download or install this piece of software anywhere - except your local django dev server. The software is currently in early development status, neither scope, datamodel(s) nor features are finalized yet. Therefore we would not recommend to download or install this piece of software anywhere - except your local django dev server.

View File

@@ -69,7 +69,7 @@ class PlaceForm(forms.ModelForm):
class Meta: class Meta:
model = Place model = Place
fields = '__all__' fields = '__all__'
exclude = ['submitted_by', 'level'] exclude = ['submitted_by', 'level', 'mode']
widgets = { widgets = {
'hero': widgets.SelectContent() 'hero': widgets.SelectContent()
} }
@@ -88,6 +88,11 @@ class PlaceForm(forms.ModelForm):
widget=forms.NumberInput(attrs={'min':-180,'max': 180,'type': 'number', 'step': 'any'}) widget=forms.NumberInput(attrs={'min':-180,'max': 180,'type': 'number', 'step': 'any'})
) )
draft = forms.BooleanField(
label=_('Save Place as draft'),
required=False
)
class PlaceImageForm(forms.ModelForm): class PlaceImageForm(forms.ModelForm):
class Meta: class Meta:
model = PlaceImage model = PlaceImage

View File

@@ -82,9 +82,25 @@ class Explorer(models.Model):
choices=EXPLORER_LEVELS choices=EXPLORER_LEVELS
) )
def get_place_list_to_display(self):
'''
Gets the list of places to show on the homepage
and the list views
'''
if self.user.is_superuser:
return Place.objects.filter(mode='live')
return Place.objects.filter(
level__lte=self.level,
mode='live'
) | Place.objects.filter(
submitted_by=self,
mode='live'
)
def get_places_eligible_to_see(self): def get_places_eligible_to_see(self):
if self.user.is_superuser: if self.user.is_superuser:
return Place.objects.all() return Place.objects.filter(mode='live')
return Place.objects.all().filter(level__lte=self.level) | self.places.all() return Place.objects.all().filter(level__lte=self.level) | self.places.all()
def is_eligible_to_see(self, place): def is_eligible_to_see(self, place):

View File

@@ -1,11 +1,13 @@
import os import os
from math import floor import datetime
from math import ceil
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.signals import post_delete, pre_save from django.db.models.signals import post_delete, pre_save
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.utils import timezone
from django.conf import settings from django.conf import settings
from lostplaces.models.abstract_models import Submittable, Taggable, Mapable, Expireable from lostplaces.models.abstract_models import Submittable, Taggable, Mapable, Expireable
@@ -21,6 +23,13 @@ PLACE_LEVELS = (
(5, 'Time Capsule') (5, 'Time Capsule')
) )
PLACE_MODES = (
('live', 'live'),
('draft', 'draft'),
('review', 'review'),
('archive', 'archive')
)
class Place(Submittable, Taggable, Mapable): class Place(Submittable, Taggable, Mapable):
""" """
Place defines a lost place (location, name, description etc.). Place defines a lost place (location, name, description etc.).
@@ -48,6 +57,12 @@ class Place(Submittable, Taggable, Mapable):
choices=PLACE_LEVELS choices=PLACE_LEVELS
) )
mode = models.TextField(
default='live',
choices=PLACE_MODES,
verbose_name=_('Mode of Place Editing')
)
def get_hero_image(self): def get_hero_image(self):
if self.hero: if self.hero:
return self.hero return self.hero
@@ -76,24 +91,22 @@ class Place(Submittable, Taggable, Mapable):
# Get center position of LP-geocoordinates. # Get center position of LP-geocoordinates.
def average_latlon(cls, place_list): def average_latlon(cls, place_list):
amount = len(place_list) amount = len(place_list)
# Init fill values to prevent None
# China Corner in Münster
# Where I almost always eat lunch
# (Does'nt help losing wheight, tho)
longitude = 7.6295628132604385
latitude = 51.961922091398904
if amount > 0: if amount > 0:
latitude = 0
longitude = 0
for place in place_list: for place in place_list:
longitude += place.longitude longitude += place.longitude
latitude += place.latitude latitude += place.latitude
return {'latitude': latitude / amount, 'longitude': longitude / amount} return {'latitude': latitude / amount, 'longitude': longitude / amount}
else:
return {'latitude': latitude, 'longitude': longitude} # Location of China Corner in Münster
# Where I almost always eat lunch
# (Does'nt help losing wheight, tho)
return {'latitude': 51.961922091398904, 'longitude': 7.6295628132604385}
def calculate_place_level(self): def calculate_place_level(self):
self.remove_expired_votes()
if self.placevotings.count() == 0: if self.placevotings.count() == 0:
self.level = 5 self.level = 5
self.save() self.save()
@@ -104,13 +117,22 @@ class Place(Submittable, Taggable, Mapable):
for vote in self.placevotings.all(): for vote in self.placevotings.all():
level += vote.vote level += vote.vote
self.level = floor(level / self.placevotings.count()) self.level = round(level / self.placevotings.count())
self.save() self.save()
def remove_expired_votes(self): def calculate_voting_accuracy(self):
place_age = timezone.now() - self.submitted_when;
accuaries = [];
for vote in self.placevotings.all(): for vote in self.placevotings.all():
if vote.is_expired: vote_age = timezone.now() - vote.submitted_when;
vote.delete() accuracy = 100 - (100 / (place_age / vote_age))
accuaries.append(accuracy)
if len(accuaries) > 0:
return ceil(sum(accuaries) / len(accuaries))
else:
return 0
def __str__(self): def __str__(self):
return self.name return self.name
@@ -210,7 +232,7 @@ def auto_delete_file_on_change(sender, instance, **kwargs):
old_file.delete(save=False) old_file.delete(save=False)
class PlaceVoting(PlaceAsset, Expireable): class PlaceVoting(PlaceAsset):
vote = models.IntegerField(choices=PLACE_LEVELS) vote = models.IntegerField(choices=PLACE_LEVELS)
def get_human_readable_level(self): def get_human_readable_level(self):

View File

@@ -28,12 +28,7 @@
</div> </div>
<div class="LP-Voting__Expiration"> <div class="LP-Voting__Expiration">
<span class="LP-Voting__InfoLabel">Your vote expires on</span> The accuracy of the voting is {{voting.accuracy}}%
<span class="LP-Voting__Date">
<time datetime="{{voting.expires_when|date:'Y-m-d'}}">
{{voting.users_vote.expires_when|date:'d.m.Y'}}
</time>
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -39,8 +39,10 @@
</div> </div>
</div> </div>
{% translate 'Create' as action %} {% translate 'Create' as action %}
<div class="LP-Form__Composition LP-Form__Composition--buttons"> <div class="LP-Form__Composition LP-Form__Composition--buttons">
{% include 'partials/form/inputField.html' with field=place_form.draft %}
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER action=action %} {% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER action=action %}
</div> </div>
</fieldset> </fieldset>

View File

@@ -23,7 +23,15 @@
{% block maincontent %} {% block maincontent %}
<article class="LP-PlaceDetail"> <article class="LP-PlaceDetail">
<header class="LP-PlaceDetail__Header"> <header class="LP-PlaceDetail__Header">
<h1 class="LP-Headline">{{ place.name }} {% include 'partials/icons/place_favorite.html' %} {% include 'partials/icons/place_visited.html' %}</h1> <h1 class="LP-Headline">
{{ place.name }}
{% include 'partials/icons/place_favorite.html' %}
{% include 'partials/icons/place_visited.html' %}
{% if user.is_superuser %}
<a class="LP-Link" href="{{'/admin/lostplaces/place/'|addstr:place.id }}" target="_blank"><span class="LP-Link__Text">{% translate 'view place in admin panel' %}</span></a>
{% endif %}
</h1>
{% if place.get_hero_image %} {% if place.get_hero_image %}
<div class="LP-PlaceDetail__Image"> <div class="LP-PlaceDetail__Image">
{% include '../partials/image.html' with source_url=place.get_hero_image.filename.hero.url link_url="#image"|addstr:place.get_hero_index_in_queryset %} {% include '../partials/image.html' with source_url=place.get_hero_image.filename.hero.url link_url="#image"|addstr:place.get_hero_index_in_queryset %}

View File

@@ -14,7 +14,7 @@
</div> </div>
<div class="LP-Form__Composition LP-Form__Composition--buttons"> <div class="LP-Form__Composition LP-Form__Composition--buttons">
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER %} {% include 'partials/form/submit.html' with referer=request.META.HTTP_REFERER %}
</div> </div>
</fieldset> </fieldset>

View File

@@ -1,13 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
from django.test import TestCase from django.test import TestCase
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone from django.utils import timezone
from lostplaces.models import Place from lostplaces.models import Place, PlaceVoting
from lostplaces.tests.models import ModelTestCase from lostplaces.tests.models import ModelTestCase
class PlaceTestCase(ModelTestCase): class PlaceTestCase(ModelTestCase):
@@ -106,12 +107,12 @@ class PlaceTestCase(ModelTestCase):
an empty list an empty list
''' '''
avg_latlon = Place.average_latlon([]) avg_latlon = Place.average_latlon([])
self.assertEqual(avg_latlon['latitude'], 0, self.assertEqual(avg_latlon['latitude'], 51.961922091398904,
msg='%s: (no places) average latitude missmatch' % ( msg='%s: (no places) average latitude missmatch' % (
self.model.__name__ self.model.__name__
) )
) )
self.assertEqual(avg_latlon['longitude'], 0, self.assertEqual(avg_latlon['longitude'], 7.6295628132604385,
msg='%s: (no places) average longitude missmatch' % ( msg='%s: (no places) average longitude missmatch' % (
self.model.__name__ self.model.__name__
) )
@@ -124,3 +125,169 @@ class PlaceTestCase(ModelTestCase):
self.model.__name__ self.model.__name__
) )
) )
def test_level_calculation(self):
explorer = self.place.submitted_by
PlaceVoting.objects.create(
submitted_by=explorer,
place=self.place,
vote=5
)
PlaceVoting.objects.create(
submitted_by=explorer,
place=self.place,
vote=2
)
PlaceVoting.objects.create(
submitted_by=explorer,
place=self.place,
vote=4
)
self.place.calculate_place_level()
self.assertEqual(
4,
self.place.level,
msg='Expecting the place level to be 4'
)
def test_level_calculation_no_votes(self):
self.place.calculate_place_level()
self.assertEqual(
5,
self.place.level,
msg='Expecting the default place level to be 5'
)
def test_level_mid_accuracy(self):
explorer = self.place.submitted_by
six_month_ago = datetime.timedelta(days=180)
self.place.submitted_when = timezone.now() - six_month_ago
votings = [
{
'date': timezone.now() - datetime.timedelta(days=170),
'vote': 5
},
{
'date': timezone.now() - datetime.timedelta(days=23),
'vote': 2
},
{
'date': timezone.now() - datetime.timedelta(days=1),
'vote': 4
}
]
for vote in votings:
voting = PlaceVoting.objects.create(
submitted_by=explorer,
place=self.place,
vote= vote['vote']
)
voting.submitted_when = vote['date']
voting.save()
self.assertEqual(
65,
self.place.calculate_voting_accuracy()
)
def test_level_high_accuracy(self):
explorer = self.place.submitted_by
six_month_ago = datetime.timedelta(days=180)
self.place.submitted_when = timezone.now() - six_month_ago
votings = [
{
'date': timezone.now() - datetime.timedelta(days=9),
'vote': 5
},
{
'date': timezone.now() - datetime.timedelta(days=14),
'vote': 2
},
{
'date': timezone.now() - datetime.timedelta(days=0),
'vote': 4
}
]
for vote in votings:
voting = PlaceVoting.objects.create(
submitted_by=explorer,
place=self.place,
vote= vote['vote']
)
voting.submitted_when = vote['date']
voting.save()
self.assertEqual(
96,
self.place.calculate_voting_accuracy()
)
def test_level_low_accuracy(self):
explorer = self.place.submitted_by
six_month_ago = datetime.timedelta(days=180)
self.place.submitted_when = timezone.now() - six_month_ago
votings = [
{
'date': timezone.now() - datetime.timedelta(days=177),
'vote': 5
},
{
'date': timezone.now() - datetime.timedelta(days=150),
'vote': 2
},
{
'date': timezone.now() - datetime.timedelta(days=100),
'vote': 4
}
]
for vote in votings:
voting = PlaceVoting.objects.create(
submitted_by=explorer,
place=self.place,
vote= vote['vote']
)
voting.submitted_when = vote['date']
voting.save()
self.assertEqual(
21,
self.place.calculate_voting_accuracy()
)
def test_level_accuracy_zero_timedelta(self):
explorer = self.place.submitted_by
six_month_ago = datetime.timedelta(days=180)
self.place.submitted_when = timezone.now() - six_month_ago
votings = [
{
'date': timezone.now() - datetime.timedelta(days=0),
'vote': 4
}
]
for vote in votings:
voting = PlaceVoting.objects.create(
submitted_by=explorer,
place=self.place,
vote= vote['vote']
)
voting.submitted_when = vote['date']
voting.save()
self.assertEqual(
100,
self.place.calculate_voting_accuracy(),
msg='Expecting the accurcy to be 100% when the vote is 0 time units old'
)

View File

@@ -13,6 +13,7 @@ from django.utils.translation import gettext as _
from lostplaces.models import Place from lostplaces.models import Place
from lostplaces.models import PLACE_MODES
from lostplaces.views import ( from lostplaces.views import (
PlaceCreateView, PlaceCreateView,
PlaceListView, PlaceListView,
@@ -155,6 +156,38 @@ class TestPlaceListView(GlobalTemplateTestCaseMixin, ViewTestCase):
msg='Expecting the user to see places where their level is high enough' msg='Expecting the user to see places where their level is high enough'
) )
def test_place_mode_filter(self):
explorer = User.objects.get(username='testpeter').explorer
Place.objects.all().delete()
for mode in PLACE_MODES:
place = Place.objects.create(
name='Im a place in mode %s' % mode[0],
submitted_when=timezone.now(),
submitted_by=explorer,
location='Test town',
latitude=50.5,
longitude=7.0,
description='This is just a test, do not worry %s' % mode[0],
level=3,
mode=mode[0]
)
self.client.login(username='testpeter', password='Develop123')
response = self.client.get(reverse('place_list'))
for mode in PLACE_MODES:
if ('Im a place in mode %s' % mode[0]) in response.content.decode():
self.assertTrue(
mode[0] == 'live',
msg='Expecting only places in mode \'live\' to be listed, saw a place in mode %s' % mode[0]
)
elif mode[0] == 'live':
self.fail(
msg='Expecting at least one place in mode \'live\' to be listed'
)
class TestPlaceCreateView(ViewTestCase): class TestPlaceCreateView(ViewTestCase):
view = PlaceCreateView view = PlaceCreateView

View File

@@ -138,4 +138,10 @@ class LevelCapPlaceListView(ListView):
model = Place model = Place
def get_queryset(self): def get_queryset(self):
return self.request.user.explorer.get_places_eligible_to_see() return self.request.user.explorer.get_place_list_to_display()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['place_list'] = context.pop('object_list')
return context

View File

@@ -74,7 +74,8 @@ class PlaceDetailView(IsAuthenticatedMixin, IsEligibleToSeePlaceMixin, View):
}, },
'placevoting': { 'placevoting': {
'users_vote': PlaceVoting.objects.filter(place=place, submitted_by=explorer).first(), 'users_vote': PlaceVoting.objects.filter(place=place, submitted_by=explorer).first(),
'all_choices': reversed(PLACE_LEVELS) 'all_choices': reversed(PLACE_LEVELS),
'accuracy': place.calculate_voting_accuracy()
} }
} }
return render(request, 'place/place_detail.html', context) return render(request, 'place/place_detail.html', context)
@@ -204,8 +205,11 @@ class PlaceVisitDeleteView(IsAuthenticatedMixin, View):
class PlaceVoteView(IsEligibleToSeePlaceMixin, View): class PlaceVoteView(IsEligibleToSeePlaceMixin, View):
delta = timedelta(weeks=24) delta = timedelta(weeks=24)
def get_place(self):
return get_object_or_404(Place, pk=self.kwargs['place_id'])
def get(self, request, place_id, vote): def get(self, request, place_id, vote):
place = get_object_or_404(Place, id=place_id) place = self.get_place()
explorer = request.user.explorer explorer = request.user.explorer
voting = PlaceVoting.objects.filter( voting = PlaceVoting.objects.filter(
@@ -217,12 +221,11 @@ class PlaceVoteView(IsEligibleToSeePlaceMixin, View):
voting = PlaceVoting.objects.create( voting = PlaceVoting.objects.create(
submitted_by=explorer, submitted_by=explorer,
place=place, place=place,
vote=vote, vote=vote
expires_when=timezone.now()+self.delta
) )
messages.success(self.request, _('Vote submitted')) messages.success(self.request, _('Vote submitted'))
else: else:
voting.expires_when=timezone.now()+self.delta voting.submitted_when = timezone.now()
voting.vote = vote voting.vote = vote
messages.success(self.request, _('Your vote has been update')) messages.success(self.request, _('Your vote has been update'))

View File

@@ -13,7 +13,7 @@ from django.utils.translation import gettext as _
from lostplaces.forms import SignupVoucherForm, TagSubmitForm from lostplaces.forms import SignupVoucherForm, TagSubmitForm
from lostplaces.models import Place, PhotoAlbum from lostplaces.models import Place, PhotoAlbum
from lostplaces.views.base_views import IsAuthenticatedMixin from lostplaces.views.base_views import IsAuthenticatedMixin, LevelCapPlaceListView
from lostplaces.common import redirect_referer_or from lostplaces.common import redirect_referer_or
from lostplaces.views.base_views import ( from lostplaces.views.base_views import (
@@ -29,17 +29,19 @@ class SignUpView(SuccessMessageMixin, CreateView):
template_name = 'signup.html' template_name = 'signup.html'
success_message = _('User created') success_message = _('User created')
class HomeView(IsAuthenticatedMixin, View): class HomeView(IsAuthenticatedMixin, LevelCapPlaceListView, View):
def get(self, request, *args, **kwargs): template_name = 'home.html'
place_list = request.user.explorer.get_places_eligible_to_see()
context = { def get_context_data(self, **kwargs):
'place_list': place_list, context = super().get_context_data(**kwargs)
'mapping_config': { place_list = context['place_list']
context['mapping_config'] = {
'all_points': place_list, 'all_points': place_list,
'map_center': Place.average_latlon(place_list) 'map_center': Place.average_latlon(place_list)
} }
} return context
return render(request, 'home.html', context)
def handle_no_permission(self): def handle_no_permission(self):
place_list = Place.objects.filter(level=1)[:5] place_list = Place.objects.filter(level=1)[:5]