31 Commits

Author SHA1 Message Date
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
cba589a38e Small tweaks 2020-09-10 21:42:25 +02:00
a42b29721e changed versioning schema 2020-09-10 21:39:15 +02:00
a102152d34 Restructuring 2020-09-10 20:57:34 +02:00
dc71dcba0d Merge branch 'master' into release 2020-09-10 20:48:57 +02:00
266bf0b368 Formatting 2020-09-10 20:46:11 +02:00
88c94acd93 small tweaks 2020-09-10 20:42:49 +02:00
eede454e76 some polishing 2020-09-10 20:42:43 +02:00
b0ddf91677 Merge branch 'master' of mowoe.com:reverend/lostplaces-backend into master 2020-09-10 18:38:28 +02:00
c30af40079 Cleaned stuff up 2020-09-10 18:38:26 +02:00
dd266f8cdf dynamic url 2020-09-10 18:37:47 +02:00
9c70da0b77 Putting Thumbnail config into the apps scope 2020-09-10 18:37:35 +02:00
89bc00e5da Signup should be conifgured globally 2020-09-10 18:37:10 +02:00
72dc41ab79 Cleaned up configuration 2020-09-10 18:36:42 +02:00
711d46da68 Merge branch 'master' into release 2020-09-10 18:08:00 +02:00
a8b033dfdb Manifest for building packages 2020-09-10 17:48:38 +02:00
ca0e1a48dc Deleted obsolete tagify stylesheet and reference. 2020-09-10 09:05:48 +02:00
e08f8f60f2 Changed thumbnail/hero images size to cropped 3:2 aspect ratio. 2020-09-10 07:42:20 +02:00
26e257fe96 Merge branch 'feature/unauth_content' 2020-09-10 00:35:08 +02:00
44b06d1c04 Minor formatting changes. 2020-09-10 00:32:56 +02:00
0c2b1a4699 voucher form length now matches voucher model length. 2020-09-10 00:25:44 +02:00
99afb30611 Configuration for building package 2020-09-09 22:53:40 +02:00
bb1dafc89c Dev dep 2020-09-09 22:53:22 +02:00
5cc391391e More comments 2020-09-09 22:52:28 +02:00
23 changed files with 321 additions and 748 deletions

5
.gitignore vendored
View File

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

5
MANIFEST.in Normal file
View File

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

View File

@@ -6,6 +6,11 @@ verify_ssl = true
[dev-packages] [dev-packages]
pylint = "*" pylint = "*"
coverage = "*" coverage = "*"
autopep8 = "*"
pipenv = "*"
wheel = "*"
twine = "*"
pandoc ="*"
[packages] [packages]
django = "*" django = "*"
@@ -13,7 +18,6 @@ easy-thumbnails = "*"
image = "*" image = "*"
django-widget-tweaks = "*" django-widget-tweaks = "*"
django-taggit = "*" django-taggit = "*"
# Commented out to not explicitly specify Python 3 subversion. # Commented out to not explicitly specify Python 3 subversion.
# [requires] # [requires]
# python_version = "3.8" # python_version = "3.8"

133
Readme.md
View File

@@ -4,22 +4,33 @@ 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. 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.
- 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 ## 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. 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. * [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+ * [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 * [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-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. * [django-taggit](https://github.com/jazzband/django-taggit) A simpler approach to tagging with Django.
## Development # Installing a development instance
### Setting up a (pipenv) virtual environment for development ## 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. 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.
``` ```shell
$ cd lostplaces-backend $ cd lostplaces-backend
$ pipenv install $ pipenv install
$ pipenv shell $ pipenv shell
@@ -29,8 +40,8 @@ $ pipenv shell
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6 (lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
``` ```
### Returning to the venv ## Returning to the venv
``` ```shell
$ cd lostplaces-backend $ cd lostplaces-backend
$ pipenv shell $ pipenv shell
(lostplaces-backend) $ pipenv update # If dependencies changed, or updates available (lostplaces-backend) $ pipenv update # If dependencies changed, or updates available
@@ -40,23 +51,46 @@ $ pipenv shell
``` ```
Visit: [admin](http://localhost:8000/admin) for administrative backend or Visit: [admin](http://localhost:8000/admin) for administrative backend or
[frontend](http://localhost:8000/) [frontend](http://localhost:8000/).
## Installing lostplaces Happy developing ;-)
### Install dependencies # Installing a productive instance
Python3, Django3, easy-thumbnails, image, django-widget-tweaks, django-taggit
```
pip install --user django easy-thumbnails image django-widget-tweaks django-taggit
```
Or, if you use pipenv
```
pipenv install / update
```
### Add 'lostplaces_app' to your INSTALLED_APPS setting like this 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 yourself.
``` ## Cloning the repository
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 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/).
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 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`
*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.
```python
INSTALLED_APPS = [ INSTALLED_APPS = [
... ...
'lostplaces_app', 'lostplaces_app',
@@ -66,41 +100,44 @@ INSTALLED_APPS = [
] ]
``` ```
### Add this configuration to your settings.py 2. Set the URL's and Root-directories for file handling, for example:
```python
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
MEDIA_URL = '/uploads/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
``` ```
from django.urls import reverse_lazy
... 3. Set the user model (this will be changed in the next release):
```python
AUTH_USER_MODEL = 'lostplaces_app.Explorer' AUTH_USER_MODEL = 'lostplaces_app.Explorer'
```
4. Set the URL's for login, for example:
```python
LOGIN_URL = reverse_lazy('login') LOGIN_URL = reverse_lazy('login')
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
THUMBNAIL_ALIASES = { LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
'': {
'thumbnail': {'size': (300, 300), 'crop': False},
'hero': {'size': (700, 700), 'crop': False},
'large': {'size': (1920, 1920), 'crop': False},
},
}
``` ```
### Configuring the URL's
### Include the lostplaces URLconf in your project urls.py like this In the `urls.py` configure the `urlpatter` like this:
```python
```
from django.urls import path, include
...
urlpatterns = [ urlpatterns = [
... path('admin/', admin.site.urls),
path('lostplaces/', include('lostplaces_app.urls')), path('signup/', SignUpView.as_view(), name='signup'), # If you want to use lostplaces' sign up view.
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 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.
``` ```
Run ``./manage.py migrate`` to create the lost places models. 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/).
Start the development server and visit http://localhost:8000/admin/ Run `lostplaces/managy.py collectstatic` you should be ready to go.
Visit http://localhost:8000/ to CRUD lost places.
Happy developing ;-)

View File

@@ -4,13 +4,16 @@
""" """
Django settings for lostplaces project. 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 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 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 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 # 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! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'n$(bx8(^)*wz1ygn@-ekt7rl^1km*!_c+fwwjiua8m@-x_rpl0' SECRET_KEY = 'n$(bx8(^)*wz1ygn@-ekt7rl^1km*!_c+fwwjiua8m@-x_rpl0'
@@ -29,22 +32,22 @@ SECRET_KEY = 'n$(bx8(^)*wz1ygn@-ekt7rl^1km*!_c+fwwjiua8m@-x_rpl0'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [ 'localhost' ] ALLOWED_HOSTS = ['localhost']
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'lostplaces_app', 'lostplaces_app',
'easy_thumbnails',
'widget_tweaks',
'taggit',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles'
'easy_thumbnails',
'widget_tweaks',
'taggit'
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -62,7 +65,7 @@ ROOT_URLCONF = 'lostplaces.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], 'DIRS': [],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@@ -79,7 +82,7 @@ WSGI_APPLICATION = 'lostplaces.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases # https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
@@ -90,7 +93,7 @@ DATABASES = {
# Password validation # 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 = [ AUTH_PASSWORD_VALIDATORS = [
{ {
@@ -109,7 +112,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/ # https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'
@@ -123,31 +126,23 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # 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_URL = '/static/'
STATICFILES_DIRS = [ STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
os.path.join(BASE_DIR, "static")
]
MEDIA_URL = '/uploads/' MEDIA_URL = '/uploads/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
# Use custom user model
AUTH_USER_MODEL = 'lostplaces_app.Explorer'
# Templates to use for authentication # Templates to use for authentication
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'
LOGIN_URL = reverse_lazy('login') LOGIN_URL = reverse_lazy('login')
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
THUMBNAIL_ALIASES = {
'': {
'thumbnail': {'size': (300, 300), 'crop': False},
'hero': {'size': (700, 700), 'crop': False},
'large': {'size': (1920, 1920), 'crop': False},
},
}
SVG_ICONS_SOURCE_FILE = os.path.join(BASE_DIR, 'lostplaces_app', 'static', 'icons', 'icons.icomoon.json') # Load and override settings from local_settings.py and don't fail on errors.
try:
from .local_settings import *
except ImportError:
pass

View File

@@ -23,8 +23,11 @@ from django.conf.urls.static import static
from django.urls import path, include from django.urls import path, include
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from lostplaces_app.views import SignUpView
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('signup/', SignUpView.as_view(), name='signup'),
path('explorers/', include('django.contrib.auth.urls')), path('explorers/', include('django.contrib.auth.urls')),
path('', include('lostplaces_app.urls')), path('', include('lostplaces_app.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,2 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.conf import settings
settings.THUMBNAIL_ALIASES = {
'': {
'thumbnail': {'size': (300, 200), 'crop': True},
'hero': {'size': (700, 466), 'crop': True},
'large': {'size': (1920, 1920), 'crop': False},
},
}

View File

@@ -12,17 +12,11 @@ from .forms import ExplorerCreationForm, ExplorerChangeForm
# Register your models here. # Register your models here.
class ExplorerAdmin(UserAdmin):
add_form = ExplorerCreationForm
form = ExplorerChangeForm
model = Explorer
list_display = ['email', 'username',]
class VoucherAdmin(admin.ModelAdmin): class VoucherAdmin(admin.ModelAdmin):
fields = ['code', 'expires', 'created'] fields = ['code', 'expires', 'created']
readonly_fields = ['created'] readonly_fields = ['created']
admin.site.register(Explorer, ExplorerAdmin) admin.site.register(Explorer)
admin.site.register(Voucher, VoucherAdmin) admin.site.register(Voucher, VoucherAdmin)
admin.site.register(Place) admin.site.register(Place)
admin.site.register(PlaceImage) admin.site.register(PlaceImage)

View File

@@ -5,13 +5,17 @@
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm 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 ExplorerCreationForm(UserCreationForm):
class Meta: class Meta:
model = Explorer model = User
fields = ('username', 'email') fields = ('username', 'email')
voucher = forms.CharField(max_length=10, help_text='The Voucher you got from an administrator') voucher = forms.CharField(
max_length=30,
help_text='The Voucher you got from an administrator'
)
def is_valid(self): def is_valid(self):
super().is_valid() super().is_valid()
@@ -27,7 +31,7 @@ class ExplorerCreationForm(UserCreationForm):
class ExplorerChangeForm(UserChangeForm): class ExplorerChangeForm(UserChangeForm):
class Meta: class Meta:
model = Explorer model = User
fields = ('username', 'email') fields = ('username', 'email')
class PlaceForm(forms.ModelForm): class PlaceForm(forms.ModelForm):
@@ -49,10 +53,9 @@ class PlaceImageCreateForm(forms.ModelForm):
self.fields['filename'].required = False self.fields['filename'].required = False
class TagSubmitForm(forms.Form): class TagSubmitForm(forms.Form):
tag_list = forms.CharField( tag_list = forms.CharField(
max_length=500, max_length=500,
required=False, required=False,
widget=forms.TextInput(attrs={'autocomplete':'off'}) widget=forms.TextInput(attrs={'autocomplete':'off'})
) )

View File

@@ -10,22 +10,40 @@ import os
import uuid import uuid
from django.db import models 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.dispatch import receiver
from django.contrib.auth.models import AbstractUser
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from easy_thumbnails.fields import ThumbnailerImageField from easy_thumbnails.fields import ThumbnailerImageField
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
# Create your models here. # Create your models here.
class Explorer(AbstractUser):
class Explorer(models.Model):
""" """
Custom user model Profile that is linked to the a User.
Addtional fields wbd Every user has a profile.
""" """
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='explorer'
)
def __str__(self): 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): class Voucher(models.Model):
""" """
@@ -42,6 +60,7 @@ class Voucher(models.Model):
def __str__(self): def __str__(self):
return "Voucher " + str(self.pk) return "Voucher " + str(self.pk)
class Place (models.Model): class Place (models.Model):
""" """
Place defines a lost place (location, name, description etc.). Place defines a lost place (location, name, description etc.).
@@ -53,7 +72,7 @@ class Place (models.Model):
Explorer, Explorer,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
related_name='places' related_name='places'
) )
location = models.CharField(max_length=50) location = models.CharField(max_length=50)
@@ -79,7 +98,7 @@ class Place (models.Model):
# Init fill values to prevent None # Init fill values to prevent None
longitude = 0 longitude = 0
latitude = 0 latitude = 0
if amount > 0: if amount > 0:
for place in place_list: for place in place_list:
longitude += place.longitude longitude += place.longitude
@@ -91,6 +110,7 @@ class Place (models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def generate_image_upload_path(instance, filename): def generate_image_upload_path(instance, filename):
""" """
Callback for generating path for uploaded images. 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] return 'places/' + str(uuid.uuid4())+'.'+filename.split('.')[-1]
class PlaceImage (models.Model): class PlaceImage (models.Model):
""" """
PlaceImage defines an image file object that points to a file in uploads/. 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 SIZES.
PlaceImage references a Place to which it belongs. PlaceImage references a Place to which it belongs.
""" """
description = models.TextField(blank=True) description = models.TextField(blank=True)
filename = ThumbnailerImageField(upload_to=generate_image_upload_path) filename = ThumbnailerImageField(upload_to=generate_image_upload_path)
place = models.ForeignKey( place = models.ForeignKey(
@@ -117,7 +138,7 @@ class PlaceImage (models.Model):
Explorer, Explorer,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
related_name='images' related_name='images'
) )
@@ -130,6 +151,8 @@ class PlaceImage (models.Model):
return ' '.join([self.place.name, str(self.pk)]) return ' '.join([self.place.name, str(self.pk)])
# These two auto-delete files from filesystem when they are unneeded: # These two auto-delete files from filesystem when they are unneeded:
@receiver(models.signals.post_delete, sender=PlaceImage) @receiver(models.signals.post_delete, sender=PlaceImage)
def auto_delete_file_on_delete(sender, instance, **kwargs): 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): if os.path.isfile(instance.filename.path):
os.remove(instance.filename.path) os.remove(instance.filename.path)
@receiver(models.signals.pre_save, sender=PlaceImage) @receiver(models.signals.pre_save, sender=PlaceImage)
def auto_delete_file_on_change(sender, instance, **kwargs): 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): if os.path.isfile(old_file.path):
os.remove(old_file.path) os.remove(old_file.path)
class ExternalLink(models.Model): class ExternalLink(models.Model):
url = models.URLField(max_length=200) url = models.URLField(max_length=200)
label = models.CharField(max_length=100) label = models.CharField(max_length=100)
@@ -167,11 +192,12 @@ class ExternalLink(models.Model):
Explorer, Explorer,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
related_name='external_links' related_name='external_links'
) )
submitted_when = models.DateTimeField(auto_now_add=True, null=True) submitted_when = models.DateTimeField(auto_now_add=True, null=True)
class PhotoAlbum(ExternalLink): class PhotoAlbum(ExternalLink):
place = models.ForeignKey( place = models.ForeignKey(
Place, Place,

View File

@@ -1,548 +0,0 @@
:root {
--tagify-dd-color-primary: rgb(53, 149, 246);
--tagify-dd-bg-color: white
}
.tagify {
--tags-border-color: #DDD;
--tags-hover-border-color: #CCC;
--tags-focus-border-color: #3595f6;
--tag-bg: #E5E5E5;
--tag-hover: #D3E2E2;
--tag-text-color: black;
--tag-text-color--edit: black;
--tag-pad: 0.3em 0.5em;
--tag-inset-shadow-size: 1.1em;
--tag-invalid-color: #D39494;
--tag-invalid-bg: rgba(211, 148, 148, 0.5);
--tag-remove-bg: rgba(211, 148, 148, 0.3);
--tag-remove-btn-color: black;
--tag-remove-btn-bg: none;
--tag-remove-btn-bg--hover: #c77777;
--input-color: inherit;
--tag--min-width: 1ch;
--tag--max-width: auto;
--tag-hide-transition: .3s;
--placeholder-color: rgba(0, 0, 0, 0.4);
--placeholder-color-focus: rgba(0, 0, 0, 0.25);
--loader-size: .8em;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
border: 1px solid #ddd;
border: 1px solid var(--tags-border-color);
padding: 0;
line-height: 1.1;
cursor: text;
outline: 0;
position: relative;
transition: .1s
}
@keyframes tags--bump {
30% {
transform: scale(1.2)
}
}
@keyframes rotateLoader {
to {
transform: rotate(1turn)
}
}
.tagify:hover {
border-color: #ccc;
border-color: var(--tags-hover-border-color)
}
.tagify.tagify--focus {
transition: 0s;
border-color: #3595f6;
border-color: var(--tags-focus-border-color)
}
.tagify[readonly]:not(.tagify--mix) {
cursor: default
}
.tagify[readonly]:not(.tagify--mix)>.tagify__input {
visibility: hidden;
width: 0;
margin: 5px 0
}
.tagify[readonly]:not(.tagify--mix) .tagify__tag__removeBtn {
display: none
}
.tagify[readonly]:not(.tagify--mix) .tagify__tag>div {
padding: .3em .5em;
padding: var(--tag-pad)
}
.tagify[readonly]:not(.tagify--mix) .tagify__tag>div::before {
background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/5px 5px;
box-shadow: none;
filter: brightness(.95)
}
.tagify--loading .tagify__input::before {
content: none
}
.tagify--loading .tagify__input::after {
content: '';
vertical-align: middle;
opacity: 1;
width: .7em;
height: .7em;
width: var(--loader-size);
height: var(--loader-size);
border: 3px solid;
border-color: #eee #bbb #888 transparent;
border-radius: 50%;
animation: rotateLoader .4s infinite linear;
margin: -2px 0 -2px .5em
}
.tagify--loading .tagify__input:empty::after {
margin-left: 0
}
.tagify+input,
.tagify+textarea {
display: none !important
}
.tagify__tag {
display: inline-flex;
align-items: center;
margin: 5px 0 5px 5px;
position: relative;
z-index: 1;
outline: 0;
cursor: default;
transition: .13s ease-out
}
.tagify__tag>div {
vertical-align: top;
box-sizing: border-box;
max-width: 100%;
padding: .3em .5em;
padding: var(--tag-pad);
color: #000;
color: var(--tag-text-color);
line-height: inherit;
border-radius: 3px;
-webkit-user-select: none;
user-select: none;
white-space: nowrap;
transition: .13s ease-out
}
.tagify__tag>div>* {
white-space: pre-wrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
vertical-align: top;
min-width: var(--tag--min-width);
max-width: var(--tag--max-width);
transition: .8s ease, .1s color
}
.tagify__tag>div>[contenteditable] {
outline: 0;
-webkit-user-select: text;
user-select: text;
cursor: text;
margin: -2px;
padding: 2px;
max-width: 350px
}
.tagify__tag>div::before {
content: '';
position: absolute;
border-radius: inherit;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
transition: 120ms ease;
animation: tags--bump .3s ease-out 1;
box-shadow: 0 0 0 1.1em #e5e5e5 inset;
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-bg) inset
}
.tagify__tag:hover:not([readonly]) div::before {
top: -2px;
right: -2px;
bottom: -2px;
left: -2px;
box-shadow: 0 0 0 1.1em #d3e2e2 inset;
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-hover) inset
}
.tagify__tag--loading {
pointer-events: none
}
.tagify__tag--loading .tagify__tag__removeBtn {
display: none
}
.tagify__tag--loading::after {
--loader-size: .4em;
content: '';
vertical-align: middle;
opacity: 1;
width: .7em;
height: .7em;
width: var(--loader-size);
height: var(--loader-size);
border: 3px solid;
border-color: #eee #bbb #888 transparent;
border-radius: 50%;
animation: rotateLoader .4s infinite linear;
margin: 0 .5em 0 -.1em
}
.tagify__tag--flash div::before {
animation: none
}
.tagify__tag--hide {
width: 0 !important;
padding-left: 0;
padding-right: 0;
margin-left: 0;
margin-right: 0;
opacity: 0;
transform: scale(0);
transition: .3s;
transition: var(--tag-hide-transition);
pointer-events: none
}
.tagify__tag.tagify--noAnim>div::before {
animation: none
}
.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div>span {
opacity: .5
}
.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div::before {
box-shadow: 0 0 0 1.1em rgba(211, 148, 148, .5) inset !important;
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-invalid-bg) inset !important;
transition: .2s
}
.tagify__tag[readonly] .tagify__tag__removeBtn {
display: none
}
.tagify__tag[readonly]>div::before {
background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/5px 5px;
box-shadow: none;
filter: brightness(.95)
}
.tagify__tag--editable>div {
color: #000;
color: var(--tag-text-color--edit)
}
.tagify__tag--editable>div::before {
box-shadow: 0 0 0 2px #d3e2e2 inset !important;
box-shadow: 0 0 0 2px var(--tag-hover) inset !important
}
.tagify__tag--editable.tagify--invalid>div::before {
box-shadow: 0 0 0 2px #d39494 inset !important;
box-shadow: 0 0 0 2px var(--tag-invalid-color) inset !important
}
.tagify__tag__removeBtn {
order: 5;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50px;
cursor: pointer;
font: 14px/1 Arial;
background: 0 0;
background: var(--tag-remove-btn-bg);
color: #000;
color: var(--tag-remove-btn-color);
width: 14px;
height: 14px;
margin-right: 4.66667px;
margin-left: -4.66667px;
transition: .2s ease-out
}
.tagify__tag__removeBtn::after {
content: "\00D7"
}
.tagify__tag__removeBtn:hover {
color: #fff;
background: #c77777;
background: var(--tag-remove-btn-bg--hover)
}
.tagify__tag__removeBtn:hover+div>span {
opacity: .5
}
.tagify__tag__removeBtn:hover+div::before {
box-shadow: 0 0 0 1.1em rgba(211, 148, 148, .3) inset !important;
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-remove-bg) inset !important;
transition: .2s
}
.tagify:not(.tagify--mix) .tagify__input br {
display: none
}
.tagify:not(.tagify--mix) .tagify__input * {
display: inline;
white-space: nowrap
}
.tagify__input {
flex-grow: 1;
display: inline-block;
min-width: 110px;
margin: 5px;
padding: .3em .5em;
padding: var(--tag-pad, .3em .5em);
line-height: inherit;
position: relative;
white-space: pre-wrap;
color: inherit;
color: var(--input-color)
}
.tagify__input:empty::before {
transition: .2s ease-out;
opacity: 1;
transform: none;
display: inline-block;
width: auto
}
.tagify--mix .tagify__input:empty::before {
display: inline-block
}
.tagify__input:focus {
outline: 0
}
.tagify__input:focus::before {
transition: .2s ease-out;
opacity: 0;
transform: translatex(6px)
}
@media all and (-ms-high-contrast:none),
(-ms-high-contrast:active) {
.tagify__input:focus::before {
display: none
}
}
@supports (-ms-ime-align:auto) {
.tagify__input:focus::before {
display: none
}
}
.tagify__input:focus:empty::before {
transition: .2s ease-out;
opacity: 1;
transform: none;
color: rgba(0, 0, 0, .25);
color: var(--placeholder-color-focus)
}
@-moz-document url-prefix() {
.tagify__input:focus:empty::after {
display: none
}
}
.tagify__input::before {
content: attr(data-placeholder);
height: 1em;
line-height: 1em;
margin: auto 0;
z-index: 1;
color: rgba(0, 0, 0, .4);
color: var(--placeholder-color);
white-space: nowrap;
pointer-events: none;
opacity: 0;
position: absolute
}
.tagify--mix .tagify__input::before {
display: none;
position: static;
line-height: inherit
}
.tagify__input::after {
content: attr(data-suggest);
display: inline-block;
white-space: pre;
color: #000;
opacity: .3;
pointer-events: none;
max-width: 100px
}
.tagify__input .tagify__tag {
margin: 0
}
.tagify__input .tagify__tag>div {
padding-top: 0;
padding-bottom: 0
}
.tagify--mix {
display: block
}
.tagify--mix .tagify__input {
padding: 5px;
margin: 0;
width: 100%;
height: 100%;
line-height: 1.5
}
.tagify--mix .tagify__input::before {
height: auto
}
.tagify--mix .tagify__input::after {
content: none
}
.tagify--select::after {
content: '>';
opacity: .5;
position: absolute;
top: 50%;
right: 0;
bottom: 0;
font: 16px monospace;
line-height: 8px;
height: 8px;
pointer-events: none;
transform: translate(-150%, -50%) scaleX(1.2) rotate(90deg);
transition: .2s ease-in-out
}
.tagify--select[aria-expanded=true]::after {
transform: translate(-150%, -50%) rotate(270deg) scaleY(1.2)
}
.tagify--select .tagify__tag {
position: absolute;
top: 0;
right: 1.8em;
bottom: 0
}
.tagify--select .tagify__tag div {
display: none
}
.tagify--select .tagify__input {
width: 100%
}
.tagify--invalid {
--tags-border-color: #D39494
}
.tagify__dropdown {
position: absolute;
z-index: 9999;
transform: translateY(1px);
overflow: hidden
}
.tagify__dropdown[placement=top] {
margin-top: 0;
transform: translateY(-100%)
}
.tagify__dropdown[placement=top] .tagify__dropdown__wrapper {
border-top-width: 1px;
border-bottom-width: 0
}
.tagify__dropdown[position=text] {
box-shadow: 0 0 0 3px rgba(var(--tagify-dd-color-primary), .1);
font-size: .9em
}
.tagify__dropdown[position=text] .tagify__dropdown__wrapper {
border-width: 1px
}
.tagify__dropdown__wrapper {
max-height: 300px;
overflow: hidden;
background: #fff;
background: var(--tagify-dd-bg-color);
border: 1px solid #3595f6;
border-color: var(--tagify-dd-color-primary);
border-top-width: 0;
box-shadow: 0 2px 4px -2px rgba(0, 0, 0, .2);
transition: .25s cubic-bezier(0, 1, .5, 1)
}
.tagify__dropdown__wrapper:hover {
overflow: auto
}
.tagify__dropdown--initial .tagify__dropdown__wrapper {
max-height: 20px;
transform: translateY(-1em)
}
.tagify__dropdown--initial[placement=top] .tagify__dropdown__wrapper {
transform: translateY(2em)
}
.tagify__dropdown__item {
box-sizing: inherit;
padding: .3em .5em;
margin: 1px;
cursor: pointer;
border-radius: 2px;
position: relative;
outline: 0
}
.tagify__dropdown__item--active {
background: #3595f6;
background: var(--tagify-dd-color-primary);
color: #fff
}
.tagify__dropdown__item:active {
filter: brightness(105%)
}

View File

@@ -16,77 +16,78 @@
</title> </title>
</head> </head>
<body> <body>
<div class="LP-Wrapper__Site"> <div class="LP-Wrapper__Site">
<header class="LP-Header"> <header class="LP-Header">
<div class="LP-Header__Logo"> <div class="LP-Header__Logo">
<a class="LP-Link" href="/"> <a class="LP-Link" href="/">
<img src="{% static 'logo.png' %}" class="LP-Image" /> <img src="{% static 'logo.png' %}" class="LP-Image" />
</a> </a>
</div> </div>
<div class="LP-Header__UserInformation"> <div class="LP-Header__UserInformation">
<span class="LP-Paragraph"> <span class="LP-Paragraph">
{% if user.is_authenticated %} {% if user.is_authenticated %}
Hi {{ user.username }}! Hi {{ user.username }}!
<a class="LP-Link" href="{% url 'logout' %}"><span class="LP-Link__Text">logout</span></a> <a class="LP-Link" href="{% url 'logout' %}"><span class="LP-Link__Text">logout</span></a>
{% if user.is_superuser %} {% if user.is_superuser %}
<a class="LP-Link" href="{% url 'admin:index' %}" target="_blank"><span class="LP-Link__Text">admin</span></a> <a class="LP-Link" href="{% url 'admin:index' %}" target="_blank"><span class="LP-Link__Text">admin</span></a>
{% endif %} {% endif %}
{% else %} {% else %}
You are not logged in. You are not logged in.
<a class="LP-Link" href="{% url 'login' %}"><span class="LP-Link__Text">login</span></a> | <a class="LP-Link" href="{% url 'login' %}"><span class="LP-Link__Text">login</span></a> |
<a class="LP-Link" href="{% url 'signup' %}"><span class="LP-Link__Text">signup</span></a> <a class="LP-Link" href="{% url 'signup' %}"><span class="LP-Link__Text">signup</span></a>
{% endif %} {% endif %}
</span> </span>
</div> </div>
</header> </header>
<input id="toggle_sidebar" class="LP-Menu__Trigger" type="checkbox" /> <input id="toggle_sidebar" class="LP-Menu__Trigger" type="checkbox" />
<label id="toggle_sidebar_label" for="toggle_sidebar" class="LP-Menu__TriggerLabel"></label> <label id="toggle_sidebar_label" for="toggle_sidebar" class="LP-Menu__TriggerLabel"></label>
<aside class="LP-Main__Sidebar"> <aside class="LP-Main__Sidebar">
<nav class="LP-Menu LP-Menu--sidebar"> <nav class="LP-Menu LP-Menu--sidebar">
<ul class="LP-Menu__List"> <ul class="LP-Menu__List">
<li class="LP-Menu__Item"><a href="/" class="LP-Link"><span class="LP-Link__Text">Home</span></a></li> <li class="LP-Menu__Item"><a href="{% url 'lostplaces_home' %}" class="LP-Link"><span class="LP-Link__Text">Home</span></a></li>
<li class="LP-Menu__Item"><a href="{% url 'flatpage' slug='codex' %}" class="LP-Link"><span class="LP-Link__Text">UrBex codex</span></a></li> <li class="LP-Menu__Item"><a href="{% url 'flatpage' slug='codex' %}" class="LP-Link"><span class="LP-Link__Text">UrBex codex</span></a></li>
{% block additional_menu_items %} {% block additional_menu_items %}
{% endblock additional_menu_items %} {% endblock additional_menu_items %}
<li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_create'%}" class="LP-Link"><span class="LP-Link__Text">Create place</span></a></li> <li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_create'%}" class="LP-Link"><span class="LP-Link__Text">Create place</span></a></li>
<li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_list'%}" class="LP-Link"><span class="LP-Link__Text">See all places</span></a></li> <li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_list'%}" class="LP-Link"><span class="LP-Link__Text">See all places</span></a></li>
</ul> </ul>
</nav> </nav>
</aside> </aside>
<main class="LP-Main__Content"> <main class="LP-Main__Content">
{% if messages %} {% if messages %}
<div class="LP-MessageList"> <div class="LP-MessageList">
<ul class="LP-MessageList__List"> <ul class="LP-MessageList__List">
{% for message in messages %} {% for message in messages %}
<li class="LP-MessageList__Item"> <li class="LP-MessageList__Item">
<div class="LP-Message {% if message.tags %}LP-Message--{{ message.tags }}{% endif %}"> <div class="LP-Message {% if message.tags %}LP-Message--{{ message.tags }}{% endif %}">
<div> <div>
<div class="LP-Message__Icon"> <div class="LP-Message__Icon">
</div>
</div>
<div class="LP-Message__Text">
{{ message }}
</div> </div>
</div> </div>
</li> <div class="LP-Message__Text">
{% endfor %} {{ message }}
</ul> </div>
</div> </div>
{% endif %} </li>
{% endfor %}
</ul>
</div>
{% endif %}
{% block maincontent %} {% block maincontent %}
{% endblock maincontent %} {% endblock maincontent %}
</main> </main>
</div> </div>
{% block footer %} {% block footer %}
{% include 'partials/nav/footer.html' %} {% include 'partials/nav/footer.html' %}
{% endblock footer %} {% endblock footer %}
</body> </body>
</html>
</html>

View File

@@ -6,7 +6,7 @@
<a href="#" class="LP-Link"> <a href="#" class="LP-Link">
<span class="LP-Link__Text">{{tag}}</span> <span class="LP-Link__Text">{{tag}}</span>
</a> </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"> <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"> <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"> <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> <legend class="LP-Form__Legend">Tags hinzufügen</legend>
{% csrf_token %} {% csrf_token %}
<div class="LP-Form__Composition LP-Form__Composition--breakable"> <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> <button id="id_tag_submit_button" class="LP-Button"> Tags hinzufügen</button>
</div> </div>
<div class="LP-Form__Field"> <div class="LP-Form__Field">
@@ -46,11 +46,15 @@
submit_form.onsubmit = () => false submit_form.onsubmit = () => false
const tagify = new Tagify(input, { const tagify = new Tagify(input, {
'whitelist': [ 'whitelist': [{
{% for tag in all_tags %} %
'{{tag}}', for tag in all_tags %
{% endfor %} }
] '{{tag}}',
{
% endfor %
}
]
}) })
const on_form_submit = function() { const on_form_submit = function() {

View File

@@ -6,12 +6,9 @@
{% block additional_head %} {% block additional_head %}
<link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css">
<script src="{% static 'maps/ol.js' %}"></script> <script src="{% static 'maps/ol.js' %}"></script>
<script src="{% static 'tagify.min.js' %}"></script> <script src="{% static 'tagify.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'minimal.css' %}" type="text/css">
{% endblock additional_head %} {% endblock additional_head %}
{% block title %}{{place.name}}{% endblock %} {% block title %}{{place.name}}{% endblock %}
@@ -65,7 +62,7 @@
<a target="_blank" href="{{photo_album.url}}" class="LP-Link"> <a target="_blank" href="{{photo_album.url}}" class="LP-Link">
<span class="LP-Text">{{photo_album.label}}</span> <span class="LP-Text">{{photo_album.label}}</span>
</a> </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"> <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"> <div class="RV-Iconized__Container RV-Iconized__Container--small">
{% icon 'trash' className="RV-Iconized__Icon" %} {% icon 'trash' className="RV-Iconized__Icon" %}

View File

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

View File

@@ -19,7 +19,7 @@ def mock_place_image():
filename=mock.MagicMock(spec=File, name='FileMock'), filename=mock.MagicMock(spec=File, name='FileMock'),
place=mock_place(), place=mock_place(),
submitted_when=datetime.datetime.now(), submitted_when=datetime.datetime.now(),
submitted_by=mock_user() submitted_by=mock_user().explorer
) )
class TestPlaceImage(TestSubmittable, TestCase): class TestPlaceImage(TestSubmittable, TestCase):

View File

@@ -14,7 +14,7 @@ def mock_place():
place = Place.objects.create( place = Place.objects.create(
name='Im a place', name='Im a place',
submitted_when=datetime.datetime.now(), submitted_when=datetime.datetime.now(),
submitted_by=mock_user(), submitted_by=mock_user().explorer,
location='Testtown', location='Testtown',
latitude=50.5, latitude=50.5,
longitude=7.0, longitude=7.0,

View File

@@ -3,7 +3,7 @@ from django.urls import reverse_lazy
from lostplaces_app.models import Place 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.models.test_place_model import mock_place
from lostplaces_app.tests import mock_user from lostplaces_app.tests import mock_user
@@ -23,7 +23,7 @@ class TestIsAuthenticated(TestCase):
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
self.assertRedirects( self.assertRedirects(
response=response, 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, status_code=302,
target_status_code=200, target_status_code=200,
msg_prefix='''Accesing an IsAuthenticated view while not logged should msg_prefix='''Accesing an IsAuthenticated view while not logged should
@@ -47,7 +47,7 @@ class TestIsPlaceSubmitter(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_is_no_submitter(self): def test_is_no_submitter(self):
Explorer.objects.create_user( User.objects.create_user(
username='manfred', username='manfred',
password='Develop123' password='Develop123'
) )

View File

@@ -23,7 +23,7 @@ class TestPlaceCreateView(TestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertRedirects( self.assertRedirects(
response=response, 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, status_code=302,
target_status_code=200, target_status_code=200,
msg_prefix='''Accesing PlaceDetailView while not logged should msg_prefix='''Accesing PlaceDetailView while not logged should

View File

@@ -3,7 +3,6 @@ from .views import (
HomeView, HomeView,
PlaceDetailView, PlaceDetailView,
PlaceListView, PlaceListView,
SignUpView,
PlaceCreateView, PlaceCreateView,
PlaceUpdateView, PlaceUpdateView,
PlaceDeleteView, PlaceDeleteView,
@@ -15,8 +14,7 @@ from .views import (
) )
urlpatterns = [ urlpatterns = [
path('', HomeView.as_view(), name='home'), path('', HomeView.as_view(), name='lostplaces_home'),
path('signup/', SignUpView.as_view(), name='signup'),
path('place/<int:pk>/', PlaceDetailView.as_view(), name='place_detail'), path('place/<int:pk>/', PlaceDetailView.as_view(), name='place_detail'),
path('place/create/', PlaceCreateView.as_view(), name='place_create'), path('place/create/', PlaceCreateView.as_view(), name='place_create'),
path('photo_album/create/<int:place_id>', PhotoAlbumCreateView.as_view(), name='photo_album_create'), path('photo_album/create/<int:place_id>', PhotoAlbumCreateView.as_view(), name='photo_album_create'),
@@ -26,7 +24,7 @@ urlpatterns = [
path('place/', PlaceListView.as_view(), name='place_list'), path('place/', PlaceListView.as_view(), name='place_list'),
path('flat/<slug:slug>/', FlatView, name='flatpage'), path('flat/<slug:slug>/', FlatView, name='flatpage'),
# POST-only URL for tag submission # POST-only URLs for tag submission
path('place/tag/<int:place_id>', PlaceTagSubmitView.as_view(), name='place_tag_submit'), path('place/tag/<int:place_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') path('place/tag/delete/<int:tagged_id>/<int:tag_id>', PlaceTagDeleteView.as_view(), name='place_tag_delete')
] ]

View File

@@ -12,7 +12,12 @@ from django.urls import reverse_lazy
from lostplaces_app.models import Place from lostplaces_app.models import Place
class IsAuthenticated(LoginRequiredMixin, View): class IsAuthenticated(LoginRequiredMixin, View):
redirect_field_name = 'redirect_to' '''
A view mixin that checks wether a user is loged in or not.
If the user is not logged in, he gets redirected to
the login page.
'''
login_url = reverse_lazy('login')
permission_denied_message = 'Please login to proceed' permission_denied_message = 'Please login to proceed'
def handle_no_permission(self): def handle_no_permission(self):
@@ -20,6 +25,12 @@ class IsAuthenticated(LoginRequiredMixin, View):
return super().handle_no_permission() return super().handle_no_permission()
class IsPlaceSubmitter(UserPassesTestMixin, View): class IsPlaceSubmitter(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
has to provide a get_place method, wich returns the
place to check.
'''
place_submitter_error_message = None place_submitter_error_message = None
def get_place(self): def get_place(self):
@@ -37,7 +48,7 @@ class IsPlaceSubmitter(UserPassesTestMixin, View):
# Check if currently logged in user was the submitter # Check if currently logged in user was the submitter
place_obj = self.get_place() 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 return True
if self.place_submitter_error_message: if self.place_submitter_error_message:
@@ -58,7 +69,7 @@ class PlaceAssetCreateView(IsAuthenticated, SuccessMessageMixin, CreateView):
self.place = Place.objects.get(pk=place_id) self.place = Place.objects.get(pk=place_id)
response = super().post(request, *args, **kwargs) response = super().post(request, *args, **kwargs)
self.object.place = self.place self.object.place = self.place
self.object.submitted_by = request.user self.object.submitted_by = request.user.explorer
self.object.save() self.object.save()
return response return response
@@ -72,7 +83,6 @@ class PlaceAssetCreateView(IsAuthenticated, SuccessMessageMixin, CreateView):
class PlaceAssetDeleteView(IsAuthenticated, IsPlaceSubmitter, SingleObjectMixin, View): class PlaceAssetDeleteView(IsAuthenticated, IsPlaceSubmitter, SingleObjectMixin, View):
model = None model = None
pk_url_kwarg = 'pk'
success_message = '' success_message = ''
permission_denied_message = '' permission_denied_message = ''
@@ -85,7 +95,7 @@ class PlaceAssetDeleteView(IsAuthenticated, IsPlaceSubmitter, SingleObjectMixin,
if can_edit_place: if can_edit_place:
return True return True
if self.get_object().submitted_by == self.request.user: if self.get_object().submitted_by == self.request.user.explorer:
return True return True
messages.error(self.request, self.permission_denied_message) messages.error(self.request, self.permission_denied_message)
@@ -95,4 +105,4 @@ class PlaceAssetDeleteView(IsAuthenticated, IsPlaceSubmitter, SingleObjectMixin,
place_id = self.get_object().place.id place_id = self.get_object().place.id
self.get_object().delete() self.get_object().delete()
messages.success(self.request, self.success_message) messages.success(self.request, self.success_message)
return redirect(reverse_lazy('place_detail', kwargs={'pk': place_id})) return redirect(reverse_lazy('place_detail', kwargs={'pk': place_id}))

View File

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

32
setup.py Normal file
View File

@@ -0,0 +1,32 @@
from setuptools import setup, find_packages
with open('Readme.md') as f:
readme = f.read()
setup(
name='django-lostplaces',
version='0.1.a5',
description='A django app to manage lost places',
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',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3.7',
'Framework :: Django'
],
install_requires=[
'django',
'easy_thumbnails',
'image',
'django-widget-tweaks',
'django-taggit'
],
include_package_data=True
)