Compare commits
80 Commits
0.1.2
...
ce91a19068
Author | SHA1 | Date | |
---|---|---|---|
ce91a19068 | |||
17b531e181 | |||
222c97d63c | |||
6a1229a4ba | |||
2bff4db1d7 | |||
a3b123a207 | |||
e698f3b224 | |||
c66baaa765 | |||
815be3126e | |||
715e953182 | |||
86915f6ec5 | |||
30b42de86f | |||
21217b067f | |||
9b160d2df0 | |||
74a9ee4f39 | |||
b0e775d299 | |||
d6b82f1556 | |||
3f642daf89 | |||
858cfe1d3c | |||
943d647a47 | |||
d96cf9470a | |||
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
|
||||
@@ -131,9 +131,15 @@ USE_TZ = True
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
|
||||
|
||||
# Upload directory
|
||||
MEDIA_URL = '/uploads/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
||||
|
||||
# Thumbnails
|
||||
THUMBNAIL_MEDIA_ROOT = os.path.join(MEDIA_ROOT, 'thumbs/')
|
||||
THUMBNAIL_MEDIA_URL = os.path.join(MEDIA_URL, 'thumbs/')
|
||||
THUMBNAIL_QUALITY = 75
|
||||
|
||||
# Templates to use for authentication
|
||||
LOGIN_URL = reverse_lazy('login')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
@@ -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()
|
75
django_lostplaces/docs/developer.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# django lostplaces documentation for developer
|
||||
|
||||
Greetings,
|
||||
this documentation is for anyone who wants to work with this projects codebase or is just curious about our project.
|
||||
|
||||
|
||||
## Models
|
||||
The models contain an custom user profile, some abstract base models and the models holding the actual data for this project
|
||||
|
||||
|
||||
### Explorer user profile
|
||||
The class `lostplaces.models.Explorer` is our custom user profile. It has an ForeignKey pointing at django's default user model instead of providing an entire custom user model. That way this django app does not conflict with any other app that has to bring it's own user model.
|
||||
|
||||
You can access the explorer profile by accessing the 'explorer' attribute of any user instance
|
||||
```python
|
||||
user.explorere
|
||||
```
|
||||
|
||||
Currently the explorer profile is used by the abstract model 'Submittable' and thus referenced by 'Place' and 'PlaceImage'. The explorer profile therefore has two attributes
|
||||
```python
|
||||
user.explorer.places
|
||||
user.explorere.placeimages
|
||||
```
|
||||
`places`
|
||||
A list containing all (lost) places the user has submitted
|
||||
`placeimages`
|
||||
A list containing all images relating a place that a user has submitted
|
||||
|
||||
|
||||
### Taggable
|
||||
The abstract model Taggable represents an model that is taggable. It depends on the django app [taggit](https://github.com/jazzband/django-taggit). It only consists of one field:
|
||||
`tag`
|
||||
TaggableManager, allows the sub class to be tagged, blank=True allows the admin form to be submitted without any tags
|
||||
|
||||
|
||||
### Mapable
|
||||
The abstract model Mapable represents an model that can be displayed on a map. It consists of tree members
|
||||
`name`
|
||||
Name of the object, displayed on the map, max length 50 characeter
|
||||
`latitude`
|
||||
Latitude of the referenced location, -90 <= value <= 90
|
||||
`longitude`
|
||||
Longitude of the referenced location -180 <= value <= 180
|
||||
A mapable model has to provide its own get_aboslute_url, in order to provide a link when clicked.
|
||||
|
||||
|
||||
### Submittable
|
||||
The abstract model Submittable represents an model that can be submitted by an user. It knows who submitted something and when:
|
||||
`submitted_by`
|
||||
Referencing the explorer profile, see [Explorer](##explorer-user-profile). If the explorer profile is deleted, this instance is kept (on_delete=models.SET_NULL). The related_name is set to the class name, lower case appending an s (%(class)s)
|
||||
`submitted_when`
|
||||
When the object was submitted, automaticly set by django (auto_now_add=True)
|
||||
|
||||
|
||||
### Voucher
|
||||
A voucher code is needed to sign up using lostplaces sign up form. The model contains
|
||||
`code`
|
||||
The voucher code, max length 30 character
|
||||
`created_when`
|
||||
When the voucher was created automaticly set by django (auto_now_add=True)
|
||||
`expires_when`
|
||||
Till what date the voucher remains valid
|
||||
|
||||
|
||||
### Place
|
||||
The place model is the heart of this project. It stores all information about a place needed.
|
||||
`location`
|
||||
Human readable location description (town, village, street), max length 50 character
|
||||
`description`
|
||||
Describing the place in detail
|
||||
The place model uses these abstract super classes
|
||||
- Submittable, see [Submittable](##submittable) for additional fields
|
||||
- Taggable, see [Taggable](##taggable) for additional fields
|
||||
- Mapable, see [Mapable](##mapable) for additional fields
|
||||
The average_latlon function takes a list of places and returns the center point of all these places
|
11
django_lostplaces/lostplaces/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.conf import settings
|
||||
|
||||
settings.THUMBNAIL_ALIASES = {
|
||||
'': {
|
||||
'thumbnail': {'size': (300, 200), 'sharpen': True, 'crop': True},
|
||||
'hero': {'size': (700, 466), 'sharpen': True, 'crop': True},
|
||||
'large': {'size': (1920, 1920), 'sharpen': True, 'crop': False},
|
||||
},
|
||||
}
|
38
django_lostplaces/lostplaces/admin.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
''' Classes and modules for the administrative backend. '''
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from lostplaces.models import *
|
||||
|
||||
from lostplaces.forms import ExplorerCreationForm, ExplorerChangeForm
|
||||
|
||||
# Register your models here.
|
||||
|
||||
class VoucherAdmin(admin.ModelAdmin):
|
||||
fields = ['code', 'expires_when', 'created_when']
|
||||
readonly_fields = ['created_when']
|
||||
list_display = ('__str__', 'code', 'created_when', 'expires_when', 'valid')
|
||||
|
||||
def valid(self, instance):
|
||||
return timezone.now() <= instance.expires_when
|
||||
|
||||
valid.boolean = True
|
||||
|
||||
class PhotoAlbumsAdmin(admin.ModelAdmin):
|
||||
list_display = ('label', 'place', 'url' )
|
||||
|
||||
class PlacesAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'submitted_by', 'submitted_when')
|
||||
|
||||
class PlaceImagesAdmin(admin.ModelAdmin):
|
||||
list_display = ('__str__', 'place', 'submitted_by')
|
||||
|
||||
admin.site.register(Explorer)
|
||||
admin.site.register(Voucher, VoucherAdmin)
|
||||
admin.site.register(Place, PlacesAdmin)
|
||||
admin.site.register(PlaceImage, PlaceImagesAdmin)
|
||||
admin.site.register(PhotoAlbum, PhotoAlbumsAdmin)
|
@@ -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:
|
||||
@@ -26,6 +26,10 @@ class ExplorerCreationForm(UserCreationForm):
|
||||
self.add_error('voucher', 'Invalid voucher')
|
||||
return False
|
||||
|
||||
if not submitted_voucher.valid:
|
||||
self.add_error('voucher', 'Expired voucher')
|
||||
return False
|
||||
|
||||
fetched_voucher.delete()
|
||||
return True
|
||||
|
@@ -9,17 +9,19 @@ 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
|
||||
from django.dispatch import receiver
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.utils import timezone
|
||||
from easy_thumbnails.fields import ThumbnailerImageField
|
||||
from easy_thumbnails.files import get_thumbnailer
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Explorer(models.Model):
|
||||
"""
|
||||
Profile that is linked to the a User.
|
||||
@@ -33,7 +35,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 +46,25 @@ def create_user_profile(sender, instance, created, **kwargs):
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
instance.explorer.save()
|
||||
|
||||
class Taggable(models.Model):
|
||||
'''
|
||||
This abstract model represtens an object that is taggalble
|
||||
using django-taggit
|
||||
'''
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
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.
|
||||
"""
|
||||
tags = TaggableManager(blank=True)
|
||||
|
||||
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 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 +77,59 @@ 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()
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
return timezone.now() <= self.expires_when
|
||||
|
||||
def __str__(self):
|
||||
return "Voucher " + str(self.pk)
|
||||
|
||||
|
||||
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 +139,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
|
||||
@@ -114,32 +150,29 @@ class Place (models.Model):
|
||||
def generate_image_upload_path(instance, filename):
|
||||
"""
|
||||
Callback for generating path for uploaded images.
|
||||
Returns filename as: placepk-placename{-rndstring}.jpg
|
||||
"""
|
||||
|
||||
return 'places/' + str(uuid.uuid4())+'.'+filename.split('.')[-1]
|
||||
return 'places/' + str(instance.place.pk) + '-' + str(instance.place.name) + '.' + 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.
|
||||
Intermediate image sizes are generated as defined in THUMBNAIL_ALIASES.
|
||||
PlaceImage references a Place to which it belongs.
|
||||
"""
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
filename = ThumbnailerImageField(upload_to=generate_image_upload_path)
|
||||
filename = ThumbnailerImageField(
|
||||
upload_to=generate_image_upload_path,
|
||||
resize_source=dict(size=(2560, 2560),
|
||||
sharpen=True)
|
||||
)
|
||||
place = models.ForeignKey(
|
||||
Place,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='images'
|
||||
)
|
||||
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'
|
||||
related_name='placeimages'
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
@@ -148,20 +181,21 @@ class PlaceImage (models.Model):
|
||||
of this image as textual represntation of this instance
|
||||
"""
|
||||
|
||||
return ' '.join([self.place.name, str(self.pk)])
|
||||
return 'Image ' + str(self.pk)
|
||||
|
||||
|
||||
# These two auto-delete files from filesystem when they are unneeded:
|
||||
|
||||
|
||||
@receiver(models.signals.post_delete, sender=PlaceImage)
|
||||
def auto_delete_file_on_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes file from filesystem
|
||||
Deletes file (including thumbnails) from filesystem
|
||||
when corresponding `PlaceImage` object is deleted.
|
||||
"""
|
||||
if instance.filename:
|
||||
if os.path.isfile(instance.filename.path):
|
||||
os.remove(instance.filename.path)
|
||||
# Get and delete all files and thumbnails from instance
|
||||
thumbmanager = get_thumbnailer(instance.filename)
|
||||
thumbmanager.delete(save=False)
|
||||
|
||||
|
||||
@receiver(models.signals.pre_save, sender=PlaceImage)
|
||||
@@ -179,6 +213,7 @@ def auto_delete_file_on_change(sender, instance, **kwargs):
|
||||
except PlaceImage.DoesNotExist:
|
||||
return False
|
||||
|
||||
# No need to delete thumbnails, as they will be overwritten on regeneration.
|
||||
new_file = instance.filename
|
||||
if not old_file == new_file:
|
||||
if os.path.isfile(old_file.path):
|
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 |
@@ -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.all_tags %}
|
||||
'{{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,101 @@
|
||||
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_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,44 @@
|
||||
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}
|
||||
)
|
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
|
||||
'mapping_config': {
|
||||
'all_points': 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:
|
8
django_lostplaces/setup.cfg
Normal file
@@ -0,0 +1,8 @@
|
||||
# Config options for coverage
|
||||
# Docs: https://coverage.readthedocs.io/en/latest/config.html
|
||||
|
||||
[coverage:run]
|
||||
source = .
|
||||
|
||||
[coverage:report]
|
||||
show_missing = True
|
5500
django_lostplaces/testdata/testdata.json
vendored
Normal file
21
django_lostplaces/testdata/testdata.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# testdata
|
||||
|
||||
## Database content
|
||||
testdata is provided in this repository / directory in testdata.json. It has been
|
||||
dumped using:
|
||||
|
||||
```
|
||||
manage.py dumpdata --all --exclude=auth --exclude=sessions --indent 4 --o testdata/testdata.json
|
||||
```
|
||||
|
||||
You can import it using
|
||||
|
||||
```
|
||||
manage.py loaddata testdata.json
|
||||
```
|
||||
|
||||
## Images
|
||||
Although I created pretty small testimages, I think they are still too clunky to
|
||||
mindlessly dump it into the code repository, so I provide an
|
||||
[archive](https://www.commander1024.de/lostplaces-testdata.zip) containing
|
||||
a folder structure of images to be extracted into the uploads/ folder.
|