Compare commits
59 Commits
0.1.2
...
c8b3cff5a6
Author | SHA1 | Date | |
---|---|---|---|
c8b3cff5a6 | |||
547179b0ca | |||
0765f6606f | |||
12881d9345 | |||
79ed029db0 | |||
1a8da002cf | |||
c828d04f05 | |||
f919fe30fa | |||
b3db6643b9 | |||
cea3a909b5 | |||
b52d96a55e | |||
1fb71a172e | |||
27520c7ca4 | |||
af14cce3f8 | |||
75bcc91037 | |||
c78ff60231 | |||
09eb8794b8 | |||
470e54da8d | |||
19299598c3 | |||
f1c51ab8a7 | |||
b77c5d1d7f | |||
05481fc0c8 | |||
6b00452830 | |||
7e4c5dcf24 | |||
c0f30e56f7 | |||
9852646fff | |||
c2d678847e | |||
e77edf18ac | |||
3780aa6cf1 | |||
21124ec2ad | |||
0ee5fc59d3 | |||
b8dfef691e | |||
a1886b0b60 | |||
e1002b5315 | |||
fed90d4f7b | |||
dcfb329c5a | |||
b8a21a8baa | |||
7f73035b02 | |||
317437fedc | |||
26286984c2 | |||
9ae31c0146 | |||
87fd8fa96f | |||
c3401e732f | |||
4ee7373b3f | |||
64c0c5f8e6 | |||
18a597c726 | |||
|
baca596603 | ||
d993387216 | |||
aed2856df3 | |||
c78858c152 | |||
f49581259e | |||
f5bf642cd6 | |||
7687acb366 | |||
e655e1598a | |||
64ed38332f | |||
d438303aec | |||
38b3736951 | |||
6be060ea40 | |||
5c5756150f |
2
.gitignore
vendored
@@ -69,7 +69,7 @@ coverage.xml
|
||||
# exclude migrations from repository. These should be created locally, matching local DB requirements.
|
||||
# lostplaces/manage.py makemigrations && lostplaces/manage.py migrate
|
||||
|
||||
lostplaces/lostplaces_app/migrations/
|
||||
django_lostplaces/lostplaces/migrations/
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
@@ -1,5 +1,5 @@
|
||||
include LICENSE
|
||||
include Readme.rst
|
||||
include Pipfile
|
||||
recursive-include lostplaces_app/static *
|
||||
recursive-include lostplaces_app/templates *
|
||||
recursive-include lostplaces/static *
|
||||
recursive-include lostplaces/templates *
|
11
Pipfile
@@ -11,6 +11,7 @@ pipenv = "*"
|
||||
wheel = "*"
|
||||
twine = "*"
|
||||
pandoc ="*"
|
||||
pylint-django ="*"
|
||||
|
||||
[packages]
|
||||
django = "*"
|
||||
@@ -18,6 +19,10 @@ easy-thumbnails = "*"
|
||||
image = "*"
|
||||
django-widget-tweaks = "*"
|
||||
django-taggit = "*"
|
||||
# Commented out to not explicitly specify Python 3 subversion.
|
||||
# [requires]
|
||||
# python_version = "3.8"
|
||||
|
||||
[scripts]
|
||||
test = "django_lostplaces/manage.py test lostplaces"
|
||||
server = "django_lostplaces/manage.py runserver --ipv6"
|
||||
dbshell = "django_lostplaces/manage.py dbshell"
|
||||
showmigrations "django_lostplaces/manage.py showmigrations"
|
||||
|
||||
|
38
Readme.md
@@ -34,10 +34,10 @@ After having obtained the repository contents (either via .zip download or git c
|
||||
$ cd lostplaces-backend
|
||||
$ pipenv install
|
||||
$ pipenv shell
|
||||
(lostplaces-backend) $ lostplaces/manage.py makemigrations
|
||||
(lostplaces-backend) $ lostplaces/manage.py migrate
|
||||
(lostplaces-backend) $ lostplaces/manage.py createsuperuser
|
||||
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py makemigrations
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py migrate
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py createsuperuser
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py runserver --ipv6
|
||||
```
|
||||
|
||||
## Returning to the venv
|
||||
@@ -45,9 +45,9 @@ $ pipenv shell
|
||||
$ cd lostplaces-backend
|
||||
$ pipenv shell
|
||||
(lostplaces-backend) $ pipenv update # If dependencies changed, or updates available
|
||||
(lostplaces-backend) $ lostplaces/manage.py makemigrations # If datamodels changed
|
||||
(lostplaces-backend) $ lostplaces/manage.py migrate # If datamodels changed
|
||||
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py makemigrations # If datamodels changed
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py migrate # If datamodels changed
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py runserver --ipv6
|
||||
```
|
||||
|
||||
Visit: [admin](http://localhost:8000/admin) for administrative backend or
|
||||
@@ -72,12 +72,12 @@ Before making the django instance public, you should tweak the config `settings.
|
||||
2. Turn off debug mode by setting `DEBUG = False`.
|
||||
3. Tune the localization settings, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/).
|
||||
|
||||
Run `lostplaces/managy.py collectstatic` and you should be ready to go.
|
||||
Run `django_lostplaces/managy.py collectstatic` and you should be ready to go.
|
||||
|
||||
|
||||
## Installing the lostplaces_app to an existing django instance
|
||||
## Installing lostplaces to an existing django instance
|
||||
|
||||
### Installing django and the lostplaces app
|
||||
### Installing django and the django_lostplaces app
|
||||
|
||||
If you haven't already setup a django instance, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/install/).
|
||||
|
||||
@@ -93,7 +93,7 @@ Now configure your `settings.py` as follows:
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'lostplaces_app',
|
||||
'django_lostplaces',
|
||||
'easy_thumbnails',
|
||||
'widget_tweaks',
|
||||
'django_taggit'
|
||||
@@ -110,18 +110,12 @@ MEDIA_URL = '/uploads/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
||||
```
|
||||
|
||||
3. Set the user model (this will be changed in the next release):
|
||||
|
||||
```python
|
||||
AUTH_USER_MODEL = 'lostplaces_app.Explorer'
|
||||
```
|
||||
|
||||
4. Set the URL's for login, for example:
|
||||
3. Set the URL's for login, for example:
|
||||
|
||||
```python
|
||||
LOGIN_URL = reverse_lazy('login')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
||||
LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('django_lostplaces_home')
|
||||
LOGOUT_REDIRECT_URL = reverse_lazy('django_lostplaces_home')
|
||||
```
|
||||
|
||||
### Configuring the URL's
|
||||
@@ -131,7 +125,7 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('signup/', SignUpView.as_view(), name='signup'), # If you want to use lostplaces' sign up view.
|
||||
path('explorers/', include('django.contrib.auth.urls')), # You can change the 'explorers/' to whatever you desire.
|
||||
path('', include('lostplaces_app.urls')), # In this configuration lostplaces will be at the top level of you website, change '' to 'lostplaces/', if you don't want this.
|
||||
path('', include('django_lostplaces.urls')), # In this configuration django_lostplaces will be at the top level of you website, change '' to 'django_lostplaces/', if you don't want this.
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # So django can deliver user uploaded files.
|
||||
```
|
||||
|
||||
@@ -140,4 +134,4 @@ Before making the django instance public, you should tweak the config `settings.
|
||||
2. Turn off debug mode by setting `DEBUG = False`.
|
||||
3. Tune the localization settings, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/).
|
||||
|
||||
Run `lostplaces/managy.py collectstatic` you should be ready to go.
|
||||
Run `django_lostplaces/managy.py collectstatic` you should be ready to go.
|
||||
|
@@ -14,6 +14,6 @@ import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lostplaces.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_lostplaces.settings')
|
||||
|
||||
application = get_asgi_application()
|
@@ -38,7 +38,7 @@ ALLOWED_HOSTS = ['localhost']
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'lostplaces_app',
|
||||
'lostplaces',
|
||||
'easy_thumbnails',
|
||||
'widget_tweaks',
|
||||
'taggit',
|
||||
@@ -60,7 +60,7 @@ MIDDLEWARE = [
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'lostplaces.urls'
|
||||
ROOT_URLCONF = 'django_lostplaces.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
@@ -78,7 +78,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'lostplaces.wsgi.application'
|
||||
WSGI_APPLICATION = 'django_lostplaces.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
@@ -23,11 +23,11 @@ from django.conf.urls.static import static
|
||||
from django.urls import path, include
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from lostplaces_app.views import SignUpView
|
||||
from lostplaces.views import SignUpView
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('signup/', SignUpView.as_view(), name='signup'),
|
||||
path('explorers/', include('django.contrib.auth.urls')),
|
||||
path('', include('lostplaces_app.urls')),
|
||||
path('', include('lostplaces.urls')),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
@@ -14,6 +14,6 @@ import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lostplaces.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_lostplaces.settings')
|
||||
|
||||
application = get_wsgi_application()
|
@@ -6,15 +6,15 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from .models import *
|
||||
from lostplaces.models import *
|
||||
|
||||
from .forms import ExplorerCreationForm, ExplorerChangeForm
|
||||
from lostplaces.forms import ExplorerCreationForm, ExplorerChangeForm
|
||||
|
||||
# Register your models here.
|
||||
|
||||
class VoucherAdmin(admin.ModelAdmin):
|
||||
fields = ['code', 'expires', 'created']
|
||||
readonly_fields = ['created']
|
||||
fields = ['code', 'expires_when', 'created_when']
|
||||
readonly_fields = ['created_when']
|
||||
|
||||
admin.site.register(Explorer)
|
||||
admin.site.register(Voucher, VoucherAdmin)
|
@@ -1,4 +1,4 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class LostplacesAppConfig(AppConfig):
|
||||
name = 'lostplaces_app'
|
||||
name = 'lostplaces'
|
@@ -6,7 +6,7 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
|
||||
from django.contrib.auth.models import User
|
||||
from lostplaces_app.models import Place, PlaceImage, Voucher
|
||||
from lostplaces.models import Place, PlaceImage, Voucher
|
||||
|
||||
class ExplorerCreationForm(UserCreationForm):
|
||||
class Meta:
|
@@ -9,6 +9,7 @@ database.
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from django.urls import reverse
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models.signals import post_save
|
||||
@@ -19,7 +20,6 @@ from taggit.managers import TaggableManager
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Explorer(models.Model):
|
||||
"""
|
||||
Profile that is linked to the a User.
|
||||
@@ -33,7 +33,7 @@ class Explorer(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.user.name
|
||||
return self.user.username
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
@@ -44,38 +44,25 @@ def create_user_profile(sender, instance, created, **kwargs):
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
instance.explorer.save()
|
||||
|
||||
|
||||
class Voucher(models.Model):
|
||||
"""
|
||||
Vouchers are authorization tokens to allow the registration of new users.
|
||||
A voucher has a code, a creation and a deletion date, which are all
|
||||
positional. Creation date is being set automatically during voucher
|
||||
creation.
|
||||
"""
|
||||
|
||||
code = models.CharField(unique=True, max_length=30)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
expires = models.DateField()
|
||||
|
||||
def __str__(self):
|
||||
return "Voucher " + str(self.pk)
|
||||
|
||||
|
||||
class Place (models.Model):
|
||||
"""
|
||||
Place defines a lost place (location, name, description etc.).
|
||||
"""
|
||||
|
||||
class Taggable(models.Model):
|
||||
'''
|
||||
This abstract model represtens an object that is taggalble
|
||||
using django-taggit
|
||||
'''
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
tags = TaggableManager(blank=True)
|
||||
|
||||
class Mapable(models.Model):
|
||||
'''
|
||||
This abstract model class represents an object that can be
|
||||
displayed on a map.
|
||||
'''
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
name = models.CharField(max_length=50)
|
||||
submitted_when = models.DateTimeField(auto_now_add=True, null=True)
|
||||
submitted_by = models.ForeignKey(
|
||||
Explorer,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='places'
|
||||
)
|
||||
location = models.CharField(max_length=50)
|
||||
latitude = models.FloatField(
|
||||
validators=[
|
||||
MinValueValidator(-90),
|
||||
@@ -88,12 +75,55 @@ class Place (models.Model):
|
||||
MaxValueValidator(180)
|
||||
]
|
||||
)
|
||||
|
||||
class Submittable(models.Model):
|
||||
'''
|
||||
This abstract model class represents an object that can be submitted by
|
||||
an explorer.
|
||||
'''
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
submitted_when = models.DateTimeField(auto_now_add=True, null=True)
|
||||
submitted_by = models.ForeignKey(
|
||||
Explorer,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='%(class)s'
|
||||
)
|
||||
|
||||
class Voucher(models.Model):
|
||||
"""
|
||||
Vouchers are authorization tokens to allow the registration of new users.
|
||||
A voucher has a code, a creation and a deletion date, which are all
|
||||
positional. Creation date is being set automatically during voucher
|
||||
creation.
|
||||
"""
|
||||
|
||||
code = models.CharField(unique=True, max_length=30)
|
||||
created_when = models.DateTimeField(auto_now_add=True)
|
||||
expires_when = models.DateTimeField()
|
||||
|
||||
def __str__(self):
|
||||
return "Voucher " + str(self.code)
|
||||
|
||||
|
||||
class Place(Submittable, Taggable, Mapable):
|
||||
"""
|
||||
Place defines a lost place (location, name, description etc.).
|
||||
"""
|
||||
|
||||
location = models.CharField(max_length=50)
|
||||
description = models.TextField()
|
||||
|
||||
tags = TaggableManager(blank=True)
|
||||
def get_absolute_url(self):
|
||||
return reverse('place_detail', kwargs={'pk': self.pk})
|
||||
|
||||
|
||||
@classmethod
|
||||
# Get center position of LP-geocoordinates.
|
||||
def average_latlon(place_list):
|
||||
def average_latlon(cls, place_list):
|
||||
amount = len(place_list)
|
||||
# Init fill values to prevent None
|
||||
longitude = 0
|
||||
@@ -103,9 +133,9 @@ class Place (models.Model):
|
||||
for place in place_list:
|
||||
longitude += place.longitude
|
||||
latitude += place.latitude
|
||||
return (latitude / amount, longitude / amount)
|
||||
return {'latitude':latitude / amount, 'longitude': longitude / amount}
|
||||
|
||||
return (latitude, longitude)
|
||||
return {'latitude': latitude, 'longitude': longitude}
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -119,7 +149,7 @@ def generate_image_upload_path(instance, filename):
|
||||
return 'places/' + str(uuid.uuid4())+'.'+filename.split('.')[-1]
|
||||
|
||||
|
||||
class PlaceImage (models.Model):
|
||||
class PlaceImage (Submittable):
|
||||
"""
|
||||
PlaceImage defines an image file object that points to a file in uploads/.
|
||||
Intermediate image sizes are generated as defined in SIZES.
|
||||
@@ -131,17 +161,9 @@ class PlaceImage (models.Model):
|
||||
place = models.ForeignKey(
|
||||
Place,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='images'
|
||||
related_name='placeimages'
|
||||
)
|
||||
submitted_when = models.DateTimeField(auto_now_add=True, null=True)
|
||||
submitted_by = models.ForeignKey(
|
||||
Explorer,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='images'
|
||||
)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Returning the name of the corresponding place + id
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 264 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 816 B After Width: | Height: | Size: 816 B |
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 850 B After Width: | Height: | Size: 850 B |
Before Width: | Height: | Size: 914 B After Width: | Height: | Size: 914 B |
Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 488 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 717 B After Width: | Height: | Size: 717 B |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
@@ -2,8 +2,8 @@
|
||||
|
||||
{% load static %}
|
||||
{% block additional_head %}
|
||||
<link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css">
|
||||
<script src="{% static 'maps/ol.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css">
|
||||
<script src="{% static 'maps/ol.js' %}"></script>
|
||||
{% endblock additional_head %}
|
||||
|
||||
# {% block title %}Start{% endblock %}
|
||||
@@ -14,7 +14,7 @@
|
||||
<article class="LP-TextSection">
|
||||
</article>
|
||||
|
||||
{% include 'partials/osm_map.html' %}
|
||||
{% include 'partials/osm_map.html' with config=mapping_config %}
|
||||
<div class="LP-PlaceGrid">
|
||||
<h1 class="LP-Headline LP-Headline">Explore the latest locations</h1>
|
||||
<ul class="LP-PlaceGrid__Grid">
|
||||
@@ -23,7 +23,7 @@
|
||||
<a href="{% url 'place_detail' pk=place.pk %}" class="LP-Link">
|
||||
<article class="LP-PlaceTeaser">
|
||||
<div class="LP-PlaceTeaser__Image">
|
||||
<img class="LP-Image" src="{{ place.images.first.filename.thumbnail.url}}" />
|
||||
<img class="LP-Image" src="{{ place.placeimages.first.filename.thumbnail.url}}" />
|
||||
</div>
|
||||
<div class="LP-PlaceTeaser__Meta">
|
||||
<div class="LP-PlaceTeaser__Info">
|
@@ -34,7 +34,7 @@
|
||||
<a href="{% url 'place_detail' pk=place.pk %}" class="LP-Link">
|
||||
<article class="LP-PlaceTeaser">
|
||||
<div class="LP-PlaceTeaser__Image">
|
||||
<img class="LP-Image" src="{{ place.images.first.filename.thumbnail.url}}" />
|
||||
<img class="LP-Image" src="{{ place.placeimages.first.filename.thumbnail.url}}" />
|
||||
</div>
|
||||
<div class="LP-PlaceTeaser__Meta">
|
||||
<div class="LP-PlaceTeaser__Info">
|
@@ -11,20 +11,20 @@
|
||||
}),
|
||||
],
|
||||
view: new ol.View({
|
||||
center: ol.proj.fromLonLat([{{place_map_center|last}}, {{place_map_center|first}}]),
|
||||
center: ol.proj.fromLonLat([{{config.map_center.longitude}}, {{config.map_center.latitude}}]),
|
||||
zoom: 9
|
||||
})
|
||||
});
|
||||
|
||||
var vectorSource = new ol.source.Vector({
|
||||
features: [
|
||||
{% for place in place_list %}
|
||||
{% for point in config.all_points %}
|
||||
new ol.Feature({
|
||||
geometry: new ol.geom.Point(
|
||||
ol.proj.fromLonLat([{{place.longitude}},{{place.latitude}}])
|
||||
ol.proj.fromLonLat([{{point.longitude}},{{point.latitude}}])
|
||||
),
|
||||
url: '{% url 'place_detail' pk=place.pk %}',
|
||||
name: '{{place.name}}'
|
||||
url: '{{point.get_absolute_url}}',
|
||||
name: ' {{point.name}}'
|
||||
}),
|
||||
{% endfor %}
|
||||
]
|
@@ -1,6 +1,6 @@
|
||||
<div class="LP-TagList">
|
||||
<ul class="LP-TagList__List">
|
||||
{% for tag in tag_list %}
|
||||
{% for tag in config.tagged_item.tags.all %}
|
||||
<li class="LP-TagList__Item">
|
||||
<div class="LP-Tag">
|
||||
<a href="#" class="LP-Link">
|
||||
@@ -23,7 +23,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form id="id_tag_submit_form" class="LP-Form LP-Form--inline LP-Form--tagging" method="POST" action="{{config.submit_url}}">
|
||||
<form id="id_tag_submit_form" class="LP-Form LP-Form--inline LP-Form--tagging" method="POST" action="{% url config.submit_url_name tagged_id=config.tagged_item.id%}">
|
||||
<fieldset class="LP-Form__Fieldset">
|
||||
<legend class="LP-Form__Legend">Tags hinzufügen</legend>
|
||||
{% csrf_token %}
|
||||
@@ -46,14 +46,10 @@
|
||||
submit_form.onsubmit = () => false
|
||||
|
||||
const tagify = new Tagify(input, {
|
||||
'whitelist': [{
|
||||
%
|
||||
for tag in all_tags %
|
||||
}
|
||||
'whitelist': [
|
||||
{% for tag in config.tagged_item.tags.all %}
|
||||
'{{tag}}',
|
||||
{
|
||||
% endfor %
|
||||
}
|
||||
{% endfor %}
|
||||
]
|
||||
})
|
||||
|
@@ -23,9 +23,9 @@
|
||||
|
||||
<header class="LP-PlaceDetail__Header">
|
||||
<h1 class="LP-Headline">{{ place.name }}</h1>
|
||||
{% if place.images.first.filename.hero.url %}
|
||||
{% if place.placeimages.first.filename.hero.url %}
|
||||
<figure class="LP-PlaceDetail__Image">
|
||||
<img src="{{ place.images.first.filename.hero.url }}" class="LP-Image" />
|
||||
<img src="{{ place.placeimages.first.filename.hero.url }}" class="LP-Image" />
|
||||
</figure>
|
||||
{% endif %}
|
||||
</header>
|
||||
@@ -37,13 +37,13 @@
|
||||
<section class="LP-Section">
|
||||
|
||||
{% url 'place_tag_submit' place_id=place.id as tag_submit_url%}
|
||||
{% include 'partials/tagging.html' with tag_list=place.tags.all config=tagging_config all_tags=all_tags %}
|
||||
{% include 'partials/tagging.html' with config=tagging_config %}
|
||||
|
||||
</section>
|
||||
|
||||
<section class="LP-Section">
|
||||
<h1 class="LP-Headline">Map-Links</h1>
|
||||
{% include 'partials/osm_map.html' %}
|
||||
{% include 'partials/osm_map.html' with config=mapping_config%}
|
||||
<div class="LP-LinkList">
|
||||
<ul class="LP-LinkList__Container">
|
||||
<li class="LP-LinkList__Item"><a target="_blank" href="https://www.google.com/maps?q={{place.latitude}},{{place.longitude}}" class="LP-Link"><span class="LP-Text">Google Maps</span></a></li>
|
||||
@@ -92,7 +92,7 @@
|
||||
<h1 class="LP-Headline">Bilder</h1>
|
||||
<div class="LP-ImageGrid">
|
||||
<ul class="LP-ImageGrid__Container">
|
||||
{% for place_image in place.images.all %}
|
||||
{% for place_image in place.placeimages.all %}
|
||||
<li class="LP-ImageGrid__Item">
|
||||
<a href="{{ place_image.filename.large.url }}" class="LP-Link"><img class="LP-Image" src="{{ place_image.filename.thumbnail.url }}"></a>
|
||||
</li>
|
59
django_lostplaces/lostplaces/templates/place/place_list.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends 'global.html'%}
|
||||
{% load static %}
|
||||
|
||||
{% block additional_head %}
|
||||
<link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css">
|
||||
<script src="{% static 'maps/ol.js' %}"></script>
|
||||
{% endblock additional_head %}
|
||||
|
||||
{% block title %}Lost Places{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
|
||||
{% include 'partials/osm_map.html' with config=mapping_config %}
|
||||
<div class="LP-PlaceList">
|
||||
<h1 class="LP-Headline">Listing our places</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">
|
||||
<article class="LP-PlaceTeaser LP-PlaceTeaser--extended">
|
||||
<div class="LP-PlaceTeaser__Image">
|
||||
<img class="LP-Image" src="{{ place.placeimages.first.filename.thumbnail.url }}" />
|
||||
</div>
|
||||
<div class="LP-PlaceTeaser__Meta">
|
||||
<div class="LP-PlaceTeaser__Info">
|
||||
<span class="LP-PlaceTeaser__Title">
|
||||
<h2 class="LP-Headline LP-Headline--teaser">{{place.name}}</h2>
|
||||
</span>
|
||||
<span class="LP-PlaceTeaser__Detail">
|
||||
<p class="LP-Paragraph">{{place.location}}</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="LP-PlaceTeaser__Description">
|
||||
<p class="LP-Paragraph">
|
||||
{% if place.description|length > 210 %}
|
||||
{{place.description|truncatechars:210|truncatewords:-1}}
|
||||
{% else %}
|
||||
{{place.description}}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="LP-PlaceTeaser__Icons">
|
||||
<ul class="LP-Icon__List">
|
||||
<li class="LP-Icon__Item"><img class="LP-Icon" src="{% static '/icons/favourite.svg' %}" /></li>
|
||||
<li class="LP-Icon__Item"><img class="LP-Icon" src="{% static '/icons/location.svg' %}" /></li>
|
||||
<li class="LP-Icon__Item"><img class="LP-Icon" src="{% static '/icons/flag.svg' %}" /></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% include 'partials/nav/pagination.html' %}
|
||||
|
||||
</div>
|
||||
{% endblock maincontent %}
|
Before Width: | Height: | Size: 209 B After Width: | Height: | Size: 209 B |
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from django.template import Library, TemplateSyntaxError
|
||||
|
||||
#icons_json_path = getattr(settings, 'SVG_ICONS_SOURCE_FILE')
|
||||
icons_json_path = os.path.join(settings.BASE_DIR, 'lostplaces_app', 'static', 'icons', 'icons.icomoon.json')
|
||||
icons_json_path = os.path.join(settings.BASE_DIR, 'lostplaces', 'static', 'icons', 'icons.icomoon.json')
|
||||
icons_json = json.load(open(icons_json_path))
|
||||
|
||||
register = Library()
|
2
django_lostplaces/lostplaces/tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
141
django_lostplaces/lostplaces/tests/models/__init__.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.test import TestCase
|
||||
|
||||
# Creating a test user
|
||||
|
||||
class ModelTestCase(TestCase):
|
||||
'''
|
||||
Base class for ModelTests.
|
||||
Parameters:
|
||||
- model : Class to test
|
||||
'''
|
||||
model = None
|
||||
|
||||
def assertField(self, field_name, field_class, must_have={}, must_not_have={}):
|
||||
'''
|
||||
Tests if a field exists under the given name and
|
||||
if the field is of the right type.
|
||||
Also checks if the field has the given must_have attributes
|
||||
and does not have any of the must_not_have attributes. If you
|
||||
dont care about the value of the attribute you can just set it to
|
||||
something that fullfills value == False (i.e. '' or 0)
|
||||
'''
|
||||
try:
|
||||
field = self.model._meta.get_field(field_name)
|
||||
except FieldDoesNotExist:
|
||||
self.fail(
|
||||
'Expecting %s to have a field named \'%s\'' % (
|
||||
self.model.__name__,
|
||||
field_name
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
type(field), field_class,
|
||||
msg='Expecting type of %s to be %s' % (
|
||||
str(field),
|
||||
field_class.__name__
|
||||
)
|
||||
)
|
||||
|
||||
for key, value in must_have.items():
|
||||
if value:
|
||||
self.assertEqual(
|
||||
getattr(field, key), value,
|
||||
msg='Expeting the value of %s %s to be \'%s\'' % (
|
||||
str(field),
|
||||
key,
|
||||
value
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.assertTrue(
|
||||
hasattr(field, key),
|
||||
msg='Expeting %s to have \'%s\'' % (
|
||||
str(field),
|
||||
key
|
||||
)
|
||||
)
|
||||
|
||||
for key, value in must_not_have.items():
|
||||
if value:
|
||||
self.assertTrue(
|
||||
getattr(field, key) != value,
|
||||
msg='Expeting the value of %s %s to not be \'%s\'' % (
|
||||
str(field),
|
||||
key,
|
||||
value
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.assertFalse(
|
||||
hasattr(field, value),
|
||||
msg='Expeting %s to not have \'%s\'' % (
|
||||
str(field),
|
||||
key
|
||||
)
|
||||
)
|
||||
|
||||
return field
|
||||
|
||||
def assertCharField(self, field_name, min_length, max_length, must_have={}, must_hot_have={}):
|
||||
'''
|
||||
Tests if the given field is a char field and if its max_length
|
||||
is in min_length and max_legth
|
||||
'''
|
||||
field = self.assertField(
|
||||
field_name, models.CharField, must_have, must_hot_have)
|
||||
self.assertTrue(
|
||||
field.max_length in range(min_length, max_length),
|
||||
msg='Expeting %s max_length to be in the range of %d and %d' % (
|
||||
str(field),
|
||||
min_length,
|
||||
max_length
|
||||
)
|
||||
)
|
||||
|
||||
def assertFloatField(self, field_name, min_value=None, max_value=None, must_have={}, must_hot_have={}):
|
||||
'''
|
||||
Tests if the field is a floatfield. If min_value and/or max_value are passed,
|
||||
the validators of the field are also checked. The validator list of the field should
|
||||
look like
|
||||
[MinValueValidator, MayValueValidator], if both values are passed,
|
||||
[MinValueValidator] if only min_value is passed,
|
||||
[MaxValueValidator] if only max_value is passed
|
||||
'''
|
||||
field = self.assertField(
|
||||
field_name, models.FloatField, must_have, must_hot_have)
|
||||
if min_value:
|
||||
self.assertTrue(
|
||||
len(field.validators) >= 1,
|
||||
msg='Expecting the first valiator of %s to check the minimum' % (
|
||||
str(field)
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
field.validators[0].limit_value,
|
||||
min_value,
|
||||
msg='Expecting the min value of %s min to be at least %d' % (
|
||||
str(field),
|
||||
min_value
|
||||
)
|
||||
)
|
||||
if max_value:
|
||||
index = 0
|
||||
if min_value:
|
||||
index += 1
|
||||
self.assertTrue(
|
||||
len(field.validators) >= index+1,
|
||||
msg='Expecting the second valiator of %s to check the maximum' % (
|
||||
str(field)
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
field.validators[1].limit_value,
|
||||
max_value,
|
||||
msg='Expecting the max value of %s min to be at most %d' % (
|
||||
str(field),
|
||||
max_value
|
||||
)
|
||||
)
|
BIN
django_lostplaces/lostplaces/tests/models/im_a_image.jpeg
Normal file
After Width: | Height: | Size: 81 KiB |
@@ -0,0 +1,92 @@
|
||||
import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from lostplaces.models import (
|
||||
Taggable,
|
||||
Mapable,
|
||||
Submittable
|
||||
)
|
||||
from lostplaces.tests.models import ModelTestCase
|
||||
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
|
||||
class TaggableTestCase(ModelTestCase):
|
||||
|
||||
model = Taggable
|
||||
|
||||
def test_tags(self):
|
||||
self.assertField('tags', TaggableManager)
|
||||
|
||||
|
||||
class MapableTestCase(ModelTestCase):
|
||||
|
||||
model = Mapable
|
||||
|
||||
def test_name(self):
|
||||
self.assertCharField(
|
||||
field_name='name',
|
||||
min_length=10,
|
||||
max_length=100
|
||||
)
|
||||
|
||||
def test_latitude(self):
|
||||
self.assertFloatField(
|
||||
field_name='latitude',
|
||||
min_value=-90,
|
||||
max_value=90
|
||||
)
|
||||
|
||||
def test_longitude(self):
|
||||
self.assertFloatField(
|
||||
field_name='longitude',
|
||||
min_value=-180,
|
||||
max_value=180
|
||||
)
|
||||
|
||||
class SubmittableTestCase(ModelTestCase):
|
||||
model = Submittable
|
||||
|
||||
def test_submitted_when(self):
|
||||
self.assertField(
|
||||
field_name='submitted_when',
|
||||
field_class=models.DateTimeField,
|
||||
must_have={'auto_now_add': True}
|
||||
)
|
||||
|
||||
def test_submitted_by(self):
|
||||
submitted_by = self.assertField(
|
||||
field_name='submitted_by',
|
||||
field_class=models.ForeignKey
|
||||
)
|
||||
self.assertEqual(
|
||||
submitted_by.remote_field.related_name,
|
||||
'%(class)s',
|
||||
msg='Expecting the related_name of %s to be \'%%(class)s\', got %s' % (
|
||||
str(submitted_by),
|
||||
submitted_by.remote_field.related_name
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
submitted_by.null,
|
||||
msg='Expecting %s to has null=True' % (
|
||||
str(submitted_by)
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
submitted_by.blank,
|
||||
msg='Expecting %s to has blank=True' % (
|
||||
str(submitted_by)
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
submitted_by.remote_field.on_delete,
|
||||
models.SET_NULL,
|
||||
msg='Expecting %s to be null when reference is delete (models.SET_NULL)' % (
|
||||
str(submitted_by)
|
||||
)
|
||||
)
|
||||
|
@@ -0,0 +1,58 @@
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from lostplaces.models import Explorer
|
||||
|
||||
class ExplorerTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
|
||||
def test_epxlorer_creation(self):
|
||||
'''
|
||||
Tests if the explorer profile will be automticly
|
||||
created when a user is created
|
||||
'''
|
||||
|
||||
user = User.objects.get(id=1)
|
||||
explorer_list = Explorer.objects.all()
|
||||
self.assertTrue(len(explorer_list) > 0,
|
||||
msg='Expecting at least one Exlorer object, none found'
|
||||
)
|
||||
self.assertTrue(hasattr(user, 'explorer'),
|
||||
msg='''Expecting the User instance to have an \'explorer\' attribute.
|
||||
Check the Explorer model and the related name.'''
|
||||
)
|
||||
|
||||
explorer = Explorer.objects.get(id=1)
|
||||
self.assertEqual(explorer, user.explorer,
|
||||
msg='''The Explorer object of the User did not match.
|
||||
Expecting User with id 1 to have Explorer with id 1'''
|
||||
)
|
||||
|
||||
explorer = Explorer.objects.get(id=1)
|
||||
self.assertEqual(explorer.user, user,
|
||||
msg='''The User object of the Explorer did not match.
|
||||
Expecting Explorer with id 1 to have User with id 1'''
|
||||
)
|
||||
|
||||
def test_explorer_deletion(self):
|
||||
'''
|
||||
Tests if the Explorer objects get's deleted when the User instance is deleted
|
||||
'''
|
||||
|
||||
user = User.objects.get(username='testpeter')
|
||||
explorer_id = user.explorer.id
|
||||
user.delete()
|
||||
with self.assertRaises(models.ObjectDoesNotExist,
|
||||
msg='Expecting explorer objec to be deleted when the corresponding User object is deleted'
|
||||
):
|
||||
Explorer.objects.get(id=explorer_id)
|
||||
|
||||
|
||||
|
@@ -0,0 +1,108 @@
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
from unittest import mock
|
||||
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
from django.core.files import File
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from lostplaces.models import PlaceImage, Place
|
||||
from lostplaces.tests.models import ModelTestCase
|
||||
|
||||
from easy_thumbnails.fields import ThumbnailerImageField
|
||||
|
||||
class PlaceImageTestCase(ModelTestCase):
|
||||
model = PlaceImage
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
|
||||
place = Place.objects.create(
|
||||
name='Im a place',
|
||||
submitted_when=datetime.datetime.now(),
|
||||
submitted_by=User.objects.get(username='testpeter').explorer,
|
||||
location='Testtown',
|
||||
latitude=50.5,
|
||||
longitude=7.0,
|
||||
description='This is just a test, do not worry'
|
||||
)
|
||||
place.tags.add('I a tag', 'testlocation')
|
||||
place.save()
|
||||
|
||||
if not os.path.isdir(settings.MEDIA_ROOT):
|
||||
os.mkdir(settings.MEDIA_ROOT)
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if not os.path.isfile(os.path.join(settings.MEDIA_ROOT, 'im_a_image_copy.jpeg')):
|
||||
shutil.copyfile(
|
||||
os.path.join(current_dir, 'im_a_image.jpeg'),
|
||||
os.path.join(settings.MEDIA_ROOT, 'im_a_image_copy.jpeg')
|
||||
)
|
||||
|
||||
shutil.copyfile(
|
||||
os.path.join(current_dir, 'im_a_image.jpeg'),
|
||||
os.path.join(settings.MEDIA_ROOT, 'im_a_image_changed.jpeg')
|
||||
)
|
||||
|
||||
PlaceImage.objects.create(
|
||||
description='Im a description',
|
||||
filename=os.path.join(settings.MEDIA_ROOT, 'im_a_image_copy.jpeg'),
|
||||
place=place,
|
||||
submitted_when=datetime.datetime.now(),
|
||||
submitted_by=user.explorer
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.place_image = PlaceImage.objects.get(id=1)
|
||||
|
||||
def test_description(self):
|
||||
self.assertField('description', models.TextField)
|
||||
|
||||
def test_filename(self):
|
||||
self.assertField('filename',ThumbnailerImageField)
|
||||
|
||||
def test_place(self):
|
||||
field = self.assertField('place', models.ForeignKey)
|
||||
self.assertEqual(field.remote_field.on_delete, models.CASCADE,
|
||||
msg='Expecting the deletion of %s to be cascading' % (
|
||||
str(field)
|
||||
)
|
||||
)
|
||||
expected_related_name = 'placeimages'
|
||||
self.assertEqual(field.remote_field.related_name, expected_related_name,
|
||||
msg='Expecting the related name of %s to be %s' % (
|
||||
str(field),
|
||||
expected_related_name
|
||||
)
|
||||
)
|
||||
|
||||
def test_str(self):
|
||||
self.assertTrue(self.place_image.place.name.lower() in str(self.place_image).lower(),
|
||||
msg='Expecting %s.__str__ to contain the name of the place' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def test_change_filename(self):
|
||||
path = self.place_image.filename.path
|
||||
self.place_image.filename = os.path.join(settings.MEDIA_ROOT, 'im_a_image_changed.jpeg')
|
||||
self.place_image.save()
|
||||
self.assertFalse(
|
||||
os.path.isfile(path),
|
||||
msg='Expecting the old file of an place_image to be deleteed when an place_image file is changed'
|
||||
)
|
||||
|
||||
def test_deletion(self):
|
||||
path = self.place_image.filename.path
|
||||
self.place_image.delete()
|
||||
self.assertFalse(
|
||||
os.path.isfile(path),
|
||||
msg='Expecting the file of an place_image to be deleteed when an place_image is deleted'
|
||||
)
|
124
django_lostplaces/lostplaces/tests/models/test_place_model.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from lostplaces.models import Place
|
||||
from lostplaces.tests.models import ModelTestCase
|
||||
|
||||
class PlaceTestCase(ModelTestCase):
|
||||
model = Place
|
||||
related_name = 'places'
|
||||
nullable = True
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
|
||||
place = Place.objects.create(
|
||||
name='Im a place',
|
||||
submitted_when=datetime.datetime.now(),
|
||||
submitted_by=user.explorer,
|
||||
location='Testtown',
|
||||
latitude=50.5,
|
||||
longitude=7.0,
|
||||
description='This is just a test, do not worry'
|
||||
)
|
||||
place.tags.add('I a tag', 'testlocation')
|
||||
place.save()
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.place = Place.objects.get(id=1)
|
||||
|
||||
def test_location(self):
|
||||
self.assertCharField(
|
||||
field_name='location',
|
||||
min_length=10,
|
||||
max_length=100
|
||||
)
|
||||
|
||||
def test_decsription(self):
|
||||
self.assertField('description', models.TextField)
|
||||
|
||||
def test_average_latlon(self):
|
||||
'''
|
||||
Tests the average latitude/longitude calculation of a list
|
||||
of 10 places
|
||||
'''
|
||||
place_list = []
|
||||
for i in range(10):
|
||||
place = Place.objects.get(id=1)
|
||||
place.id = None
|
||||
place.latitude = i+1
|
||||
place.longitude = i+10
|
||||
place.save()
|
||||
place_list.append(place)
|
||||
|
||||
avg_latlon = Place.average_latlon(place_list)
|
||||
|
||||
self.assertTrue('latitude' in avg_latlon,
|
||||
msg='Expecting avg_latlon dict to have an \'latitude\' key'
|
||||
)
|
||||
self.assertTrue('longitude' in avg_latlon,
|
||||
msg='Expecting avg_latlon dict to have an \'longitude\' key'
|
||||
)
|
||||
|
||||
self.assertEqual(avg_latlon['latitude'], 5.5,
|
||||
msg='%s: average latitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
self.assertEqual(avg_latlon['longitude'], 14.5,
|
||||
msg='%s: average longitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def test_average_latlon_one_place(self):
|
||||
'''
|
||||
Tests the average latitude/longitude calculation of a list
|
||||
of one place
|
||||
'''
|
||||
place = Place.objects.get(id=1)
|
||||
avg_latlon = Place.average_latlon([place])
|
||||
self.assertEqual(avg_latlon['latitude'], place.latitude,
|
||||
msg='%s:(one place) average latitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
self.assertEqual(avg_latlon['longitude'], place.longitude,
|
||||
msg='%s: (one place) average longitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def test_average_latlon_no_places(self):
|
||||
'''
|
||||
Tests the average latitude/longitude calculation of
|
||||
an empty list
|
||||
'''
|
||||
avg_latlon = Place.average_latlon([])
|
||||
self.assertEqual(avg_latlon['latitude'], 0,
|
||||
msg='%s: (no places) average latitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
self.assertEqual(avg_latlon['longitude'], 0,
|
||||
msg='%s: a(no places) verage longitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def test_str(self):
|
||||
place = self.place
|
||||
self.assertTrue(place.name.lower() in str(place).lower(),
|
||||
msg='Expecting %s.__str__ to contain the name' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
@@ -0,0 +1,52 @@
|
||||
import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from lostplaces.models import Voucher
|
||||
from lostplaces.tests.models import ModelTestCase
|
||||
|
||||
|
||||
class VoucheTestCase(ModelTestCase):
|
||||
model = Voucher
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
Voucher.objects.create(
|
||||
code='ayDraJCCwfhcFiYmSR5GrcjcchDfcahv',
|
||||
expires_when=timezone.now() + datetime.timedelta(days=1)
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.voucher = Voucher.objects.get(id=1)
|
||||
|
||||
def test_voucher_code(self):
|
||||
self.assertCharField(
|
||||
field_name='code',
|
||||
min_length=10,
|
||||
max_length=100,
|
||||
must_have={'unique': True}
|
||||
)
|
||||
|
||||
def test_voucher_created(self):
|
||||
self.assertField(
|
||||
field_name='created_when',
|
||||
field_class=models.DateTimeField,
|
||||
must_have={'auto_now_add': True}
|
||||
)
|
||||
|
||||
def test_voucher_expires(self):
|
||||
self.assertField(
|
||||
field_name='expires_when',
|
||||
field_class=models.DateTimeField,
|
||||
must_not_have={'auto_now_add': True}
|
||||
)
|
||||
|
||||
def test_str(self):
|
||||
self.assertTrue(
|
||||
self.voucher.code.lower() in str(self.voucher).lower(),
|
||||
msg='Expecting %s.__str__ to contain the voucher code' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
224
django_lostplaces/lostplaces/tests/views/__init__.py
Normal file
@@ -0,0 +1,224 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from lostplaces.models import Taggable, Mapable
|
||||
|
||||
from taggit.models import Tag
|
||||
|
||||
class ViewTestCase(TestCase):
|
||||
'''
|
||||
This is a mixni for testing views. It provides functionality to
|
||||
test the context, forms and HTTP Response of responses.
|
||||
All methods take responses, so this base class can be used
|
||||
with django's RequestFactory and Test-Client
|
||||
'''
|
||||
view = None
|
||||
|
||||
def assertContext(self, response, key, value=None):
|
||||
'''
|
||||
Checks weather the response's context has the given key
|
||||
and, if passed, checks the value
|
||||
'''
|
||||
self.assertTrue(
|
||||
key in response.context,
|
||||
msg='Expecting the context of %s to have an attribute \'%s\'' % (
|
||||
self.view.__name__,
|
||||
key
|
||||
)
|
||||
)
|
||||
|
||||
if value:
|
||||
self.assertEqual(
|
||||
value,
|
||||
response.context[key],
|
||||
msg='Expecting the context of %s to have %s set to \'%s\'' % (
|
||||
self.view.__name__,
|
||||
key,
|
||||
str(value)
|
||||
)
|
||||
)
|
||||
|
||||
def assertHasForm(self, response, key, form_class):
|
||||
'''
|
||||
Checks if response has a form under the given key and if
|
||||
the forms class matches.
|
||||
'''
|
||||
self.assertContext(response, key)
|
||||
self.assertEqual(
|
||||
type(response.context[key]),
|
||||
form_class,
|
||||
msg='Expecting %s\'s context.%s to be of the type %s' % (
|
||||
self.view.__name__,
|
||||
key,
|
||||
form_class.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def assertHttpCode(self, response, code):
|
||||
'''
|
||||
Checks if the response has the given status code
|
||||
'''
|
||||
self.assertEqual(
|
||||
response.status_code, code,
|
||||
msg='Expecting an HTTP %s response, but got HTTP %s' % (
|
||||
code,
|
||||
response.status_code
|
||||
)
|
||||
)
|
||||
|
||||
def assertHttpRedirect(self, response, redirect_to=None):
|
||||
'''
|
||||
Checks weather the response redirected, and if passed,
|
||||
if it redirected to the expected loaction
|
||||
'''
|
||||
|
||||
self.assertTrue(
|
||||
300 <= response.status_code < 400,
|
||||
'Expected an HTTP 3XX (redirect) response, but got HTTP %s' %
|
||||
response.status_code
|
||||
)
|
||||
self.assertTrue(
|
||||
'location' in response,
|
||||
msg='Expecting a redirect to have an location, got none'
|
||||
)
|
||||
if redirect_to:
|
||||
self.assertEqual(
|
||||
response['location'],
|
||||
redirect_to,
|
||||
msg='Expecing the response to redirect to %s, where redirected to %s instea' % (
|
||||
str(redirect_to),
|
||||
str(response['location'])
|
||||
)
|
||||
)
|
||||
|
||||
def assertHttpOK(self, response):
|
||||
self.assertHttpCode(response, 200)
|
||||
|
||||
def assertHttpCreated(self, response):
|
||||
self.assertHttpCode(response, 201)
|
||||
|
||||
def assertHttpBadRequest(self, response):
|
||||
self.assertHttpCode(response, 400)
|
||||
|
||||
def assertHttpUnauthorized(self, response):
|
||||
self.assertHttpCode(response, 401)
|
||||
|
||||
def assertHttpForbidden(self, response):
|
||||
self.assertHttpCode(response, 403)
|
||||
|
||||
def assertHttpNotFound(self, response):
|
||||
self.assertHttpCode(response, 404)
|
||||
|
||||
def assertHttpMethodNotAllowed(self, response):
|
||||
self.assertHttpCode(response, 405)
|
||||
|
||||
class TaggableViewTestCaseMixin:
|
||||
|
||||
def assertTaggableContext(self, context):
|
||||
self.assertTrue(
|
||||
'all_tags' in context,
|
||||
msg='Expecting the context for taggable to contain an \'all_tags\' attribute'
|
||||
)
|
||||
|
||||
for tag in context['all_tags']:
|
||||
self.assertTrue(
|
||||
isinstance(tag, Tag),
|
||||
msg='Expecting all entries to be an instance of %s, got %s' % (
|
||||
str(Tag),
|
||||
str(type(tag))
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'submit_form' in context,
|
||||
msg='Expecting the context for taggable to contain \'submit_form\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'tagged_item' in context,
|
||||
msg='Expecting the context for taggable to contain \'tagged_item\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
isinstance(context['tagged_item'], Taggable),
|
||||
msg='Expecting the tagged_item to be an instance of %s' % (
|
||||
str(Taggable)
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'submit_url_name' in context,
|
||||
msg='Expecting the context for taggable to contain \'submit_url_name\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
type(context['submit_url_name']) == str,
|
||||
msg='Expecting submit_url_name to be of type string'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'delete_url_name' in context,
|
||||
msg='Expecting the context for taggable to contain \'delete_url_name\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
type(context['delete_url_name']) == str,
|
||||
msg='Expecting delete_url_name to be of type string'
|
||||
)
|
||||
|
||||
class MapableViewTestCaseMixin:
|
||||
|
||||
def assertMapableContext(self, context):
|
||||
self.assertTrue(
|
||||
'all_points' in context,
|
||||
msg='Expecting the context for mapable point to contain \'all_points\' attribute'
|
||||
)
|
||||
|
||||
for point in context['all_points']:
|
||||
self.assertTrue(
|
||||
isinstance(point, Mapable),
|
||||
msg='Expecting all entries to be an instance of %s, got %s' % (
|
||||
str(Mapable),
|
||||
str(type(point))
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'map_center' in context,
|
||||
msg='Expecting the context for mapable point to contain \'map_center\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'latitude' in context['map_center'],
|
||||
msg='Expecting the map center to contain an \'latitude\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
isinstance(context['map_center']['latitude'], float) or isinstance(context['map_center']['latitude'], int),
|
||||
msg='Expecting the latitude of the map center to be numeric, type %s given' % (
|
||||
str(type(context['map_center']['latitude']))
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
-90 <= context['map_center']['latitude'] <= 90,
|
||||
msg='Expecting the latitude of map center to be in the range of -90 and 90'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'longitude' in context['map_center'],
|
||||
msg='Expecting the map center to contain an \'longitude\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
isinstance(context['map_center']['longitude'], float) or isinstance(context['map_center']['longitude'], int),
|
||||
msg='Expecting the longitude of the map center to be numeric, type %s given' % (
|
||||
str(type(context['map_center']['longitude']))
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
-180 <= context['map_center']['longitude'] <= 180,
|
||||
msg='Expecting the longitude of map center to be in the range of -180 and 180'
|
||||
)
|
||||
|
||||
|
87
django_lostplaces/lostplaces/tests/views/test_base_views.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import datetime
|
||||
|
||||
from django.test import TestCase, RequestFactory, Client
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||
|
||||
from lostplaces.models import Place
|
||||
from lostplaces.views import IsAuthenticatedMixin
|
||||
from lostplaces.tests.views import ViewTestCase
|
||||
|
||||
class TestIsAuthenticated(ViewTestCase):
|
||||
view = IsAuthenticatedMixin
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_logged_in(self):
|
||||
request = RequestFactory().get('/')
|
||||
request.user = User.objects.get(id=1)
|
||||
|
||||
response = IsAuthenticatedMixin.as_view()(request)
|
||||
# Expecting a 405 because IsAuthenticatedMixin has no 'get' method
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_not_logged_in(self):
|
||||
request = RequestFactory().get('/someurl1234')
|
||||
request.user = AnonymousUser()
|
||||
request.session = 'session'
|
||||
messages = FallbackStorage(request)
|
||||
request._messages = messages
|
||||
|
||||
response = IsAuthenticatedMixin.as_view()(request)
|
||||
self.assertHttpRedirect(response, '?'.join([str(reverse_lazy('login')), 'next=/someurl1234']))
|
||||
|
||||
response = self.client.get(response['Location'])
|
||||
self.assertTrue(len(messages) > 0)
|
||||
|
||||
class TestIsPlaceSubmitterMixin(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
|
||||
place = Place.objects.create(
|
||||
name='Im a place',
|
||||
submitted_when=datetime.datetime.now(),
|
||||
submitted_by=user.explorer,
|
||||
location='Testtown',
|
||||
latitude=50.5,
|
||||
longitude=7.0,
|
||||
description='This is just a test, do not worry'
|
||||
)
|
||||
place.tags.add('I a tag', 'testlocation')
|
||||
place.save()
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def setUp(self):
|
||||
self. client = Client()
|
||||
|
||||
def test_is_submitter(self):
|
||||
self.client.login(username='testpeter', password='Develop123')
|
||||
response = self.client.get(reverse_lazy('place_edit', kwargs={'pk': 1}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_is_no_submitter(self):
|
||||
User.objects.create_user(
|
||||
username='manfred',
|
||||
password='Develop123'
|
||||
)
|
||||
self.client.login(username='manfred', password='Develop123')
|
||||
response = self.client.get(reverse_lazy('place_edit', kwargs={'pk': 1}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTrue(response.context['messages'])
|
||||
self.assertTrue(len(response.context['messages']) > 0)
|
126
django_lostplaces/lostplaces/tests/views/test_place_views.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import datetime
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from lostplaces.models import Place
|
||||
from lostplaces.views import (
|
||||
PlaceCreateView,
|
||||
PlaceListView,
|
||||
PlaceDetailView
|
||||
)
|
||||
from lostplaces.forms import PlaceImageCreateForm, PlaceForm
|
||||
from lostplaces.tests.views import (
|
||||
ViewTestCase,
|
||||
TaggableViewTestCaseMixin,
|
||||
MapableViewTestCaseMixin
|
||||
)
|
||||
|
||||
|
||||
class TestPlaceCreateView(ViewTestCase):
|
||||
view = PlaceCreateView
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
|
||||
place = Place.objects.create(
|
||||
name='Im a place',
|
||||
submitted_when=datetime.datetime.now(),
|
||||
submitted_by=user.explorer,
|
||||
location='Testtown',
|
||||
latitude=50.5,
|
||||
longitude=7.0,
|
||||
description='This is just a test, do not worry'
|
||||
)
|
||||
place.tags.add('I a tag', 'testlocation')
|
||||
place.save()
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_has_forms(self):
|
||||
self.client.login(username='testpeter', password='Develop123')
|
||||
response = self.client.get(reverse('place_create'))
|
||||
|
||||
self.assertHasForm(response, 'place_image_form', PlaceImageCreateForm)
|
||||
self.assertHasForm(response, 'place_form', PlaceForm)
|
||||
|
||||
|
||||
class TestPlaceListView(ViewTestCase):
|
||||
view = PlaceListView
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
|
||||
place = Place.objects.create(
|
||||
name='Im a place',
|
||||
submitted_when=datetime.datetime.now(),
|
||||
submitted_by=user.explorer,
|
||||
location='Testtown',
|
||||
latitude=50.5,
|
||||
longitude=7.0,
|
||||
description='This is just a test, do not worry'
|
||||
)
|
||||
place.tags.add('I a tag', 'testlocation')
|
||||
place.save()
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_list_view(self):
|
||||
self.client.login(username='testpeter', password='Develop123')
|
||||
response = self.client.get(reverse('place_list'))
|
||||
|
||||
self.assertContext(response, 'mapping_config')
|
||||
|
||||
|
||||
class PlaceDetailViewTestCase(TaggableViewTestCaseMixin, MapableViewTestCaseMixin, ViewTestCase):
|
||||
view = PlaceDetailView
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
|
||||
place = Place.objects.create(
|
||||
name='Im a place',
|
||||
submitted_when=datetime.datetime.now(),
|
||||
submitted_by=user.explorer,
|
||||
location='Testtown',
|
||||
latitude=50.5,
|
||||
longitude=7.0,
|
||||
description='This is just a test, do not worry'
|
||||
)
|
||||
place.tags.add('I a tag', 'testlocation')
|
||||
place.save()
|
||||
|
||||
def test_context(self):
|
||||
self.client.login(username='testpeter', password='Develop123')
|
||||
response = self.client.get(reverse('place_detail', kwargs={'pk': 1}))
|
||||
|
||||
self.assertTrue(
|
||||
'tagging_config' in response.context,
|
||||
msg='Expecting the context of %s to have an \'tagging_config\'' % (
|
||||
str(self.view)
|
||||
)
|
||||
)
|
||||
self.assertTaggableContext(response.context['tagging_config'])
|
||||
|
||||
self.assertTrue(
|
||||
'mapping_config' in response.context,
|
||||
msg='Expecting the context of %s to have an \'mapping_config\'' % (
|
||||
str(self.view)
|
||||
)
|
||||
)
|
||||
self.assertMapableContext(response.context['mapping_config'])
|
@@ -1,5 +1,5 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
from lostplaces.views import (
|
||||
HomeView,
|
||||
PlaceDetailView,
|
||||
PlaceListView,
|
||||
@@ -25,6 +25,6 @@ urlpatterns = [
|
||||
path('flat/<slug:slug>/', FlatView, name='flatpage'),
|
||||
|
||||
# POST-only URLs for tag submission
|
||||
path('place/tag/<int:place_id>', PlaceTagSubmitView.as_view(), name='place_tag_submit'),
|
||||
path('place/tag/<int:tagged_id>', PlaceTagSubmitView.as_view(), name='place_tag_submit'),
|
||||
path('place/tag/delete/<int:tagged_id>/<int:tag_id>', PlaceTagDeleteView.as_view(), name='place_tag_delete')
|
||||
]
|
3
django_lostplaces/lostplaces/views/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from lostplaces.views.base_views import *
|
||||
from lostplaces.views.views import *
|
||||
from lostplaces.views.place_views import *
|
@@ -9,9 +9,9 @@ from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from lostplaces_app.models import Place
|
||||
from lostplaces.models import Place
|
||||
|
||||
class IsAuthenticated(LoginRequiredMixin, View):
|
||||
class IsAuthenticatedMixin(LoginRequiredMixin, View):
|
||||
'''
|
||||
A view mixin that checks wether a user is loged in or not.
|
||||
If the user is not logged in, he gets redirected to
|
||||
@@ -24,7 +24,7 @@ class IsAuthenticated(LoginRequiredMixin, View):
|
||||
messages.error(self.request, self.permission_denied_message)
|
||||
return super().handle_no_permission()
|
||||
|
||||
class IsPlaceSubmitter(UserPassesTestMixin, View):
|
||||
class IsPlaceSubmitterMixin(UserPassesTestMixin, View):
|
||||
'''
|
||||
A view mixin that checks wethe a user is the submitter
|
||||
of a place Throws 403 if the user is not. The subclass
|
||||
@@ -55,7 +55,7 @@ class IsPlaceSubmitter(UserPassesTestMixin, View):
|
||||
messages.error(self.request, self.place_submitter_error_message)
|
||||
return False
|
||||
|
||||
class PlaceAssetCreateView(IsAuthenticated, SuccessMessageMixin, CreateView):
|
||||
class PlaceAssetCreateView(IsAuthenticatedMixin, SuccessMessageMixin, CreateView):
|
||||
model = None
|
||||
fields = []
|
||||
template_name = ''
|
||||
@@ -81,7 +81,7 @@ class PlaceAssetCreateView(IsAuthenticated, SuccessMessageMixin, CreateView):
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('place_detail', kwargs={'pk': self.place.id})
|
||||
|
||||
class PlaceAssetDeleteView(IsAuthenticated, IsPlaceSubmitter, SingleObjectMixin, View):
|
||||
class PlaceAssetDeleteView(IsAuthenticatedMixin, IsPlaceSubmitterMixin, SingleObjectMixin, View):
|
||||
model = None
|
||||
success_message = ''
|
||||
permission_denied_message = ''
|
@@ -9,13 +9,13 @@ from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.shortcuts import render, redirect
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from lostplaces_app.models import Place, PlaceImage
|
||||
from lostplaces_app.views import IsAuthenticated, IsPlaceSubmitter
|
||||
from lostplaces_app.forms import PlaceForm, PlaceImageCreateForm, TagSubmitForm
|
||||
from lostplaces.models import Place, PlaceImage
|
||||
from lostplaces.views import IsAuthenticatedMixin, IsPlaceSubmitterMixin
|
||||
from lostplaces.forms import PlaceForm, PlaceImageCreateForm, TagSubmitForm
|
||||
|
||||
from taggit.models import Tag
|
||||
|
||||
class PlaceListView(IsAuthenticated, ListView):
|
||||
class PlaceListView(IsAuthenticatedMixin, ListView):
|
||||
paginate_by = 5
|
||||
model = Place
|
||||
template_name = 'place/place_list.html'
|
||||
@@ -23,27 +23,32 @@ class PlaceListView(IsAuthenticated, ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['place_map_center'] = Place.average_latlon(context['place_list'])
|
||||
context['mapping_config'] = {
|
||||
'all_points': context['place_list'],
|
||||
'map_center': Place.average_latlon(context['place_list'])
|
||||
}
|
||||
return context
|
||||
|
||||
class PlaceDetailView(IsAuthenticated, View):
|
||||
class PlaceDetailView(IsAuthenticatedMixin, View):
|
||||
def get(self, request, pk):
|
||||
place = Place.objects.get(pk=pk)
|
||||
context = {
|
||||
'place': place,
|
||||
'place_list': [ place ],
|
||||
'place_map_center': [ place.latitude, place.longitude ],
|
||||
'all_tags': Tag.objects.all(),
|
||||
'mapping_config': {
|
||||
'all_points': [ place ],
|
||||
'map_center': {'latitude': place.latitude, 'longitude': place.longitude},
|
||||
},
|
||||
'tagging_config': {
|
||||
'submit_url': reverse_lazy('place_tag_submit', kwargs={'place_id': place.id}),
|
||||
'all_tags': Tag.objects.all(),
|
||||
'submit_form': TagSubmitForm(),
|
||||
'tagged_item': place,
|
||||
'submit_url_name': 'place_tag_submit',
|
||||
'delete_url_name': 'place_tag_delete'
|
||||
}
|
||||
}
|
||||
return render(request, 'place/place_detail.html', context)
|
||||
|
||||
class PlaceUpdateView(IsAuthenticated, IsPlaceSubmitter, SuccessMessageMixin, UpdateView):
|
||||
class PlaceUpdateView(IsAuthenticatedMixin, IsPlaceSubmitterMixin, SuccessMessageMixin, UpdateView):
|
||||
template_name = 'place/place_update.html'
|
||||
model = Place
|
||||
form_class = PlaceForm
|
||||
@@ -56,7 +61,7 @@ class PlaceUpdateView(IsAuthenticated, IsPlaceSubmitter, SuccessMessageMixin, Up
|
||||
def get_place(self):
|
||||
return self.get_object()
|
||||
|
||||
class PlaceCreateView(IsAuthenticated, View):
|
||||
class PlaceCreateView(IsAuthenticatedMixin, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
place_image_form = PlaceImageCreateForm()
|
||||
@@ -85,23 +90,19 @@ class PlaceCreateView(IsAuthenticated, View):
|
||||
submitter=submitter
|
||||
)
|
||||
|
||||
kwargs_to_pass = {
|
||||
'pk': place.pk
|
||||
}
|
||||
|
||||
messages.success(
|
||||
self.request, 'Successfully created place.')
|
||||
return redirect(reverse_lazy('place_detail', kwargs=kwargs_to_pass))
|
||||
self.request,
|
||||
'Successfully created place.'
|
||||
)
|
||||
return redirect(reverse_lazy('place_detail', kwargs={'pk': place.pk}))
|
||||
|
||||
else:
|
||||
context = {
|
||||
'form': form_place
|
||||
}
|
||||
|
||||
# Usually the browser should have checked the form before sending.
|
||||
messages.error(
|
||||
self.request, 'Please fill in all required fields.')
|
||||
return render(request, 'place/place_create.html', context)
|
||||
self.request,
|
||||
'Please fill in all required fields.'
|
||||
)
|
||||
return render(request, 'place/place_create.html', context={'form': form_place})
|
||||
|
||||
def _apply_multipart_image_upload(self, files, place, submitter):
|
||||
for image in files:
|
||||
@@ -112,7 +113,7 @@ class PlaceCreateView(IsAuthenticated, View):
|
||||
)
|
||||
place_image.save()
|
||||
|
||||
class PlaceDeleteView(IsAuthenticated, IsPlaceSubmitter, DeleteView):
|
||||
class PlaceDeleteView(IsAuthenticatedMixin, IsPlaceSubmitterMixin, DeleteView):
|
||||
template_name = 'place/place_delete.html'
|
||||
model = Place
|
||||
success_message = 'Successfully deleted place.'
|
@@ -7,11 +7,11 @@ from django.urls import reverse_lazy
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.http import HttpResponseForbidden
|
||||
|
||||
from lostplaces_app.forms import ExplorerCreationForm, TagSubmitForm
|
||||
from lostplaces_app.models import Place, PhotoAlbum
|
||||
from lostplaces_app.views.base_views import IsAuthenticated
|
||||
from lostplaces.forms import ExplorerCreationForm, TagSubmitForm
|
||||
from lostplaces.models import Place, PhotoAlbum
|
||||
from lostplaces.views.base_views import IsAuthenticatedMixin
|
||||
|
||||
from lostplaces_app.views.base_views import (
|
||||
from lostplaces.views.base_views import (
|
||||
PlaceAssetCreateView,
|
||||
PlaceAssetDeleteView,
|
||||
)
|
||||
@@ -24,13 +24,15 @@ class SignUpView(SuccessMessageMixin, CreateView):
|
||||
template_name = 'signup.html'
|
||||
success_message = 'User created.'
|
||||
|
||||
class HomeView(IsAuthenticated, View):
|
||||
class HomeView(IsAuthenticatedMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
place_list = Place.objects.all().order_by('-submitted_when')[:10]
|
||||
place_map_center = Place.average_latlon(place_list)
|
||||
context = {
|
||||
'place_list': place_list,
|
||||
'place_map_center': place_map_center
|
||||
'all_points': place_list,
|
||||
'mapping_config': {
|
||||
'point_list': place_list,
|
||||
'map_center': Place.average_latlon(place_list)
|
||||
}
|
||||
}
|
||||
return render(request, 'home.html', context)
|
||||
|
||||
@@ -53,9 +55,9 @@ class PhotoAlbumDeleteView(PlaceAssetDeleteView):
|
||||
success_message = 'Photo Album deleted'
|
||||
permission_denied_messsage = 'You do not have permissions to alter this photo album'
|
||||
|
||||
class PlaceTagSubmitView(IsAuthenticated, View):
|
||||
def post(self, request, place_id, *args, **kwargs):
|
||||
place = Place.objects.get(pk=place_id)
|
||||
class PlaceTagSubmitView(IsAuthenticatedMixin, View):
|
||||
def post(self, request, tagged_id, *args, **kwargs):
|
||||
place = Place.objects.get(pk=tagged_id)
|
||||
form = TagSubmitForm(request.POST)
|
||||
if form.is_valid():
|
||||
tag_list_raw = form.cleaned_data['tag_list']
|
||||
@@ -68,7 +70,7 @@ class PlaceTagSubmitView(IsAuthenticated, View):
|
||||
|
||||
return redirect(reverse_lazy('place_detail', kwargs={'pk': place.id}))
|
||||
|
||||
class PlaceTagDeleteView(IsAuthenticated, View):
|
||||
class PlaceTagDeleteView(IsAuthenticatedMixin, View):
|
||||
def get(self, request, tagged_id, tag_id, *args, **kwargs):
|
||||
place = Place.objects.get(pk=tagged_id)
|
||||
tag = Tag.objects.get(pk=tag_id)
|
@@ -7,7 +7,7 @@ import os
|
||||
import sys
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lostplaces.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_lostplaces.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
@@ -1,59 +0,0 @@
|
||||
{% extends 'global.html'%}
|
||||
{% load static %}
|
||||
|
||||
{% block additional_head %}
|
||||
<link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css">
|
||||
<script src="{% static 'maps/ol.js' %}"></script>
|
||||
{% endblock additional_head %}
|
||||
|
||||
{% block title %}Lost Places{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
|
||||
{% include 'partials/osm_map.html' %}
|
||||
<div class="LP-PlaceList">
|
||||
<h1 class="LP-Headline">Listing our places</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">
|
||||
<article class="LP-PlaceTeaser LP-PlaceTeaser--extended">
|
||||
<div class="LP-PlaceTeaser__Image">
|
||||
<img class="LP-Image" src="{{ place.images.first.filename.thumbnail.url }}" />
|
||||
</div>
|
||||
<div class="LP-PlaceTeaser__Meta">
|
||||
<div class="LP-PlaceTeaser__Info">
|
||||
<span class="LP-PlaceTeaser__Title">
|
||||
<h2 class="LP-Headline LP-Headline--teaser">{{place.name}}</h2>
|
||||
</span>
|
||||
<span class="LP-PlaceTeaser__Detail">
|
||||
<p class="LP-Paragraph">{{place.location}}</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="LP-PlaceTeaser__Description">
|
||||
<p class="LP-Paragraph">
|
||||
{% if place.description|length > 210 %}
|
||||
{{place.description|truncatechars:210|truncatewords:-1}}
|
||||
{% else %}
|
||||
{{place.description}}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="LP-PlaceTeaser__Icons">
|
||||
<ul class="LP-Icon__List">
|
||||
<li class="LP-Icon__Item"><img class="LP-Icon" src="{% static '/icons/favourite.svg' %}" /></li>
|
||||
<li class="LP-Icon__Item"><img class="LP-Icon" src="{% static '/icons/location.svg' %}" /></li>
|
||||
<li class="LP-Icon__Item"><img class="LP-Icon" src="{% static '/icons/flag.svg' %}" /></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% include 'partials/nav/pagination.html' %}
|
||||
|
||||
</div>
|
||||
{% endblock maincontent %}
|
@@ -1,13 +0,0 @@
|
||||
from django.db import models as django_models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
def mock_user():
|
||||
explorer_list = User.objects.all()
|
||||
if len(explorer_list) <= 0:
|
||||
return User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
else:
|
||||
return explorer_list[0]
|
@@ -1,121 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
class TestModel:
|
||||
'''
|
||||
Base class for Lostplaces models
|
||||
'''
|
||||
model_name = None
|
||||
|
||||
def _test_field(self, field_name, field_class):
|
||||
'''
|
||||
Tests if a field exists under the given name and
|
||||
if the field is of the right type
|
||||
'''
|
||||
field = self.object._meta.get_field(field_name)
|
||||
self.assertTrue(
|
||||
field,
|
||||
msg="%s has no field named '%s'" % (
|
||||
self.model_name,
|
||||
field_name
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
type(field), field_class,
|
||||
msg='%s.%s name field is no CharField' % (
|
||||
self.model_name,
|
||||
field_name
|
||||
)
|
||||
)
|
||||
return field
|
||||
|
||||
|
||||
def _test_char_field(self, field_name, min_length, max_length):
|
||||
'''
|
||||
Tests if the given field is a char field and if its max_length
|
||||
is in min_length and max_legth
|
||||
'''
|
||||
field = self._test_field(field_name, models.CharField)
|
||||
self.assertEqual(
|
||||
type(field), models.CharField,
|
||||
msg='%s.%s name field is no CharField' % (
|
||||
self.model_name,
|
||||
field_name
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
field.max_length in range(min_length, max_length),
|
||||
msg='%s.%s field max_length not in range of %d and %d' % (
|
||||
self.model_name,
|
||||
field_name,
|
||||
min_length,
|
||||
max_length
|
||||
)
|
||||
)
|
||||
|
||||
def _test_float_field(self, field_name, min_value=None, max_value=None):
|
||||
'''
|
||||
Tests if the field is a floatfield. If min_value and/or max_value are passed,
|
||||
the validators of the field are also checked. The validator list of the field should
|
||||
look like
|
||||
[MinValueValidator, MayValueValidator], if both values are passed,
|
||||
[MinValueValidator] if only min_value is passed,
|
||||
[MaxValueValidator] if only max_value is passed
|
||||
'''
|
||||
field = self._test_field(field_name, models.FloatField)
|
||||
if min_value:
|
||||
self.assertTrue(
|
||||
len(field.validators) >= 1,
|
||||
msg='%s.%s first validator should check minimum' % (
|
||||
self.model_name,
|
||||
field_name
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
field.validators[0].limit_value,
|
||||
min_value,
|
||||
msg='%s.%s min value missmatch' % (
|
||||
self.model_name,
|
||||
field_name
|
||||
)
|
||||
)
|
||||
if max_value:
|
||||
index = 0
|
||||
if min_value:
|
||||
index += 1
|
||||
self.assertTrue(
|
||||
len(field.validators) >= index+1,
|
||||
msg='%s.%s second validator should check maximum' % (
|
||||
self.model_name,
|
||||
field_name
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
field.validators[1].limit_value,
|
||||
max_value,
|
||||
msg='%s.%s max value missmatch' % (
|
||||
self.model_name,
|
||||
field_name
|
||||
)
|
||||
)
|
||||
|
||||
class TestSubmittable(TestModel):
|
||||
model_name='<Class>'
|
||||
related_name = None
|
||||
nullable = False
|
||||
|
||||
def test_submitted_when(self):
|
||||
submitted_when = self._test_field('submitted_when', models.DateTimeField)
|
||||
self.assertTrue(submitted_when.auto_now_add,
|
||||
msg='%s.submitted_when should be auto_now_add' % (
|
||||
self.model_name
|
||||
)
|
||||
)
|
||||
|
||||
def test_submitted_by(self):
|
||||
submitted_by = self._test_field('submitted_by',models.ForeignKey)
|
||||
if self.related_name:
|
||||
self.assertEqual(submitted_by.remote_field.related_name, self.related_name)
|
||||
if self.nullable:
|
||||
self.assertTrue(submitted_by.null,)
|
||||
self.assertTrue(submitted_by.blank)
|
||||
self.assertEqual(submitted_by.remote_field.on_delete, models.SET_NULL)
|
@@ -1,55 +0,0 @@
|
||||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
from django.core.files import File
|
||||
|
||||
from lostplaces_app.models import PlaceImage
|
||||
from lostplaces_app.tests.models import TestSubmittable
|
||||
from lostplaces_app.tests import mock_user
|
||||
from lostplaces_app.tests.models import TestModel
|
||||
from lostplaces_app.tests.models.test_place_model import mock_place
|
||||
|
||||
from easy_thumbnails.fields import ThumbnailerImageField
|
||||
|
||||
def mock_place_image():
|
||||
return PlaceImage(
|
||||
description='Im a description',
|
||||
filename=mock.MagicMock(spec=File, name='FileMock'),
|
||||
place=mock_place(),
|
||||
submitted_when=datetime.datetime.now(),
|
||||
submitted_by=mock_user().explorer
|
||||
)
|
||||
|
||||
class TestPlaceImage(TestSubmittable, TestCase):
|
||||
model_name = 'PlaceImage'
|
||||
|
||||
def setUp(self):
|
||||
self.object = mock_place_image()
|
||||
|
||||
def test_description(self):
|
||||
self._test_field('description', models.TextField)
|
||||
|
||||
def test_filename(self):
|
||||
self._test_field('filename',ThumbnailerImageField)
|
||||
|
||||
def test_place(self):
|
||||
field = self._test_field('place', models.ForeignKey)
|
||||
self.assertEqual(field.remote_field.on_delete, models.CASCADE,
|
||||
msg='%s.%s deleting of %s should be cascadinf' % (
|
||||
self.model_name,
|
||||
'place',
|
||||
self.model_name
|
||||
)
|
||||
)
|
||||
self.assertEqual(field.remote_field.related_name, 'images',
|
||||
msg='%s.%s related name should be images' % (
|
||||
self.model_name,
|
||||
'place'
|
||||
)
|
||||
)
|
||||
|
||||
def test_str(self):
|
||||
place_image = mock_place_image()
|
||||
self.assertEqual(str(place_image), ' '.join([place_image.place.name, str(place_image.pk)]))
|