Compare commits
7 Commits
update/dja
...
c9d83dfc2c
Author | SHA1 | Date | |
---|---|---|---|
|
c9d83dfc2c | ||
|
49301afe51 | ||
|
624878624f | ||
|
a2ee323fa4 | ||
|
86c9de3213 | ||
|
8597e53599 | ||
|
d213b51a59 |
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
|
@@ -69,7 +69,7 @@ class PlaceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Place
|
||||
fields = '__all__'
|
||||
exclude = ['submitted_by', 'level']
|
||||
exclude = ['submitted_by', 'level', 'mode']
|
||||
widgets = {
|
||||
'hero': widgets.SelectContent()
|
||||
}
|
||||
@@ -88,6 +88,11 @@ class PlaceForm(forms.ModelForm):
|
||||
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 Meta:
|
||||
model = PlaceImage
|
||||
|
@@ -82,9 +82,25 @@ class Explorer(models.Model):
|
||||
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):
|
||||
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()
|
||||
|
||||
def is_eligible_to_see(self, place):
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import os
|
||||
from math import floor
|
||||
import datetime
|
||||
from math import ceil
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_delete, pre_save
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
from lostplaces.models.abstract_models import Submittable, Taggable, Mapable, Expireable
|
||||
@@ -21,6 +23,13 @@ PLACE_LEVELS = (
|
||||
(5, 'Time Capsule')
|
||||
)
|
||||
|
||||
PLACE_MODES = (
|
||||
('live', 'live'),
|
||||
('draft', 'draft'),
|
||||
('review', 'review'),
|
||||
('archive', 'archive')
|
||||
)
|
||||
|
||||
class Place(Submittable, Taggable, Mapable):
|
||||
"""
|
||||
Place defines a lost place (location, name, description etc.).
|
||||
@@ -48,6 +57,12 @@ class Place(Submittable, Taggable, Mapable):
|
||||
choices=PLACE_LEVELS
|
||||
)
|
||||
|
||||
mode = models.TextField(
|
||||
default='live',
|
||||
choices=PLACE_MODES,
|
||||
verbose_name=_('Mode of Place Editing')
|
||||
)
|
||||
|
||||
def get_hero_image(self):
|
||||
if self.hero:
|
||||
return self.hero
|
||||
@@ -76,24 +91,22 @@ class Place(Submittable, Taggable, Mapable):
|
||||
# Get center position of LP-geocoordinates.
|
||||
def average_latlon(cls, 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:
|
||||
latitude = 0
|
||||
longitude = 0
|
||||
|
||||
for place in place_list:
|
||||
longitude += place.longitude
|
||||
latitude += place.latitude
|
||||
return {'latitude': latitude / amount, 'longitude': longitude / amount}
|
||||
|
||||
return {'latitude': latitude, 'longitude': longitude}
|
||||
else:
|
||||
# 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):
|
||||
self.remove_expired_votes()
|
||||
|
||||
if self.placevotings.count() == 0:
|
||||
self.level = 5
|
||||
self.save()
|
||||
@@ -104,13 +117,22 @@ class Place(Submittable, Taggable, Mapable):
|
||||
for vote in self.placevotings.all():
|
||||
level += vote.vote
|
||||
|
||||
self.level = floor(level / self.placevotings.count())
|
||||
self.level = round(level / self.placevotings.count())
|
||||
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():
|
||||
if vote.is_expired:
|
||||
vote.delete()
|
||||
vote_age = timezone.now() - vote.submitted_when;
|
||||
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):
|
||||
return self.name
|
||||
@@ -210,7 +232,7 @@ def auto_delete_file_on_change(sender, instance, **kwargs):
|
||||
old_file.delete(save=False)
|
||||
|
||||
|
||||
class PlaceVoting(PlaceAsset, Expireable):
|
||||
class PlaceVoting(PlaceAsset):
|
||||
vote = models.IntegerField(choices=PLACE_LEVELS)
|
||||
|
||||
def get_human_readable_level(self):
|
||||
|
@@ -28,12 +28,7 @@
|
||||
</div>
|
||||
|
||||
<div class="LP-Voting__Expiration">
|
||||
<span class="LP-Voting__InfoLabel">Your vote expires on</span>
|
||||
<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>
|
||||
The accuracy of the voting is {{voting.accuracy}}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -39,8 +39,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% translate 'Create' as action %}
|
||||
<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 %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
@@ -23,7 +23,15 @@
|
||||
{% block maincontent %}
|
||||
<article class="LP-PlaceDetail">
|
||||
<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 %}
|
||||
<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 %}
|
||||
|
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</fieldset>
|
||||
|
||||
|
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
|
||||
from lostplaces.models import Place
|
||||
from lostplaces.models import Place, PlaceVoting
|
||||
from lostplaces.tests.models import ModelTestCase
|
||||
|
||||
class PlaceTestCase(ModelTestCase):
|
||||
@@ -106,12 +107,12 @@ class PlaceTestCase(ModelTestCase):
|
||||
an empty list
|
||||
'''
|
||||
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' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
self.assertEqual(avg_latlon['longitude'], 0,
|
||||
self.assertEqual(avg_latlon['longitude'], 7.6295628132604385,
|
||||
msg='%s: (no places) average longitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
@@ -124,3 +125,169 @@ class PlaceTestCase(ModelTestCase):
|
||||
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'
|
||||
)
|
||||
|
||||
|
||||
|
@@ -13,6 +13,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
from lostplaces.models import Place
|
||||
from lostplaces.models import PLACE_MODES
|
||||
from lostplaces.views import (
|
||||
PlaceCreateView,
|
||||
PlaceListView,
|
||||
@@ -155,6 +156,38 @@ class TestPlaceListView(GlobalTemplateTestCaseMixin, ViewTestCase):
|
||||
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):
|
||||
view = PlaceCreateView
|
||||
|
||||
|
@@ -138,4 +138,10 @@ class LevelCapPlaceListView(ListView):
|
||||
model = Place
|
||||
|
||||
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
|
||||
|
@@ -74,7 +74,8 @@ class PlaceDetailView(IsAuthenticatedMixin, IsEligibleToSeePlaceMixin, View):
|
||||
},
|
||||
'placevoting': {
|
||||
'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)
|
||||
@@ -204,8 +205,11 @@ class PlaceVisitDeleteView(IsAuthenticatedMixin, View):
|
||||
class PlaceVoteView(IsEligibleToSeePlaceMixin, View):
|
||||
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):
|
||||
place = get_object_or_404(Place, id=place_id)
|
||||
place = self.get_place()
|
||||
explorer = request.user.explorer
|
||||
|
||||
voting = PlaceVoting.objects.filter(
|
||||
@@ -217,12 +221,11 @@ class PlaceVoteView(IsEligibleToSeePlaceMixin, View):
|
||||
voting = PlaceVoting.objects.create(
|
||||
submitted_by=explorer,
|
||||
place=place,
|
||||
vote=vote,
|
||||
expires_when=timezone.now()+self.delta
|
||||
vote=vote
|
||||
)
|
||||
messages.success(self.request, _('Vote submitted'))
|
||||
else:
|
||||
voting.expires_when=timezone.now()+self.delta
|
||||
voting.submitted_when = timezone.now()
|
||||
voting.vote = vote
|
||||
messages.success(self.request, _('Your vote has been update'))
|
||||
|
||||
|
@@ -13,7 +13,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from lostplaces.forms import SignupVoucherForm, TagSubmitForm
|
||||
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.views.base_views import (
|
||||
@@ -29,17 +29,19 @@ class SignUpView(SuccessMessageMixin, CreateView):
|
||||
template_name = 'signup.html'
|
||||
success_message = _('User created')
|
||||
|
||||
class HomeView(IsAuthenticatedMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
place_list = request.user.explorer.get_places_eligible_to_see()
|
||||
context = {
|
||||
'place_list': place_list,
|
||||
'mapping_config': {
|
||||
class HomeView(IsAuthenticatedMixin, LevelCapPlaceListView, View):
|
||||
template_name = 'home.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
place_list = context['place_list']
|
||||
|
||||
context['mapping_config'] = {
|
||||
'all_points': place_list,
|
||||
'map_center': Place.average_latlon(place_list)
|
||||
}
|
||||
}
|
||||
return render(request, 'home.html', context)
|
||||
return context
|
||||
|
||||
|
||||
def handle_no_permission(self):
|
||||
place_list = Place.objects.filter(level=1)[:5]
|
||||
|
Reference in New Issue
Block a user