19 Commits

Author SHA1 Message Date
aed2856df3 Made the test timezone aware, DateTimeFiled(auto_now_add) already was. 2020-09-11 22:22:03 +02:00
c78858c152 Changed class attibutes to match test expectation. 2020-09-11 22:03:20 +02:00
f49581259e Typo in class name. 2020-09-11 19:32:07 +02:00
f5bf642cd6 Ups, not for the settings file. 2020-09-11 19:23:13 +02:00
7687acb366 Changed scope (?) of remaining local imports. 2020-09-11 19:15:39 +02:00
e655e1598a Trying to test deletion, wip 2020-09-11 12:09:51 +02:00
64ed38332f Refactoring and testing __str__ 2020-09-11 12:09:34 +02:00
d438303aec file for model tests 2020-09-11 12:08:43 +02:00
38b3736951 New Model tests 2020-09-11 12:08:27 +02:00
6be060ea40 Refactoring and more keywords to test 2020-09-11 12:08:15 +02:00
5c5756150f More dev dep 2020-09-11 12:07:57 +02:00
6e6f4ced7b More about the project 2020-09-10 23:34:24 +02:00
b806f7f9e3 tweaks 2020-09-10 23:33:46 +02:00
98c5ddad65 Updated release process 2020-09-10 23:07:51 +02:00
654cd87006 Merge branch 'master' of mowoe.com:reverend/lostplaces-backend into master 2020-09-10 22:30:32 +02:00
533efb328c Switching from a custom user model to a user profile *yikes* 2020-09-10 22:30:29 +02:00
5b1b23d3e6 Linked to django 3.1, added local_settings import to the very end of file. 2020-09-10 22:20:44 +02:00
bcd003a685 Typos and punctuation. 2020-09-10 22:17:46 +02:00
c4adc3f434 Dev dep 2020-09-10 21:51:03 +02:00
24 changed files with 392 additions and 368 deletions

5
.gitignore vendored
View File

@@ -94,4 +94,7 @@ env.bak/
venv.bak/
# Editors
.vscode
.vscode
# twine / pypi
.pypirc

View File

@@ -1,5 +1,5 @@
include LICENSE
include README.rst
include Readme.rst
include Pipfile
recursive-include lostplaces_app/static *
recursive-include lostplaces_app/templates *

View File

@@ -8,6 +8,10 @@ pylint = "*"
coverage = "*"
autopep8 = "*"
pipenv = "*"
wheel = "*"
twine = "*"
pandoc ="*"
pylint-django ="*"
[packages]
django = "*"

View File

@@ -4,18 +4,21 @@ lostplaces-backend is a django (3.x) based webproject. It once wants to become a
The software is currently in early development status, neither scope, datalmodel(s) nor features are finalized yet. Therefore we would not recommend to download or install this piece of software anywhere - except your local django dev server.
We value privacy as a whole, all ressources the frontend requires will be shipped with lostplace's distribution. We also try to minimze the use of JavaScript as far as we can and try to offer JS-less alternatives where we can.
## Features
- manage lost places with lots of usefull information
- sensitive information is not accesiable for anonymous (not logged in) users
- user self registration using a voucher system, only people you invite can join your instance
- collaboration, every user can add informations like tags, photos and external links to your place
- Manage lost places with lots of usefull information.
- OSM-Maps
- Sensitive information is not accesiable for anonymous (not logged in) users.
- User self registration using a voucher system, only people you invite can join your instance.
- Collaboration, every user can add informations like tags, photos and external links to your place.
## Dependencies
Right now it depends on the following non-core Python 3 libraries. These can be installed using the package manager of your distribution or into the venv locally.
* [django](https://www.djangoproject.com/) django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.
* [easy-thumbnails](https://github.com/SmileyChris/easy-thumbnails) A powerful, yet easy to implement thumbnailing application for Django 1.11+
* [image](https://github.com/francescortiz/image) Image cropping for django
* [django](https://www.djangoproject.com/) is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.
* [easy-thumbnails](https://github.com/SmileyChris/easy-thumbnails) A powerful, yet easy to implement thumbnailing application for Django 1.11+.
* [image](https://github.com/francescortiz/image) Image cropping for django.
* [django-widget-tweaks](https://github.com/jazzband/django-widget-tweaks) Tweak the form field rendering in templates, not in python-level form definitions.
* [django-taggit](https://github.com/jazzband/django-taggit) A simpler approach to tagging with Django.
@@ -48,42 +51,44 @@ $ pipenv shell
```
Visit: [admin](http://localhost:8000/admin) for administrative backend or
[frontend](http://localhost:8000/)
[frontend](http://localhost:8000/).
Happy developing ;-)
# Installing a productive instance
Currently there are two ways to deploy the lostplaces project:
1. Cloning this repository, including the configured django instance
2. Install the package and setup the django instance your self
1. Cloning this repository, including the configured django instance.
2. Install the package and setup the django instance yourself.
## Cloning the repository
Essently, this is the same as installing an development instance, but without the development server (manage.py runserver) and something powerfull (Apache, NGINX) instead. You have to configure the webserve to work with the *SGI Api respectivly, reference [django's guide for deployment](https://docs.djangoproject.com/en/3.1/howto/deployment/) for further information.
Essentially, this is the same as installing a development instance, but without the development server (manage.py runserver) and something powerfull (Apache, NGINX) instead. You have to configure the webserve to work with the *SGI Api respectivly, reference [django's guide for deployment](https://docs.djangoproject.com/en/3.1/howto/deployment/) for further information.
You also should setup an dedicated database server, the build in SQLite file is not recommened for production use. Reference [django's guide for databases](https://docs.djangoproject.com/en/3.1/ref/databases/) for further information.
You also should setup a dedicated database server, the built-in SQLite file is not recommened for production use. Reference [django's guide for databases](https://docs.djangoproject.com/en/3.1/ref/databases/) for further information.
Before making the django instance public, you should tweak the config `settings.py`:
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64))
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/)
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64)).
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 `lostplaces/managy.py collectstatic` and you should be ready to go.
## Installing the lostplaces_app to an existing django instance
### Installing django and the lostplaces app
If you haven't already setup an django instance, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/install/).
If you haven't already setup a django instance, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/install/).
After that, download the desired release (probably the latest one) [from the realeases page](https://git.mowoe.com/reverend/lostplaces-backend/releases) and install it using `pip install --user name-of-the-file.tar.gz`.
After that, download the desired release (probably the latest one) [from the realeases page](https://git.mowoe.com/reverend/lostplaces-backend/releases) and install it using `pip install --user name-of-the-file.tar.gz`
*Note: You can run pip install without the --user flag, which will require root privileges and introduces potential security issues*
*Note: You can run pip install without the --user flag, which will require root privileges and introduces potential security issues.*
### Configuring the django instance
Now configure your `settings.py` as follows:
1. Add the following apps to the django project
1. Add the following apps to the django project.
```python
INSTALLED_APPS = [
@@ -95,7 +100,7 @@ INSTALLED_APPS = [
]
```
2. Set the URL's and Root-directories for file handling, for example
2. Set the URL's and Root-directories for file handling, for example:
```python
STATIC_URL = '/static/'
@@ -105,13 +110,13 @@ MEDIA_URL = '/uploads/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
```
3. Set the user model (this will be changed in the next release)
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
4. Set the URL's for login, for example:
```python
LOGIN_URL = reverse_lazy('login')
@@ -120,21 +125,19 @@ LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
```
### Configuring the URL's
In the `urls.py` configure the `urlpatter` like this
In the `urls.py` configure the `urlpatter` like this:
```python
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
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # So django can deliver user uploaded files
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.
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # So django can deliver user uploaded files.
```
Before making the django instance public, you should tweak the config `settings.py`:
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64))
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/)
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64)).
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.
Happy developing ;-)

View File

@@ -1,199 +0,0 @@
lostplaces-backend
==================
lostplaces-backend is a django (3.x) based webproject. It once wants to
become a software which allows a group of urban explorers to manage,
document and share the locations of lost places while not exposing too
much / any information to the public.
The software is currently in early development status, neither scope,
datalmodel(s) nor features are finalized yet. Therefore we would not
recommend to download or install this piece of software anywhere -
except your local django dev server.
Features
--------
- manage lost places with lots of usefull information
- sensitive information is not accesiable for anonymous (not logged in)
users
- user self registration using a voucher system, only people you invite
can join your instance
- collaboration, every user can add informations like tags, photos and
external links to your place
Dependencies
------------
Right now it depends on the following non-core Python 3 libraries. These
can be installed using the package manager of your distribution or into
the venv locally.
- `django <https://www.djangoproject.com/>`__ django is a high-level
Python Web framework that encourages rapid development and clean,
pragmatic design.
- `easy-thumbnails <https://github.com/SmileyChris/easy-thumbnails>`__
A powerful, yet easy to implement thumbnailing application for Django
1.11+
- `image <https://github.com/francescortiz/image>`__ Image cropping for
django
- `django-widget-tweaks <https://github.com/jazzband/django-widget-tweaks>`__
Tweak the form field rendering in templates, not in python-level form
definitions.
- `django-taggit <https://github.com/jazzband/django-taggit>`__ A
simpler approach to tagging with Django.
Installing a development instance
=================================
Clone the repository
--------------------
``git clone https://git.mowoe.com/reverend/lostplaces-backend.git`` ##
Setting up a (pipenv) virtual environment for development
After having obtained the repository contents (either via .zip download
or git clone), you can easily setup a
`pipenv <https://docs.pipenv.org/>`__ virtual environment. The repo
provides a Pipfile for easy dependency management that does not mess
with your system.
.. code:: shell
$ 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
Returning to the venv
---------------------
.. code:: 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
Visit: `admin <http://localhost:8000/admin>`__ for administrative
backend or `frontend <http://localhost:8000/>`__
Installing a productive instance
================================
Currently there are two ways to deploy the lostplaces project: 1.
Cloning this repository, including the configured django instance 2.
Install the package and setup the django instance your self
Cloning the repository
----------------------
Essently, this is the same as installing an development instance, but
without the development server (manage.py runserver) and something
powerfull (Apache, NGINX) instead. You have to configure the webserve to
work with the \*SGI Api respectivly, reference `django's guide for
deployment <https://docs.djangoproject.com/en/3.1/howto/deployment/>`__
for further information.
You also should setup an dedicated database server, the build in SQLite
file is not recommened for production use. Reference `django's guide for
databases <https://docs.djangoproject.com/en/3.1/ref/databases/>`__ for
further information.
Before making the django instance public, you should tweak the config
``settings.py``: 1. Change the secret key, the one found in the config
is already public. Choose something secure (i.e.
`this <https://duckduckgo.com/?q=password+generator+64>`__) 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.
Installing the lostplaces\_app to an existing django instance
-------------------------------------------------------------
Installing django and the lostplaces app
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you haven't already setup an django instance, see `django's
documentation <https://docs.djangoproject.com/en/3.1/topics/install/>`__.
After that, download the desired release (probably the latest one) `from
the realeases
page <https://git.mowoe.com/reverend/lostplaces-backend/releases>`__ and
install it using ``pip install --user name-of-the-file.tar.gz``.
*Note: You can run pip install without the --user flag, which will
require root privileges and introduces potential security issues*
Configuring the django instance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now configure your ``settings.py`` as follows: 1. Add the following apps
to the django project
.. code:: python
INSTALLED_APPS = [
...
'lostplaces_app',
'easy_thumbnails',
'widget_tweaks',
'django_taggit'
]
2. Set the URL's and Root-directories for file handling, for example
.. code:: python
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
MEDIA_URL = '/uploads/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
3. Set the user model (this will be changed in the next release)
.. code:: python
AUTH_USER_MODEL = 'lostplaces_app.Explorer'
4. Set the URL's for login, for example
.. code:: python
LOGIN_URL = reverse_lazy('login')
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
Configuring the URL's
~~~~~~~~~~~~~~~~~~~~~
In the ``urls.py`` configure the ``urlpatter`` like this
.. code:: python
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
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # So django can deliver user uploaded files
Before making the django instance public, you should tweak the config
``settings.py``: 1. Change the secret key, the one found in the config
is already public. Choose something secure (i.e.
`this <https://duckduckgo.com/?q=password+generator+64>`__) 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.
Happy developing ;-)

View File

@@ -4,13 +4,16 @@
"""
Django settings for lostplaces project.
Generated by 'django-admin startproject' using Django 3.0.8.
!!! Instead of changing this file, you can override settings in !!!
!!! local_settings.py. That way it won't cause merge conflicts on pull. !!!
Generated by 'django-admin startproject' using Django 3.1.8.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
@@ -21,7 +24,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'n$(bx8(^)*wz1ygn@-ekt7rl^1km*!_c+fwwjiua8m@-x_rpl0'
@@ -79,7 +82,7 @@ WSGI_APPLICATION = 'lostplaces.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
@@ -90,7 +93,7 @@ DATABASES = {
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
@@ -109,7 +112,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
@@ -123,7 +126,7 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
@@ -131,10 +134,15 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
MEDIA_URL = '/uploads/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
# Use custom user model
AUTH_USER_MODEL = 'lostplaces_app.Explorer'
# Templates to use for authentication
LOGIN_URL = reverse_lazy('login')
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
# Load and override settings from local_settings.py and don't fail on errors.
try:
from .local_settings import *
except ImportError:
pass

View File

@@ -6,23 +6,17 @@
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_app.models import *
from .forms import ExplorerCreationForm, ExplorerChangeForm
from lostplaces_app.forms import ExplorerCreationForm, ExplorerChangeForm
# Register your models here.
class ExplorerAdmin(UserAdmin):
add_form = ExplorerCreationForm
form = ExplorerChangeForm
model = Explorer
list_display = ['email', 'username',]
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, ExplorerAdmin)
admin.site.register(Explorer)
admin.site.register(Voucher, VoucherAdmin)
admin.site.register(Place)
admin.site.register(PlaceImage)

View File

@@ -5,11 +5,12 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import Explorer, Place, PlaceImage, Voucher
from django.contrib.auth.models import User
from lostplaces_app.models import Place, PlaceImage, Voucher
class ExplorerCreationForm(UserCreationForm):
class Meta:
model = Explorer
model = User
fields = ('username', 'email')
voucher = forms.CharField(
max_length=30,
@@ -30,7 +31,7 @@ class ExplorerCreationForm(UserCreationForm):
class ExplorerChangeForm(UserChangeForm):
class Meta:
model = Explorer
model = User
fields = ('username', 'email')
class PlaceForm(forms.ModelForm):

View File

@@ -10,22 +10,40 @@ import os
import uuid
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.contrib.auth.models import AbstractUser
from django.core.validators import MaxValueValidator, MinValueValidator
from easy_thumbnails.fields import ThumbnailerImageField
from taggit.managers import TaggableManager
# Create your models here.
class Explorer(AbstractUser):
class Explorer(models.Model):
"""
Custom user model
Addtional fields wbd
Profile that is linked to the a User.
Every user has a profile.
"""
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='explorer'
)
def __str__(self):
return self.username
return self.user.name
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Explorer.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.explorer.save()
class Voucher(models.Model):
"""
@@ -36,11 +54,12 @@ class Voucher(models.Model):
"""
code = models.CharField(unique=True, max_length=30)
created = models.DateTimeField(auto_now_add=True)
expires = models.DateField()
created_when = models.DateTimeField(auto_now_add=True)
expires_when = models.DateTimeField()
def __str__(self):
return "Voucher " + str(self.pk)
return "Voucher " + str(self.code)
class Place (models.Model):
"""
@@ -53,7 +72,7 @@ class Place (models.Model):
Explorer,
on_delete=models.SET_NULL,
null=True,
blank=True,
blank=True,
related_name='places'
)
location = models.CharField(max_length=50)
@@ -79,7 +98,7 @@ class Place (models.Model):
# Init fill values to prevent None
longitude = 0
latitude = 0
if amount > 0:
for place in place_list:
longitude += place.longitude
@@ -91,6 +110,7 @@ class Place (models.Model):
def __str__(self):
return self.name
def generate_image_upload_path(instance, filename):
"""
Callback for generating path for uploaded images.
@@ -98,13 +118,14 @@ def generate_image_upload_path(instance, filename):
return 'places/' + str(uuid.uuid4())+'.'+filename.split('.')[-1]
class PlaceImage (models.Model):
"""
PlaceImage defines an image file object that points to a file in uploads/.
Intermediate image sizes are generated as defined in SIZES.
PlaceImage references a Place to which it belongs.
"""
description = models.TextField(blank=True)
filename = ThumbnailerImageField(upload_to=generate_image_upload_path)
place = models.ForeignKey(
@@ -117,7 +138,7 @@ class PlaceImage (models.Model):
Explorer,
on_delete=models.SET_NULL,
null=True,
blank=True,
blank=True,
related_name='images'
)
@@ -130,6 +151,8 @@ class PlaceImage (models.Model):
return ' '.join([self.place.name, 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):
"""
@@ -140,6 +163,7 @@ def auto_delete_file_on_delete(sender, instance, **kwargs):
if os.path.isfile(instance.filename.path):
os.remove(instance.filename.path)
@receiver(models.signals.pre_save, sender=PlaceImage)
def auto_delete_file_on_change(sender, instance, **kwargs):
"""
@@ -160,6 +184,7 @@ def auto_delete_file_on_change(sender, instance, **kwargs):
if os.path.isfile(old_file.path):
os.remove(old_file.path)
class ExternalLink(models.Model):
url = models.URLField(max_length=200)
label = models.CharField(max_length=100)
@@ -167,11 +192,12 @@ class ExternalLink(models.Model):
Explorer,
on_delete=models.SET_NULL,
null=True,
blank=True,
blank=True,
related_name='external_links'
)
submitted_when = models.DateTimeField(auto_now_add=True, null=True)
class PhotoAlbum(ExternalLink):
place = models.ForeignKey(
Place,

View File

@@ -6,7 +6,7 @@
<a href="#" class="LP-Link">
<span class="LP-Link__Text">{{tag}}</span>
</a>
{% if request.user and request.user == config.tagged_item.submitted_by %}
{% if request.user and request.user.explorer == config.tagged_item.submitted_by %}
<a href="{% url config.delete_url_name tagged_id=config.tagged_item.id tag_id=tag.id %}" class="LP-Link">
<span class="LP-Tag__Remove RV-Iconized__Container RV-Iconized__Container--extraSmall">
<svg class="RV-Iconized__Icon" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
@@ -28,7 +28,7 @@
<legend class="LP-Form__Legend">Tags hinzufügen</legend>
{% csrf_token %}
<div class="LP-Form__Composition LP-Form__Composition--breakable">
<div class="LP-Form__Field LP-Form__Button LP-Input LP-Input--tagging">
<div class="LP-Form__Field LP-Form__Button LP-Input LP-Input--tagging">
<button id="id_tag_submit_button" class="LP-Button"> Tags hinzufügen</button>
</div>
<div class="LP-Form__Field">
@@ -46,11 +46,15 @@
submit_form.onsubmit = () => false
const tagify = new Tagify(input, {
'whitelist': [
{% for tag in all_tags %}
'{{tag}}',
{% endfor %}
]
'whitelist': [{
%
for tag in all_tags %
}
'{{tag}}',
{
% endfor %
}
]
})
const on_form_submit = function() {

View File

@@ -62,7 +62,7 @@
<a target="_blank" href="{{photo_album.url}}" class="LP-Link">
<span class="LP-Text">{{photo_album.label}}</span>
</a>
{% if user == photo_album.submitted_by or user == place.submitted_by %}
{% if user.explorer == photo_album.submitted_by or user.explorer == place.submitted_by %}
<a href="{% url 'photo_album_delete' pk=photo_album.pk%}" class="LP-Link LP-LinkList__ItemHover" title="Delete Photo Album">
<div class="RV-Iconized__Container RV-Iconized__Container--small">
{% icon 'trash' className="RV-Iconized__Icon" %}

View File

@@ -1,11 +1,11 @@
from django.db import models as django_models
from lostplaces_app.models import Explorer
from django.contrib.auth.models import User
def mock_user():
explorer_list = Explorer.objects.all()
explorer_list = User.objects.all()
if len(explorer_list) <= 0:
return Explorer.objects.create_user(
return User.objects.create_user(
username='testpeter',
password='Develop123'
)

View File

@@ -1,58 +1,99 @@
from django.db import models
from django.core.exceptions import FieldDoesNotExist
from django.test import TestCase
class TestModel:
class ModelTestCase:
'''
Base class for Lostplaces models
'''
model_name = None
def _test_field(self, field_name, field_class):
def _test_field(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
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)
'''
field = self.object._meta.get_field(field_name)
self.assertTrue(
field,
msg="%s has no field named '%s'" % (
self.model_name,
field_name
try:
field = self.object._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='%s.%s name field is no CharField' % (
type(field), field_class,
msg='Expecting type of %s.%s to be %s' % (
self.model_name,
field_name
field_name,
field_class.__name__
)
)
for key, value in must_have.items():
if value:
self.assertEqual(getattr(field, key), value,
msg='Expeting %s.%s.%s to be \'%s\'' % (
self.model_name,
field_name,
key,
value
)
)
else:
self.assertTrue(hasattr(field, key),
msg='Expeting %s.%s to have \'%s\'' % (
self.model_name,
field_name,
key
)
)
for key, value in must_not_have.items():
if value:
self.assertTrue(getattr(field, key) != value,
msg='Expeting %s.%s.%s to not be \'%s\'' % (
self.model_name,
field_name,
key,
value
)
)
else:
self.assertFalse(hasattr(field, value),
msg='Expeting %s.%s to not have \'%s\'' % (
self.model_name,
field_name,
key
)
)
return field
def _test_char_field(self, field_name, min_length, max_length):
def _test_char_field(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._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
)
)
field = self._test_field(
field_name, models.CharField, must_have, must_hot_have)
self.assertTrue(
field.max_length in range(min_length, max_length),
msg='%s.%s field max_length not in range of %d and %d' % (
msg='Expeting %s.%s field max_length to be in the 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):
def _test_float_field(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
@@ -61,21 +102,23 @@ class TestModel:
[MinValueValidator] if only min_value is passed,
[MaxValueValidator] if only max_value is passed
'''
field = self._test_field(field_name, models.FloatField)
field = self._test_field(
field_name, models.FloatField, must_have, must_hot_have)
if min_value:
self.assertTrue(
len(field.validators) >= 1,
msg='%s.%s first validator should check minimum' % (
msg='Expecting the first valiator of %s.%s to check the minimum' % (
self.model_name,
field_name
)
)
)
self.assertEqual(
field.validators[0].limit_value,
min_value,
msg='%s.%s min value missmatch' % (
msg='Expecting the min value of %s.%s min to be at least %d' % (
self.model_name,
field_name
field_name,
min_value
)
)
if max_value:
@@ -84,38 +127,41 @@ class TestModel:
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' % (
msg='Expecting the second valiator of %s.%s to check the maximum' % (
self.model_name,
field_name
)
)
self.assertEqual(
field.validators[1].limit_value,
max_value,
msg='Expecting the max value of %s.%s min to be at most %d' % (
self.model_name,
field_name,
max_value
)
)
class TestSubmittable(TestModel):
model_name='<Class>'
class SubmittableTestCase(ModelTestCase):
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
)
submitted_when = self._test_field(
'submitted_when',
models.DateTimeField,
must_have={'auto_now_add': True}
)
def test_submitted_by(self):
submitted_by = self._test_field('submitted_by',models.ForeignKey)
submitted_by = self._test_field('submitted_by', models.ForeignKey)
if self.related_name:
self.assertEqual(submitted_by.remote_field.related_name, 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)
self.assertEqual(
submitted_by.remote_field.on_delete, models.SET_NULL)

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1,55 @@
from django.test import TestCase
from django.db import models
from django.contrib.auth.models import User
from lostplaces_app.models import Explorer
from lostplaces_app.tests import mock_user
class ExplorerTestCase(TestCase):
def setUp(self):
mock_user()
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(id=1)
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)

View File

@@ -1,28 +1,39 @@
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 lostplaces_app.models import PlaceImage
from lostplaces_app.tests.models import TestSubmittable
from lostplaces_app.tests.models import SubmittableTestCase
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()
)
all_place_images = PlaceImage.objects.all()
if len(all_place_images) <= 0:
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')
)
return PlaceImage.objects.create(
description='Im a description',
filename=os.path.join(settings.MEDIA_ROOT, 'im_a_image_copy.jpeg'),
place=mock_place(),
submitted_when=datetime.datetime.now(),
submitted_by=mock_user().explorer
)
return all_place_images[0]
class TestPlaceImage(TestSubmittable, TestCase):
class TestPlaceImage(SubmittableTestCase, TestCase):
model_name = 'PlaceImage'
def setUp(self):
@@ -51,5 +62,18 @@ class TestPlaceImage(TestSubmittable, TestCase):
)
def test_str(self):
place_image = mock_place_image()
self.assertEqual(str(place_image), ' '.join([place_image.place.name, str(place_image.pk)]))
place_image = self.object
self.assertTrue(place_image.place.name.lower() in str(place_image).lower(),
msg='Expecting %s.__str__ to contain the name of the place' % (
self.model_name
)
)
def test_deletion(self):
# TODO
path = self.object.filename.path
self.object.delete()
self.assertFalse(
os.path.isfile(path),
msg='Expecting the file of an place_image to be deleteed when an place_image is deleted'
)

View File

@@ -4,9 +4,8 @@ from django.test import TestCase
from django.db import models
from lostplaces_app.models import Place
from lostplaces_app.tests.models import TestSubmittable
from lostplaces_app.tests.models import SubmittableTestCase
from lostplaces_app.tests import mock_user
from lostplaces_app.tests.models import TestModel
from taggit.managers import TaggableManager
@@ -14,7 +13,7 @@ def mock_place():
place = Place.objects.create(
name='Im a place',
submitted_when=datetime.datetime.now(),
submitted_by=mock_user(),
submitted_by=mock_user().explorer,
location='Testtown',
latitude=50.5,
longitude=7.0,
@@ -24,7 +23,7 @@ def mock_place():
return place
class PlaceTestCase(TestSubmittable, TestCase):
class PlaceTestCase(SubmittableTestCase, TestCase):
model_name = 'Place'
related_name = 'places'
nullable = True
@@ -127,10 +126,9 @@ class PlaceTestCase(TestSubmittable, TestCase):
)
def test_str(self):
place = mock_place()
self.assertEqual(str(place), place.name,
msg='%s __str__ should return the name' % (
place = self.object
self.assertTrue(place.name.lower() in str(place).lower(),
msg='Expecting %s.__str__ to contain the name' % (
self.model_name
)
)

View File

@@ -0,0 +1,52 @@
import datetime
from django.test import TestCase
from django.db import models
from django.utils import timezone
from lostplaces_app.models import Voucher
from lostplaces_app.tests.models import ModelTestCase
def mock_voucher():
return Voucher.objects.create(
code='ayDraJCCwfhcFiYmSR5GrcjcchDfcahv',
expires_when=timezone.now() + datetime.timedelta(days=1)
)
class VoucherTestCase(ModelTestCase, TestCase):
model_name = 'Voucher'
def setUp(self):
self.object = mock_voucher()
def test_voucher_code(self):
self._test_char_field(
'code',
10,
100,
must_have={'unique': True}
)
def test_voucher_created(self):
self._test_field(
'created_when',
models.DateTimeField,
must_have={'auto_now_add': True}
)
def test_voucher_expires(self):
self._test_field(
'expires_when',
models.DateTimeField,
must_not_have={'auto_now_add': True}
)
def test_str(self):
voucher = self.object
self.assertTrue(voucher.code.lower() in str(voucher).lower(),
msg='Expecting %s.__str__ to contain the voucher code' % (
self.model_name
)
)

View File

@@ -3,7 +3,7 @@ from django.urls import reverse_lazy
from lostplaces_app.models import Place
from lostplaces_app.models import Explorer
from django.contrib.auth.models import User
from lostplaces_app.tests.models.test_place_model import mock_place
from lostplaces_app.tests import mock_user
@@ -23,7 +23,7 @@ class TestIsAuthenticated(TestCase):
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='?'.join([str(reverse_lazy('login')), 'redirect_to=/place/1/']),
expected_url='?'.join([str(reverse_lazy('login')), 'next=/place/1/']),
status_code=302,
target_status_code=200,
msg_prefix='''Accesing an IsAuthenticated view while not logged should
@@ -47,7 +47,7 @@ class TestIsPlaceSubmitter(TestCase):
self.assertEqual(response.status_code, 200)
def test_is_no_submitter(self):
Explorer.objects.create_user(
User.objects.create_user(
username='manfred',
password='Develop123'
)

View File

@@ -23,7 +23,7 @@ class TestPlaceCreateView(TestCase):
response = self.client.get(url)
self.assertRedirects(
response=response,
expected_url='?'.join([str(reverse_lazy('login')), 'redirect_to=/place/1/']),
expected_url='?'.join([str(reverse_lazy('login')), 'next=/place/1/']),
status_code=302,
target_status_code=200,
msg_prefix='''Accesing PlaceDetailView while not logged should

View File

@@ -1,5 +1,5 @@
from django.urls import path
from .views import (
from lostplaces_app.views import (
HomeView,
PlaceDetailView,
PlaceListView,

View File

@@ -48,7 +48,7 @@ class IsPlaceSubmitter(UserPassesTestMixin, View):
# Check if currently logged in user was the submitter
place_obj = self.get_place()
if place_obj and hasattr(place_obj, 'submitted_by') and self.request.user == place_obj.submitted_by:
if place_obj and hasattr(place_obj, 'submitted_by') and self.request.user.explorer == place_obj.submitted_by:
return True
if self.place_submitter_error_message:
@@ -69,7 +69,7 @@ class PlaceAssetCreateView(IsAuthenticated, SuccessMessageMixin, CreateView):
self.place = Place.objects.get(pk=place_id)
response = super().post(request, *args, **kwargs)
self.object.place = self.place
self.object.submitted_by = request.user
self.object.submitted_by = request.user.explorer
self.object.save()
return response
@@ -95,7 +95,7 @@ class PlaceAssetDeleteView(IsAuthenticated, IsPlaceSubmitter, SingleObjectMixin,
if can_edit_place:
return True
if self.get_object().submitted_by == self.request.user:
if self.get_object().submitted_by == self.request.user.explorer:
return True
messages.error(self.request, self.permission_denied_message)

View File

@@ -72,7 +72,7 @@ class PlaceCreateView(IsAuthenticated, View):
place_form = PlaceForm(request.POST)
if place_form.is_valid():
submitter = request.user
submitter = request.user.explorer
place = place_form.save(commit=False)
# Save logged in user as "submitted_by"
place.submitted_by = submitter

View File

@@ -1,20 +1,25 @@
from setuptools import setup, find_packages
with open('Readme.md') as f:
readme = f.read()
setup(
name='django-lostplaces',
version='0.1.a2',
version='0.1.a5',
description='A django app to manage lost places',
author='Reverend :: Commander1024',
author_email='reverend@reverend2048.de :: commander@commander1024.de',
author='Reverend',
author_email='reverend@reverend2048.de',
url='https://git.mowoe.com/reverend/lostplaces-backend',
packages=find_packages(exclude=['lostplaces']),
long_description=readme,
long_description_content_type='text/markdown',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Web Environment',
'Intended Audience :: Explorer :: Photographers :: Proper People',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3.7'
'Programming Language :: Python :: 3.7',
'Framework :: Django'
],
install_requires=[
'django',