Compare commits
132 Commits
6e6f4ced7b
...
a2004bd789
Author | SHA1 | Date | |
---|---|---|---|
a2004bd789 | |||
b3b8ee3104 | |||
9613f553e9 | |||
1a8a7cc4f0 | |||
702ebd9137 | |||
aa3922da50 | |||
d6c609d107 | |||
61566714a3 | |||
f4ae5c9f77 | |||
cfe3d222e6 | |||
e0d6d81907 | |||
5e8e865a99 | |||
3061540396 | |||
bb119e8636 | |||
14a98deff3 | |||
4919c90ae8 | |||
5c47587189 | |||
3223637471 | |||
389e3eac43 | |||
302e8bb994 | |||
992f8a7b37 | |||
c8c3852323 | |||
8d9ab8b088 | |||
45185b9236 | |||
5f33ec5824 | |||
e217d3254e | |||
263416a401 | |||
b4bd86ed44 | |||
277cac323e | |||
5f304b91f3 | |||
85f2a81ebb | |||
8de23a3f3b | |||
3b526c9c6f | |||
516e7a04ad | |||
84f13ced80 | |||
dcd92f7d2a | |||
2a944eeefe | |||
dc93539e23 | |||
dd58c76aba | |||
106ef6dac8 | |||
2bc99c78c2 | |||
0185c13a95 | |||
9632040ade | |||
85a34b20f9 | |||
e25f3da5a5 | |||
4c9234d07a | |||
f5853f7777 | |||
eb7fe88d59 | |||
2a3ee3de8d | |||
00ab65d648 | |||
79854f3fa1 | |||
618a152f23 | |||
ce91a19068 | |||
17b531e181 | |||
222c97d63c | |||
6a1229a4ba | |||
2bff4db1d7 | |||
a3b123a207 | |||
e698f3b224 | |||
c66baaa765 | |||
815be3126e | |||
715e953182 | |||
86915f6ec5 | |||
30b42de86f | |||
21217b067f | |||
9b160d2df0 | |||
74a9ee4f39 | |||
b0e775d299 | |||
d6b82f1556 | |||
3f642daf89 | |||
858cfe1d3c | |||
943d647a47 | |||
d96cf9470a | |||
c8b3cff5a6 | |||
547179b0ca | |||
0765f6606f | |||
12881d9345 | |||
79ed029db0 | |||
1a8da002cf | |||
c828d04f05 | |||
f919fe30fa | |||
b3db6643b9 | |||
cea3a909b5 | |||
b52d96a55e | |||
1fb71a172e | |||
27520c7ca4 | |||
af14cce3f8 | |||
75bcc91037 | |||
c78ff60231 | |||
09eb8794b8 | |||
470e54da8d | |||
19299598c3 | |||
f1c51ab8a7 | |||
b77c5d1d7f | |||
05481fc0c8 | |||
6b00452830 | |||
7e4c5dcf24 | |||
c0f30e56f7 | |||
9852646fff | |||
c2d678847e | |||
e77edf18ac | |||
3780aa6cf1 | |||
21124ec2ad | |||
0ee5fc59d3 | |||
b8dfef691e | |||
a1886b0b60 | |||
e1002b5315 | |||
fed90d4f7b | |||
dcfb329c5a | |||
b8a21a8baa | |||
7f73035b02 | |||
317437fedc | |||
26286984c2 | |||
9ae31c0146 | |||
87fd8fa96f | |||
c3401e732f | |||
4ee7373b3f | |||
64c0c5f8e6 | |||
18a597c726 | |||
|
baca596603 | ||
d993387216 | |||
aed2856df3 | |||
c78858c152 | |||
f49581259e | |||
f5bf642cd6 | |||
7687acb366 | |||
e655e1598a | |||
64ed38332f | |||
d438303aec | |||
38b3736951 | |||
6be060ea40 | |||
5c5756150f |
6
.gitignore
vendored
@ -65,12 +65,6 @@ coverage.xml
|
|||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
# exclude migrations from repository. These should be created locally, matching local DB requirements.
|
|
||||||
# lostplaces/manage.py makemigrations && lostplaces/manage.py migrate
|
|
||||||
|
|
||||||
lostplaces/lostplaces_app/migrations/
|
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
|
18
LICENSE
@ -1,7 +1,19 @@
|
|||||||
Copyright 2020 Reverend, Commander1024
|
Copyright 2020 Reverend, Commander1024
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
IN THE SOFTWARE.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
include LICENSE
|
include LICENSE
|
||||||
include Readme.rst
|
include Readme.rst
|
||||||
include Pipfile
|
include Pipfile
|
||||||
recursive-include lostplaces_app/static *
|
recursive-include lostplaces/static *
|
||||||
recursive-include lostplaces_app/templates *
|
recursive-include lostplaces/templates *
|
10
Pipfile
@ -11,6 +11,7 @@ pipenv = "*"
|
|||||||
wheel = "*"
|
wheel = "*"
|
||||||
twine = "*"
|
twine = "*"
|
||||||
pandoc = "*"
|
pandoc = "*"
|
||||||
|
pylint-django = "*"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
django = "*"
|
django = "*"
|
||||||
@ -18,6 +19,9 @@ easy-thumbnails = "*"
|
|||||||
image = "*"
|
image = "*"
|
||||||
django-widget-tweaks = "*"
|
django-widget-tweaks = "*"
|
||||||
django-taggit = "*"
|
django-taggit = "*"
|
||||||
# Commented out to not explicitly specify Python 3 subversion.
|
|
||||||
# [requires]
|
[scripts]
|
||||||
# python_version = "3.8"
|
test = "django_lostplaces/manage.py test lostplaces"
|
||||||
|
server = "django_lostplaces/manage.py runserver --ipv6"
|
||||||
|
dbshell = "django_lostplaces/manage.py dbshell"
|
||||||
|
showmigrations = "django_lostplaces/manage.py showmigrations"
|
||||||
|
63
Readme.md
@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
lostplaces-backend is a django (3.x) based webproject. It once wants to become a software which allows a group of urban explorers to manage, document and share the locations of lost places while not exposing too much / any information to the public.
|
lostplaces-backend is a django (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.
|
The software is currently in early development status, neither scope, datamodel(s) nor features are finalized yet. Therefore we would not recommend to download or install this piece of software anywhere - except your local django dev server.
|
||||||
|
|
||||||
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.
|
We value privacy as a whole, all resources the frontend requires will be shipped with lostplace's distribution. We also try to minimize the use of JavaScript as far as we can and try to offer JS-less alternatives where we can.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Manage lost places with lots of usefull information.
|
- Manage lost places with lots of useful information.
|
||||||
- OSM-Maps
|
- OSM-Maps
|
||||||
- Sensitive information is not accesiable for anonymous (not logged in) users.
|
- Sensitive information is not accessible for anonymous (not logged in) users.
|
||||||
- User self registration using a voucher system, only people you invite can join your instance.
|
- 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.
|
- Collaboration, every user can add informations like tags, photos and external links to your place.
|
||||||
|
|
||||||
@ -22,6 +22,13 @@ Right now it depends on the following non-core Python 3 libraries. These can be
|
|||||||
* [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.
|
||||||
|
|
||||||
|
### Bundled Dependencies
|
||||||
|
We also leverage some other great OpenSource projects' code. We bundle those in the distribution to obsolete the need to pull those files from monitored CDNs.
|
||||||
|
|
||||||
|
* [OpenLayers](https://openlayers.org/) [6.4.3] OpenLayers makes it easy to put a dynamic map in any web page. It can display map tiles, vector data and markers loaded from any source.
|
||||||
|
* [Crimson Pro font](https://github.com/Fonthausen/CrimsonPro) [1.002] Crimson Pro is a serif typeface family: Contemporary, clear, classic and rounded/open.
|
||||||
|
* [Montserrat](https://github.com/JulietaUla/Montserrat) [7.210] A beautiful sans serif typeface.
|
||||||
|
|
||||||
|
|
||||||
# Installing a development instance
|
# Installing a development instance
|
||||||
## Clone the repository
|
## Clone the repository
|
||||||
@ -34,10 +41,10 @@ After having obtained the repository contents (either via .zip download or git c
|
|||||||
$ cd lostplaces-backend
|
$ cd lostplaces-backend
|
||||||
$ pipenv install
|
$ pipenv install
|
||||||
$ pipenv shell
|
$ pipenv shell
|
||||||
(lostplaces-backend) $ lostplaces/manage.py makemigrations
|
(lostplaces-backend) $ django_lostplaces/manage.py makemigrations
|
||||||
(lostplaces-backend) $ lostplaces/manage.py migrate
|
(lostplaces-backend) $ django_lostplaces/manage.py migrate
|
||||||
(lostplaces-backend) $ lostplaces/manage.py createsuperuser
|
(lostplaces-backend) $ django_lostplaces/manage.py createsuperuser
|
||||||
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
|
(lostplaces-backend) $ django_lostplaces/manage.py runserver --ipv6
|
||||||
```
|
```
|
||||||
|
|
||||||
## Returning to the venv
|
## Returning to the venv
|
||||||
@ -45,9 +52,9 @@ $ pipenv 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
|
||||||
(lostplaces-backend) $ lostplaces/manage.py makemigrations # If datamodels changed
|
(lostplaces-backend) $ django_lostplaces/manage.py makemigrations # If datamodels changed
|
||||||
(lostplaces-backend) $ lostplaces/manage.py migrate # If datamodels changed
|
(lostplaces-backend) $ django_lostplaces/manage.py migrate # If datamodels changed
|
||||||
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
|
(lostplaces-backend) $ django_lostplaces/manage.py runserver --ipv6
|
||||||
```
|
```
|
||||||
|
|
||||||
Visit: [admin](http://localhost:8000/admin) for administrative backend or
|
Visit: [admin](http://localhost:8000/admin) for administrative backend or
|
||||||
@ -63,25 +70,25 @@ Currently there are two ways to deploy the lostplaces project:
|
|||||||
|
|
||||||
## Cloning the repository
|
## 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.
|
Essentially, this is the same as installing a development instance, but without the development server (manage.py runserver) and something powerful (Apache, NGINX) instead. You have to configure the webserver to work with the *SGI Api respectively, 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.
|
You also should setup a dedicated database server, the built-in SQLite file is not recommended 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`:
|
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)).
|
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`.
|
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/).
|
3. Tune the localization settings, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/).
|
||||||
|
|
||||||
Run `lostplaces/managy.py collectstatic` and you should be ready to go.
|
Run `django_lostplaces/managy.py collectstatic` and you should be ready to go.
|
||||||
|
|
||||||
|
|
||||||
## Installing the lostplaces_app to an existing django instance
|
## Installing lostplaces to an existing django instance
|
||||||
|
|
||||||
### Installing django and the lostplaces app
|
### Installing django and the django_lostplaces app
|
||||||
|
|
||||||
If you haven't already setup a django instance, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/install/).
|
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 releases 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.*
|
||||||
|
|
||||||
@ -93,7 +100,7 @@ Now configure your `settings.py` as follows:
|
|||||||
```python
|
```python
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
...
|
...
|
||||||
'lostplaces_app',
|
'django_lostplaces',
|
||||||
'easy_thumbnails',
|
'easy_thumbnails',
|
||||||
'widget_tweaks',
|
'widget_tweaks',
|
||||||
'django_taggit'
|
'django_taggit'
|
||||||
@ -110,28 +117,22 @@ MEDIA_URL = '/uploads/'
|
|||||||
MEDIA_ROOT = os.path.join(BASE_DIR, '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 URL's for login, for example:
|
||||||
|
|
||||||
```python
|
|
||||||
AUTH_USER_MODEL = 'lostplaces_app.Explorer'
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Set the URL's for login, for example:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
LOGIN_URL = reverse_lazy('login')
|
LOGIN_URL = reverse_lazy('login')
|
||||||
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
LOGIN_REDIRECT_URL = reverse_lazy('django_lostplaces_home')
|
||||||
LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
LOGOUT_REDIRECT_URL = reverse_lazy('django_lostplaces_home')
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuring the URL's
|
### Configuring the URL's
|
||||||
In the `urls.py` configure the `urlpatter` like this:
|
In the `urls.py` configure the `urlpatterns` like this:
|
||||||
```python
|
```python
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('signup/', SignUpView.as_view(), name='signup'), # If you want to use lostplaces' sign up view.
|
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('explorers/', include('django.contrib.auth.urls')), # You can change the 'explorers/' to whatever you desire.
|
||||||
path('', include('lostplaces_app.urls')), # In this configuration lostplaces will be at the top level of you website, change '' to 'lostplaces/', if you don't want this.
|
path('', include('django_lostplaces.urls')), # In this configuration django_lostplaces will be at the top level of you website, change '' to 'django_lostplaces/', if you don't want this.
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # So django can deliver user uploaded files.
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # So django can deliver user uploaded files.
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -139,5 +140,7 @@ Before making the django instance public, you should tweak the config `settings.
|
|||||||
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)).
|
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`.
|
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/).
|
3. Tune the localization settings, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/).
|
||||||
|
4. Set a new (random) SECRET_KEY in settings.py, e. g.: `base64 /dev/urandom | head -c50`
|
||||||
|
|
||||||
Run `lostplaces/managy.py collectstatic` you should be ready to go.
|
|
||||||
|
Run `django_lostplaces/manage.py collectstatic` you should be ready to go.
|
||||||
|
@ -14,6 +14,6 @@ import os
|
|||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lostplaces.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_lostplaces.settings')
|
||||||
|
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
@ -38,7 +38,7 @@ ALLOWED_HOSTS = ['localhost']
|
|||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'lostplaces_app',
|
'lostplaces',
|
||||||
'easy_thumbnails',
|
'easy_thumbnails',
|
||||||
'widget_tweaks',
|
'widget_tweaks',
|
||||||
'taggit',
|
'taggit',
|
||||||
@ -60,7 +60,7 @@ MIDDLEWARE = [
|
|||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'lostplaces.urls'
|
ROOT_URLCONF = 'django_lostplaces.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
@ -78,7 +78,7 @@ TEMPLATES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'lostplaces.wsgi.application'
|
WSGI_APPLICATION = 'django_lostplaces.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
@ -131,9 +131,15 @@ USE_TZ = True
|
|||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
|
||||||
|
|
||||||
|
# Upload directory
|
||||||
MEDIA_URL = '/uploads/'
|
MEDIA_URL = '/uploads/'
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
THUMBNAIL_MEDIA_ROOT = os.path.join(MEDIA_ROOT, 'thumbs/')
|
||||||
|
THUMBNAIL_MEDIA_URL = os.path.join(MEDIA_URL, 'thumbs/')
|
||||||
|
THUMBNAIL_QUALITY = 75
|
||||||
|
|
||||||
# Templates to use for authentication
|
# Templates to use for authentication
|
||||||
LOGIN_URL = reverse_lazy('login')
|
LOGIN_URL = reverse_lazy('login')
|
||||||
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
@ -23,11 +23,11 @@ from django.conf.urls.static import static
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
from lostplaces_app.views import SignUpView
|
from lostplaces.views import SignUpView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('signup/', SignUpView.as_view(), name='signup'),
|
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.urls')),
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
@ -14,6 +14,6 @@ import os
|
|||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lostplaces.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_lostplaces.settings')
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
155
django_lostplaces/docs/developer.md
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# django lostplaces documentation for developer
|
||||||
|
|
||||||
|
Greetings,
|
||||||
|
this documentation is for anyone who wants to work with this projects codebase or is just curious about how it works.
|
||||||
|
|
||||||
|
|
||||||
|
## Models
|
||||||
|
The models contain an custom user profile, some abstract base models and the models holding the actual data for this project
|
||||||
|
|
||||||
|
|
||||||
|
### Explorer user profile
|
||||||
|
Location: `lostplaces.models.models.Explorer`
|
||||||
|
Import from: `lostplaces.models.Explorer`
|
||||||
|
#### Super Classes
|
||||||
|
- django's `models.Model`
|
||||||
|
|
||||||
|
The class `lostplaces.models.Explorer` is our custom user profile. It has an ForeignKey pointing at django's default user model instead of providing an entire custom user model. That way this django app does not conflict with any other app that brings it's own user model.
|
||||||
|
|
||||||
|
You can access the explorer profile by accessing the 'explorer' attribute of any user instance
|
||||||
|
```python
|
||||||
|
user.explorer
|
||||||
|
```
|
||||||
|
|
||||||
|
Currently the explorer profile is used by the abstract model 'Submittable' and has the following realated names/fiels:
|
||||||
|
- [places](###place) A list containing all (lost) places the user has submitted
|
||||||
|
- [placeimages](###placeimages) A list containing all images relating a place that a user has submitted
|
||||||
|
- [photoalbums](###photoalbums) A list of all photo albums a explorere has submitted
|
||||||
|
|
||||||
|
|
||||||
|
### Taggable
|
||||||
|
Location: `lostplaces.models.abstract_models.Taggable`
|
||||||
|
Import from: `lostplaces.models.Taggable`
|
||||||
|
#### Super Classes
|
||||||
|
- django's `models.Model`
|
||||||
|
|
||||||
|
The abstract model Taggable represents an model that is taggable. It depends on the django app [taggit](https://github.com/jazzband/django-taggit). It only consists of one field:
|
||||||
|
- `tag`: TaggableManager, allows the sub class to be tagged, blank=True allows the admin form to be submitted without any tags
|
||||||
|
|
||||||
|
|
||||||
|
### Mapable
|
||||||
|
Location: `lostplaces.models.abstract_models.Mapable`
|
||||||
|
Import from: `lostplaces.models.Mapable`
|
||||||
|
#### Super Classes
|
||||||
|
- django's `models.Model`
|
||||||
|
|
||||||
|
The abstract model Mapable represents an model that can be displayed on a map. It consists of tree attributes
|
||||||
|
`name`
|
||||||
|
Name of the object, displayed on the map, max length 50 characters
|
||||||
|
`latitude`
|
||||||
|
Latitude of the referenced location, -90 <= value <= 90
|
||||||
|
`longitude`
|
||||||
|
Longitude of the referenced location -180 <= value <= 180
|
||||||
|
A mapable model has to provide its own get_absolute_url, in order to provide a link when clicked.
|
||||||
|
|
||||||
|
|
||||||
|
### Submittable
|
||||||
|
Location: `lostplaces.models.abstract_models.Submittable`
|
||||||
|
Import from: `lostplaces.models.Submittable`
|
||||||
|
#### Super Classes
|
||||||
|
- django's `models.Model`
|
||||||
|
|
||||||
|
The abstract model Submittable represents an model that can be submitted by an user. It knows who submitted something and when:
|
||||||
|
`submitted_by`
|
||||||
|
Referencing the explorer profile, see [Explorer](##explorer-user-profile). If the explorer profile is deleted, this instance is kept (on_delete=models.SET_NULL). The related_name is set to the class name, lower case appending an s (%(class)ss)
|
||||||
|
`submitted_when`
|
||||||
|
When the object was submitted, automatically set by django (auto_now_add=True)
|
||||||
|
|
||||||
|
### Expireable
|
||||||
|
Location: `lostplaces.models.abstract_models.Expireable`
|
||||||
|
Import from: `lostplaces.models.Expireable`
|
||||||
|
#### Super Classes
|
||||||
|
- django's `models.Model`
|
||||||
|
|
||||||
|
This abstract model represents an object that can expire. It is currently used by the Voucher model. I consists of two fields:
|
||||||
|
`created_when`
|
||||||
|
The date the object was created, automatically set by django (auto_now_add=True)
|
||||||
|
`expires_when`
|
||||||
|
The date the object expires.
|
||||||
|
|
||||||
|
### Voucher
|
||||||
|
Locatoin: `lostplaces.models.Voucher`
|
||||||
|
Import from: `lostplaces.models.Voucher`
|
||||||
|
#### Super Classes
|
||||||
|
- django's `models.Model`
|
||||||
|
- [lostplaces.models.Expireable](###expireable)
|
||||||
|
|
||||||
|
A voucher code is needed to sign up using lostplaces sign up form. The model contains
|
||||||
|
`code`
|
||||||
|
The voucher code, max length 30 characters
|
||||||
|
`created_when`
|
||||||
|
When the voucher was created automatically set by django (auto_now_add=True)
|
||||||
|
`expires_when`
|
||||||
|
Till what date the voucher remains valid
|
||||||
|
|
||||||
|
|
||||||
|
### Place
|
||||||
|
Location: `lostplaces.models.place.Place`
|
||||||
|
Import from: `lostplaces.models.Place`
|
||||||
|
#### Super Classes
|
||||||
|
- django's `models.Model`
|
||||||
|
- [lostplaces.models.Submittable](###submittable)-
|
||||||
|
- [lostplaces.models.Taggable](###taggable)
|
||||||
|
- [lostlaces.models.Mapable]
|
||||||
|
|
||||||
|
The place model is the heart of this project. It stores all information about a place needed.
|
||||||
|
`location`
|
||||||
|
Human readable location description (town, village, street), max length 50 characters
|
||||||
|
`description`
|
||||||
|
Describing the place in detail
|
||||||
|
The place model uses these abstract super classes
|
||||||
|
- Submittable, see [Submittable](##submittable) for additional fields
|
||||||
|
- Taggable, see [Taggable](##taggable) for additional fields
|
||||||
|
- Mapable, see [Mapable](##mapable) for additional fields
|
||||||
|
The average_latlon function takes a list of places and returns the center point of all these places.
|
||||||
|
|
||||||
|
### PlaceAsset
|
||||||
|
Location: `lostplaces.models.PlaceAsset`
|
||||||
|
Import from: `lostplaces.models.place.PlaceAsset`
|
||||||
|
#### Super Classes
|
||||||
|
- django's `models.Model`
|
||||||
|
- [lostplaces.models.Submittable](###submittable)
|
||||||
|
|
||||||
|
A PlaceAsset is anything that belongs to a place. The PlaceImage model for exmpaple is a subclass of PlaceAsset. A PlaceAsset is supposed to be submittable by every user. Only the user who has submitted the asset, the place the asset belongs to or both can delete a asset.
|
||||||
|
|
||||||
|
An PlaceAsset has the following fields
|
||||||
|
`place`
|
||||||
|
The Place this PlaceAsset belongs to
|
||||||
|
|
||||||
|
The PlaceAsset model is a subclass of Submittable, see [Submittable](###submittable) for more details.
|
||||||
|
|
||||||
|
### External Link
|
||||||
|
Loacation: `lostplaces.models.external_link.ExternalLink`
|
||||||
|
Import From: `lostplaces.models.ExternalLink`
|
||||||
|
#### Super Classes
|
||||||
|
- django's `models.Model`
|
||||||
|
- [lostplaces.models.Submittable](###submittable)
|
||||||
|
- [lostplaces.models.PlaceAsset](###placeasset)
|
||||||
|
|
||||||
|
This model represents an URL to an external website. External Link is an PlaceAsset, see [above](###placeasset) for more details. External Link has to fields
|
||||||
|
`url`
|
||||||
|
the URL to the target
|
||||||
|
`label`
|
||||||
|
the label that is shown on the website
|
||||||
|
|
||||||
|
### PhotoAlbum
|
||||||
|
Loacation: `lostplaces.models.external_link.PhototAlbum`
|
||||||
|
Import From: `lostplaces.models.PhotoAlbum`
|
||||||
|
#### Super Classes
|
||||||
|
- django's `models.Model`
|
||||||
|
- [lostplaces.models.Submittable](###submittable)
|
||||||
|
- [lostplaces.models.PlaceAsset](###placeasset)
|
||||||
|
- [lostplaces.models.ExternalLink](###externallink)
|
||||||
|
|
||||||
|
A photo album is a link to an external site that is meant to contain photos of the place it is referenced in. It
|
||||||
|
does not have any fields, just the ones inherited from it's super class [ExternalLink](###externallink).
|
12
django_lostplaces/lostplaces/__init__.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
settings.THUMBNAIL_ALIASES = {
|
||||||
|
'': {
|
||||||
|
'thumbnail': {'size': (300, 200), 'sharpen': True, 'crop': True},
|
||||||
|
'hero': {'size': (700, 466), 'sharpen': True, 'crop': True},
|
||||||
|
'large': {'size': (1920, 1920), 'sharpen': True, 'crop': False},
|
||||||
|
},
|
||||||
|
}
|
39
django_lostplaces/lostplaces/admin.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
''' Classes and modules for the administrative backend. '''
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.utils import timezone
|
||||||
|
from lostplaces.models import *
|
||||||
|
|
||||||
|
from lostplaces.forms import ExplorerCreationForm, ExplorerChangeForm
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
|
||||||
|
class VoucherAdmin(admin.ModelAdmin):
|
||||||
|
fields = ['code', 'expires_when', 'created_when']
|
||||||
|
readonly_fields = ['created_when']
|
||||||
|
list_display = ('__str__', 'code', 'created_when', 'expires_when', 'valid')
|
||||||
|
|
||||||
|
def valid(self, instance):
|
||||||
|
return timezone.now() <= instance.expires_when
|
||||||
|
|
||||||
|
valid.boolean = True
|
||||||
|
|
||||||
|
class PhotoAlbumsAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('label', 'place', 'url' )
|
||||||
|
|
||||||
|
class PlacesAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'submitted_by', 'submitted_when')
|
||||||
|
|
||||||
|
class PlaceImagesAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('__str__', 'place', 'submitted_by')
|
||||||
|
|
||||||
|
admin.site.register(Explorer)
|
||||||
|
admin.site.register(Voucher, VoucherAdmin)
|
||||||
|
admin.site.register(Place, PlacesAdmin)
|
||||||
|
admin.site.register(PlaceImage, PlaceImagesAdmin)
|
||||||
|
admin.site.register(PhotoAlbum, PhotoAlbumsAdmin)
|
@ -1,4 +1,7 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
class LostplacesAppConfig(AppConfig):
|
class LostplacesAppConfig(AppConfig):
|
||||||
name = 'lostplaces_app'
|
name = 'lostplaces'
|
@ -6,7 +6,7 @@
|
|||||||
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 django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from lostplaces_app.models import Place, PlaceImage, Voucher
|
from lostplaces.models import Place, PlaceImage, Voucher
|
||||||
|
|
||||||
class ExplorerCreationForm(UserCreationForm):
|
class ExplorerCreationForm(UserCreationForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -19,13 +19,17 @@ class ExplorerCreationForm(UserCreationForm):
|
|||||||
|
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
super().is_valid()
|
super().is_valid()
|
||||||
sumitted_voucher = self.cleaned_data.get('voucher')
|
submitted_voucher = self.cleaned_data.get('voucher')
|
||||||
try:
|
try:
|
||||||
fetched_voucher = Voucher.objects.get(code=sumitted_voucher)
|
fetched_voucher = Voucher.objects.get(code=submitted_voucher)
|
||||||
except Voucher.DoesNotExist:
|
except Voucher.DoesNotExist:
|
||||||
self.add_error('voucher', 'Invalid voucher')
|
self.add_error('voucher', 'Invalid voucher')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not submitted_voucher.valid:
|
||||||
|
self.add_error('voucher', 'Expired voucher')
|
||||||
|
return False
|
||||||
|
|
||||||
fetched_voucher.delete()
|
fetched_voucher.delete()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -40,7 +44,7 @@ class PlaceForm(forms.ModelForm):
|
|||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
exclude = ['submitted_by']
|
exclude = ['submitted_by']
|
||||||
|
|
||||||
class PlaceImageCreateForm(forms.ModelForm):
|
class PlaceImageForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PlaceImage
|
model = PlaceImage
|
||||||
fields = ['filename']
|
fields = ['filename']
|
90
django_lostplaces/lostplaces/migrations/0001_initial.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# Generated by Django 3.1.1 on 2020-09-28 18:39
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import easy_thumbnails.fields
|
||||||
|
import lostplaces.models.place
|
||||||
|
import taggit.managers
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('taggit', '0003_taggeditem_add_unique_index'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Expireable',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_when', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('expires_when', models.DateTimeField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Explorer',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='explorer', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Place',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=50)),
|
||||||
|
('latitude', models.FloatField(validators=[django.core.validators.MinValueValidator(-90), django.core.validators.MaxValueValidator(90)])),
|
||||||
|
('longitude', models.FloatField(validators=[django.core.validators.MinValueValidator(-180), django.core.validators.MaxValueValidator(180)])),
|
||||||
|
('submitted_when', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('location', models.CharField(max_length=50)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='places', to='lostplaces.explorer')),
|
||||||
|
('tags', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Voucher',
|
||||||
|
fields=[
|
||||||
|
('expireable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='lostplaces.expireable')),
|
||||||
|
('code', models.CharField(max_length=30, unique=True)),
|
||||||
|
],
|
||||||
|
bases=('lostplaces.expireable',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PlaceImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('submitted_when', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('description', models.TextField(blank=True)),
|
||||||
|
('filename', easy_thumbnails.fields.ThumbnailerImageField(upload_to=lostplaces.models.place.generate_image_upload_path)),
|
||||||
|
('place', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='placeimages', to='lostplaces.place')),
|
||||||
|
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='placeimages', to='lostplaces.explorer')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PhotoAlbum',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('submitted_when', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('url', models.URLField()),
|
||||||
|
('label', models.CharField(max_length=100)),
|
||||||
|
('place', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='photoalbums', to='lostplaces.place')),
|
||||||
|
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='photoalbums', to='lostplaces.explorer')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
4
django_lostplaces/lostplaces/models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from lostplaces.models.abstract_models import *
|
||||||
|
from lostplaces.models.place import *
|
||||||
|
from lostplaces.models.external_links import *
|
||||||
|
from lostplaces.models.models import *
|
61
django_lostplaces/lostplaces/models/abstract_models.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
|
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
|
class Taggable(models.Model):
|
||||||
|
'''
|
||||||
|
This abstract model represtens an object that is taggalble
|
||||||
|
using django-taggit
|
||||||
|
'''
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
tags = TaggableManager(blank=True)
|
||||||
|
|
||||||
|
class Mapable(models.Model):
|
||||||
|
'''
|
||||||
|
This abstract model class represents an object that can be
|
||||||
|
displayed on a map.
|
||||||
|
'''
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
latitude = models.FloatField(
|
||||||
|
validators=[
|
||||||
|
MinValueValidator(-90),
|
||||||
|
MaxValueValidator(90)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
longitude = models.FloatField(
|
||||||
|
validators=[
|
||||||
|
MinValueValidator(-180),
|
||||||
|
MaxValueValidator(180)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
class Submittable(models.Model):
|
||||||
|
'''
|
||||||
|
This abstract model class represents an object that can be submitted by
|
||||||
|
an explorer.
|
||||||
|
'''
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
submitted_when = models.DateTimeField(auto_now_add=True, null=True)
|
||||||
|
submitted_by = models.ForeignKey(
|
||||||
|
'Explorer',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='%(class)ss'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Expireable(models.Model):
|
||||||
|
"""
|
||||||
|
Base class for things that can expire, i.e. VouchersAv
|
||||||
|
"""
|
||||||
|
created_when = models.DateTimeField(auto_now_add=True)
|
||||||
|
expires_when = models.DateTimeField()
|
14
django_lostplaces/lostplaces/models/external_links.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from lostplaces.models.place import PlaceAsset
|
||||||
|
|
||||||
|
class ExternalLink(PlaceAsset):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
url = models.URLField(max_length=200)
|
||||||
|
label = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class PhotoAlbum(ExternalLink):
|
||||||
|
pass
|
55
django_lostplaces/lostplaces/models/models.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
'''
|
||||||
|
(Data)models which describe the structure of data to be saved into
|
||||||
|
database.
|
||||||
|
'''
|
||||||
|
|
||||||
|
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 lostplaces.models.abstract_models import Expireable
|
||||||
|
|
||||||
|
class Explorer(models.Model):
|
||||||
|
"""
|
||||||
|
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.user.username
|
||||||
|
|
||||||
|
@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(Expireable):
|
||||||
|
"""
|
||||||
|
Vouchers are authorization to created_when = models.DateTimeField(auto_now_add=True)
|
||||||
|
expires_when = models.DateTimeField()kens to allow the registration of new users.
|
||||||
|
A voucher has a code, a creation and a deletion date, which are all
|
||||||
|
positional. Creation date is being set automatically during voucher
|
||||||
|
creation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
code = models.CharField(unique=True, max_length=30)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "Voucher " + str(self.code)
|
||||||
|
|
129
django_lostplaces/lostplaces/models/place.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.db.models.signals import post_delete, pre_save
|
||||||
|
|
||||||
|
from lostplaces.models.abstract_models import Submittable, Taggable, Mapable
|
||||||
|
|
||||||
|
from easy_thumbnails.fields import ThumbnailerImageField
|
||||||
|
from easy_thumbnails.files import get_thumbnailer
|
||||||
|
|
||||||
|
class Place(Submittable, Taggable, Mapable):
|
||||||
|
"""
|
||||||
|
Place defines a lost place (location, name, description etc.).
|
||||||
|
"""
|
||||||
|
|
||||||
|
location = models.CharField(max_length=50)
|
||||||
|
description = models.TextField()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('place_detail', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
# Get center position of LP-geocoordinates.
|
||||||
|
def average_latlon(cls, place_list):
|
||||||
|
amount = len(place_list)
|
||||||
|
# Init fill values to prevent None
|
||||||
|
longitude = 0
|
||||||
|
latitude = 0
|
||||||
|
|
||||||
|
if amount > 0:
|
||||||
|
for place in place_list:
|
||||||
|
longitude += place.longitude
|
||||||
|
latitude += place.latitude
|
||||||
|
return {'latitude':latitude / amount, 'longitude': longitude / amount}
|
||||||
|
|
||||||
|
return {'latitude': latitude, 'longitude': longitude}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
def generate_image_upload_path(instance, filename):
|
||||||
|
"""
|
||||||
|
Callback for generating path for uploaded images.
|
||||||
|
Returns filename as: place_pk-placename{-rnd_string}.jpg
|
||||||
|
"""
|
||||||
|
|
||||||
|
return 'places/' + str(instance.place.pk) + '-' + str(instance.place.name) + '.' + filename.split('.')[-1]
|
||||||
|
|
||||||
|
class PlaceAsset(Submittable):
|
||||||
|
"""
|
||||||
|
Assets to a place, i.e. images
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
place = models.ForeignKey(
|
||||||
|
Place,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='%(class)ss',
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class PlaceImage (Submittable):
|
||||||
|
"""
|
||||||
|
PlaceImage defines an image file object that points to a file in uploads/.
|
||||||
|
Intermediate image sizes are generated as defined in THUMBNAIL_ALIASES.
|
||||||
|
PlaceImage references a Place to which it belongs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
filename = ThumbnailerImageField(
|
||||||
|
upload_to=generate_image_upload_path,
|
||||||
|
resize_source=dict(size=(2560, 2560),
|
||||||
|
sharpen=True)
|
||||||
|
)
|
||||||
|
place = models.ForeignKey(
|
||||||
|
Place,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='placeimages'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""
|
||||||
|
Returning the name of the corresponding place + id
|
||||||
|
of this image as textual representation of this instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
return 'Image ' + str(self.pk)
|
||||||
|
|
||||||
|
|
||||||
|
# These two auto-delete files from filesystem when they are unneeded:
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=PlaceImage)
|
||||||
|
def auto_delete_file_on_delete(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Deletes file (including thumbnails) from filesystem
|
||||||
|
when corresponding `PlaceImage` object is deleted.
|
||||||
|
"""
|
||||||
|
if instance.filename:
|
||||||
|
# Get and delete all files and thumbnails from instance
|
||||||
|
thumbmanager = get_thumbnailer(instance.filename)
|
||||||
|
thumbmanager.delete(save=False)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_save, sender=PlaceImage)
|
||||||
|
def auto_delete_file_on_change(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Deletes old file from filesystem
|
||||||
|
when corresponding `PlaceImage` object is updated
|
||||||
|
with new file.
|
||||||
|
"""
|
||||||
|
if not instance.pk:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_file = PlaceImage.objects.get(pk=instance.pk).filename
|
||||||
|
except PlaceImage.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# No need to delete thumbnails, as they will be overwritten on regeneration.
|
||||||
|
new_file = instance.filename
|
||||||
|
if not old_file == new_file:
|
||||||
|
if os.path.isfile(old_file.path):
|
||||||
|
os.remove(old_file.path)
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 264 KiB |
@ -1,5 +1,4 @@
|
|||||||
Copyright (c) 2010, Sebastian Kosch (sebastian@aldusleaf.org),
|
Copyright 2018 The Crimson Pro Project Authors (https://github.com/Fonthausen/CrimsonPro)
|
||||||
with Reserved Font Name "Crimson" and "Crimson Text".
|
|
||||||
|
|
||||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
This license is copied below, and is also available with a FAQ at:
|
This license is copied below, and is also available with a FAQ at:
|
93
django_lostplaces/lostplaces/static/fonts/Montserrat/OFL.txt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 816 B After Width: | Height: | Size: 816 B |
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 850 B After Width: | Height: | Size: 850 B |
BIN
django_lostplaces/lostplaces/static/icons/map-marker-icon.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 914 B After Width: | Height: | Size: 914 B |
Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 488 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 717 B After Width: | Height: | Size: 717 B |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
@ -424,46 +424,46 @@
|
|||||||
object-position: center; }
|
object-position: center; }
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Crimson Text';
|
font-family: 'Crimson Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: local("Crimson Text Regular"), local("CrimsonText-Regular"), url(fonts/Crimson/CrimsonText-Regular.ttf) format("truetype"); }
|
src: local("Crimson Pro Regular"), local("CrimsonPro-Regular"), url(fonts/Crimson/CrimsonPro-Regular.ttf) format("truetype"); }
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Crimson Text';
|
font-family: 'Crimson Pro';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: local("Crimson Text Italic"), local("CrimsonText-Italic"), url(fonts/Crimson/CrimsonText-Italic.ttf) format("truetype"); }
|
src: local("Crimson Pro Italic"), local("CrimsonPro-Italic"), url(fonts/Crimson/CrimsonPro-Italic.ttf) format("truetype"); }
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Crimson Text';
|
font-family: 'Crimson Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: local("Crimson Text Bold"), local("CrimsonText-Bold"), url(fonts/Crimson/CrimsonText-Bold.ttf) format("truetype"); }
|
src: local("Crimson Pro Bold"), local("CrimsonPro-Bold"), url(fonts/Crimson/CrimsonPro-Bold.ttf) format("truetype"); }
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Montserrat';
|
font-family: 'Montserrat';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: local("Montserrat Regular"), local("Montserrat-Regular"), url(fonts/Montserrat/Montserrat-Regular.ttf) format("truetype"); }
|
src: local("Montserrat Regular"), local("Montserrat-Regular"), url(fonts/Montserrat/Montserrat-Regular.woff2) format("woff2"); }
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Montserrat';
|
font-family: 'Montserrat';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: local("Montserrat Italic"), local("Montserrat-Italic"), url(fonts/Montserrat/Montserrat-Italic.ttf) format("truetype"); }
|
src: local("Montserrat Italic"), local("Montserrat-Italic"), url(fonts/Montserrat/Montserrat-Italic.woff2) format("woff2"); }
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Montserrat';
|
font-family: 'Montserrat';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: local("Montserrat Bold"), local("Montserrat-Bold"), url(fonts/Montserrat/Montserrat-Bold.ttf) format("truetype"); }
|
src: local("Montserrat Bold"), local("Montserrat-Bold"), url(fonts/Montserrat/Montserrat-Bold.woff2) format("woff2"); }
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -583,7 +583,7 @@ body {
|
|||||||
|
|
||||||
.LP-Paragraph {
|
.LP-Paragraph {
|
||||||
color: black;
|
color: black;
|
||||||
font-family: "Crimson Text", Times, serif;
|
font-family: "Crimson Pro", Times, serif;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -856,7 +856,7 @@ body {
|
|||||||
padding: 15px; } }
|
padding: 15px; } }
|
||||||
|
|
||||||
.LP-TextSection__Text {
|
.LP-TextSection__Text {
|
||||||
font-family: "Crimson Text", Times, serif;
|
font-family: "Crimson Pro", Times, serif;
|
||||||
font-size: 1.4rem; }
|
font-size: 1.4rem; }
|
||||||
.LP-TextSection__Text .LP-Link {
|
.LP-TextSection__Text .LP-Link {
|
||||||
margin: 0 3px; }
|
margin: 0 3px; }
|
||||||
@ -1588,23 +1588,78 @@ body {
|
|||||||
border: none; }
|
border: none; }
|
||||||
.LP-ImageGrid__Container {
|
.LP-ImageGrid__Container {
|
||||||
gap: 10px; }
|
gap: 10px; }
|
||||||
.LP-ImageGrid .LP-ImageGrid__Item, .LP-ImageGrid .LP-ImageGrid__Item * {
|
.LP-ImageGrid__Item {
|
||||||
|
position: relative; }
|
||||||
|
.LP-ImageGrid__Item, .LP-ImageGrid__Item * {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
word-break: break-all; }
|
word-break: break-all; }
|
||||||
.LP-ImageGrid .LP-ImageGrid__Item img {
|
.LP-ImageGrid__Item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover; }
|
object-fit: cover; }
|
||||||
.LP-ImageGrid .LP-ImageGrid__Item--left img {
|
.LP-ImageGrid__Item--left img {
|
||||||
object-position: left; }
|
object-position: left; }
|
||||||
.LP-ImageGrid .LP-ImageGrid__Item--center img {
|
.LP-ImageGrid__Item--center img {
|
||||||
object-position: center; }
|
object-position: center; }
|
||||||
.LP-ImageGrid .LP-ImageGrid__Item--top img {
|
.LP-ImageGrid__Item--top img {
|
||||||
object-position: top; }
|
object-position: top; }
|
||||||
.LP-ImageGrid .LP-ImageGrid__Item--bottom img {
|
.LP-ImageGrid__Item--bottom img {
|
||||||
object-position: botom; }
|
object-position: botom; }
|
||||||
.LP-ImageGrid .LP-ImageGrid__Item--center img {
|
.LP-ImageGrid__Item--center img {
|
||||||
object-position: center; }
|
object-position: center; }
|
||||||
|
.LP-ImageGrid__Item--add .LP-Link {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
background: #f9f9f9; }
|
||||||
|
.LP-ImageGrid__Item--add .LP-Link .LP-Icon {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%); }
|
||||||
|
.LP-ImageGrid__Item--add:hover .LP-Link {
|
||||||
|
background-color: #D7CEC7; }
|
||||||
|
.LP-ImageGrid__DeleteItem {
|
||||||
|
opacity: 0.7;
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background-color: #C09F80;
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 35px;
|
||||||
|
width: 35px; }
|
||||||
|
.LP-ImageGrid__DeleteItem .LP-Link .LP-Icon {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
position: relative;
|
||||||
|
left: 7.8125px;
|
||||||
|
top: 7.8125px; }
|
||||||
|
.LP-ImageGrid__Item > .LP-Link:hover + .LP-ImageGrid__DeleteItem, .LP-ImageGrid__DeleteItem:hover {
|
||||||
|
visibility: visible; }
|
||||||
|
|
||||||
|
.LP-Map {
|
||||||
|
margin-bottom: 25px; }
|
||||||
|
.LP-Map .ol-attribution {
|
||||||
|
font-family: "Montserrat", Helvetica, sans-serif;
|
||||||
|
color: #565656; }
|
||||||
|
.LP-Map .ol-attribution a {
|
||||||
|
color: #C09F80; }
|
||||||
|
.LP-Map .ol-attribution a:hover, .LP-Map .ol-attribution a:focus {
|
||||||
|
color: #D7CEC7; }
|
||||||
|
.LP-Map .ol-zoom-in, .LP-Map .ol-zoom-out {
|
||||||
|
background-color: #C09F80; }
|
||||||
|
.LP-Map .ol-zoom-in:hover, .LP-Map .ol-zoom-in:focus, .LP-Map .ol-zoom-out:hover, .LP-Map .ol-zoom-out:focus {
|
||||||
|
background-color: #565656; }
|
||||||
|
.LP-Map .LP-Map__Popup {
|
||||||
|
font-family: "Montserrat", Helvetica, sans-serif;
|
||||||
|
color: #565656;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: .5em;
|
||||||
|
border-radius: 2px; }
|
||||||
|
|
||||||
.LP-MainContainer {
|
.LP-MainContainer {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -1642,7 +1697,7 @@ body {
|
|||||||
margin-bottom: 25px; } }
|
margin-bottom: 25px; } }
|
||||||
|
|
||||||
.LP-TextSection .LP-UnorderedList {
|
.LP-TextSection .LP-UnorderedList {
|
||||||
font-family: "Crimson Text", Times, serif;
|
font-family: "Crimson Pro", Times, serif;
|
||||||
font-size: 1.4rem; }
|
font-size: 1.4rem; }
|
||||||
.LP-TextSection .LP-UnorderedList li {
|
.LP-TextSection .LP-UnorderedList li {
|
||||||
margin-bottom: 0.75em;
|
margin-bottom: 0.75em;
|
2
django_lostplaces/lostplaces/static/maps/ol.css
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.ol-box{box-sizing:border-box;border-radius:2px;border:2px solid #00f}.ol-mouse-position{top:8px;right:8px;position:absolute}.ol-scale-line{background:rgba(0,60,136,.3);border-radius:4px;bottom:8px;left:8px;padding:2px;position:absolute}.ol-scale-line-inner{border:1px solid #eee;border-top:none;color:#eee;font-size:10px;text-align:center;margin:1px;will-change:contents,width;transition:all .25s}.ol-scale-bar{position:absolute;bottom:8px;left:8px}.ol-scale-step-marker{width:1px;height:15px;background-color:#000;float:right;z-Index:10}.ol-scale-step-text{position:absolute;bottom:-5px;font-size:12px;z-Index:11;color:#000;text-shadow:-2px 0 #fff,0 2px #fff,2px 0 #fff,0 -2px #fff}.ol-scale-text{position:absolute;font-size:14px;text-align:center;bottom:25px;color:#000;text-shadow:-2px 0 #fff,0 2px #fff,2px 0 #fff,0 -2px #fff}.ol-scale-singlebar{position:relative;height:10px;z-Index:9;box-sizing:border-box;border:1px solid #000}.ol-unsupported{display:none}.ol-unselectable,.ol-viewport{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.ol-selectable{-webkit-touch-callout:default;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.ol-grabbing{cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.ol-grab{cursor:move;cursor:-webkit-grab;cursor:-moz-grab;cursor:grab}.ol-control{position:absolute;background-color:rgba(255,255,255,.4);border-radius:4px;padding:2px}.ol-control:hover{background-color:rgba(255,255,255,.6)}.ol-zoom{top:.5em;left:.5em}.ol-rotate{top:.5em;right:.5em;transition:opacity .25s linear,visibility 0s linear}.ol-rotate.ol-hidden{opacity:0;visibility:hidden;transition:opacity .25s linear,visibility 0s linear .25s}.ol-zoom-extent{top:4.643em;left:.5em}.ol-full-screen{right:.5em;top:.5em}.ol-control button{display:block;margin:1px;padding:0;color:#fff;font-size:1.14em;font-weight:700;text-decoration:none;text-align:center;height:1.375em;width:1.375em;line-height:.4em;background-color:rgba(0,60,136,.5);border:none;border-radius:2px}.ol-control button::-moz-focus-inner{border:none;padding:0}.ol-zoom-extent button{line-height:1.4em}.ol-compass{display:block;font-weight:400;font-size:1.2em;will-change:transform}.ol-touch .ol-control button{font-size:1.5em}.ol-touch .ol-zoom-extent{top:5.5em}.ol-control button:focus,.ol-control button:hover{text-decoration:none;background-color:rgba(0,60,136,.7)}.ol-zoom .ol-zoom-in{border-radius:2px 2px 0 0}.ol-zoom .ol-zoom-out{border-radius:0 0 2px 2px}.ol-attribution{text-align:right;bottom:.5em;right:.5em;max-width:calc(100% - 1.3em)}.ol-attribution ul{margin:0;padding:0 .5em;color:#000;text-shadow:0 0 2px #fff}.ol-attribution li{display:inline;list-style:none}.ol-attribution li:not(:last-child):after{content:" "}.ol-attribution img{max-height:2em;max-width:inherit;vertical-align:middle}.ol-attribution button,.ol-attribution ul{display:inline-block}.ol-attribution.ol-collapsed ul{display:none}.ol-attribution:not(.ol-collapsed){background:rgba(255,255,255,.8)}.ol-attribution.ol-uncollapsible{bottom:0;right:0;border-radius:4px 0 0}.ol-attribution.ol-uncollapsible img{margin-top:-.2em;max-height:1.6em}.ol-attribution.ol-uncollapsible button{display:none}.ol-zoomslider{top:4.5em;left:.5em;height:200px}.ol-zoomslider button{position:relative;height:10px}.ol-touch .ol-zoomslider{top:5.5em}.ol-overviewmap{left:.5em;bottom:.5em}.ol-overviewmap.ol-uncollapsible{bottom:0;left:0;border-radius:0 4px 0 0}.ol-overviewmap .ol-overviewmap-map,.ol-overviewmap button{display:inline-block}.ol-overviewmap .ol-overviewmap-map{border:1px solid #7b98bc;height:150px;margin:2px;width:150px}.ol-overviewmap:not(.ol-collapsed) button{bottom:1px;left:2px;position:absolute}.ol-overviewmap.ol-collapsed .ol-overviewmap-map,.ol-overviewmap.ol-uncollapsible button{display:none}.ol-overviewmap:not(.ol-collapsed){background:rgba(255,255,255,.8)}.ol-overviewmap-box{border:2px dotted rgba(0,60,136,.7)}.ol-overviewmap .ol-overviewmap-box:hover{cursor:move}
|
||||||
|
/*# sourceMappingURL=ol.css.map */
|
1
django_lostplaces/lostplaces/static/maps/ol.css.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":["src/ol/ol.css"],"names":[],"mappings":"AAAA,QACE,WAAY,WACZ,cAAe,IACf,OAAQ,IAAI,MAAM,KAGpB,mBACE,IAAK,IACL,MAAO,IACP,SAAU,SAGZ,eACE,WAAY,kBACZ,cAAe,IACf,OAAQ,IACR,KAAM,IACN,QAAS,IACT,SAAU,SAEZ,qBACE,OAAQ,IAAI,MAAM,KAClB,WAAY,KACZ,MAAO,KACP,UAAW,KACX,WAAY,OACZ,OAAQ,IACR,YAAa,QAAQ,CAAE,MACvB,WAAY,IAAI,KAElB,cACE,SAAU,SACV,OAAQ,IACR,KAAM,IAER,sBACE,MAAO,IACP,OAAQ,KACR,iBAAkB,KAClB,MAAO,MACP,QAAS,GAEX,oBACE,SAAU,SACV,OAAQ,KACR,UAAW,KACX,QAAS,GACT,MAAO,KACP,YAAa,KAAK,EAAE,IAAO,CAAE,EAAE,IAAI,IAAO,CAAE,IAAI,EAAE,IAAO,CAAE,EAAE,KAAK,KAEpE,eACE,SAAU,SACV,UAAW,KACX,WAAY,OACZ,OAAQ,KACR,MAAO,KACP,YAAa,KAAK,EAAE,IAAO,CAAE,EAAE,IAAI,IAAO,CAAE,IAAI,EAAE,IAAO,CAAE,EAAE,KAAK,KAEpE,oBACE,SAAU,SACV,OAAQ,KACR,QAAS,EACT,WAAY,WACZ,OAAQ,IAAI,MAAM,KAGpB,gBACE,QAAS,KAEG,iBAAd,aACE,sBAAuB,KACvB,oBAAqB,KACrB,iBAAkB,KAClB,gBAAiB,KACjB,YAAa,KACb,4BAA6B,YAE/B,eACE,sBAAuB,QACvB,oBAAqB,KACrB,iBAAkB,KAClB,gBAAiB,KACjB,YAAa,KAEf,aACE,OAAQ,iBACR,OAAQ,cACR,OAAQ,SAEV,SACE,OAAQ,KACR,OAAQ,aACR,OAAQ,UACR,OAAQ,KAEV,YACE,SAAU,SACV,iBAAkB,qBAClB,cAAe,IACf,QAAS,IAEX,kBACE,iBAAkB,qBAEpB,SACE,IAAK,KACL,KAAM,KAER,WACE,IAAK,KACL,MAAO,KACP,WAAY,QAAQ,KAAK,MAAM,CAAE,WAAW,GAAG,OAEjD,qBACE,QAAS,EACT,WAAY,OACZ,WAAY,QAAQ,KAAK,MAAM,CAAE,WAAW,GAAG,OAAO,KAExD,gBACE,IAAK,QACL,KAAM,KAER,gBACE,MAAO,KACP,IAAK,KAGP,mBACE,QAAS,MACT,OAAQ,IACR,QAAS,EACT,MAAO,KACP,UAAW,OACX,YAAa,IACb,gBAAiB,KACjB,WAAY,OACZ,OAAQ,QACR,MAAO,QACP,YAAa,KACb,iBAAkB,kBAClB,OAAQ,KACR,cAAe,IAEjB,qCACE,OAAQ,KACR,QAAS,EAEX,uBACE,YAAa,MAEf,YACE,QAAS,MACT,YAAa,IACb,UAAW,MACX,YAAa,UAEf,6BACE,UAAW,MAEb,0BACE,IAAK,MAGP,yBADA,yBAEE,gBAAiB,KACjB,iBAAkB,kBAEpB,qBACE,cAAe,IAAI,IAAI,EAAE,EAE3B,sBACE,cAAe,EAAE,EAAE,IAAI,IAIzB,gBACE,WAAY,MACZ,OAAQ,KACR,MAAO,KACP,UAAW,mBAGb,mBACE,OAAQ,EACR,QAAS,EAAE,KACX,MAAO,KACP,YAAa,EAAE,EAAE,IAAI,KAEvB,mBACE,QAAS,OACT,WAAY,KAEd,0CACE,QAAS,IAEX,oBACE,WAAY,IACZ,UAAW,QACX,eAAgB,OAEE,uBAApB,mBACE,QAAS,aAEX,gCACE,QAAS,KAEX,mCACE,WAAY,qBAEd,iCACE,OAAQ,EACR,MAAO,EACP,cAAe,IAAI,EAAE,EAEvB,qCACE,WAAY,MACZ,WAAY,MAEd,wCACE,QAAS,KAGX,eACE,IAAK,MACL,KAAM,KACN,OAAQ,MAEV,sBACE,SAAU,SACV,OAAQ,KAGV,yBACE,IAAK,MAGP,gBACE,KAAM,KACN,OAAQ,KAEV,iCACE,OAAQ,EACR,KAAM,EACN,cAAe,EAAE,IAAI,EAAE,EAEzB,oCACA,uBACE,QAAS,aAEX,oCACE,OAAQ,IAAI,MAAM,QAClB,OAAQ,MACR,OAAQ,IACR,MAAO,MAET,0CACE,OAAQ,IACR,KAAM,IACN,SAAU,SAEZ,iDACA,wCACE,QAAS,KAEX,mCACE,WAAY,qBAEd,oBACE,OAAQ,IAAI,OAAO,kBAGrB,0CACE,OAAQ"}
|
9
django_lostplaces/lostplaces/static/maps/ol.js
Normal file
1
django_lostplaces/lostplaces/static/maps/ol.js.map
Normal file
@ -14,7 +14,7 @@
|
|||||||
<article class="LP-TextSection">
|
<article class="LP-TextSection">
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{% include 'partials/osm_map.html' %}
|
{% include 'partials/osm_map.html' with config=mapping_config %}
|
||||||
<div class="LP-PlaceGrid">
|
<div class="LP-PlaceGrid">
|
||||||
<h1 class="LP-Headline LP-Headline">Explore the latest locations</h1>
|
<h1 class="LP-Headline LP-Headline">Explore the latest locations</h1>
|
||||||
<ul class="LP-PlaceGrid__Grid">
|
<ul class="LP-PlaceGrid__Grid">
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<a href="{% url 'place_detail' pk=place.pk %}" class="LP-Link">
|
<a href="{% url 'place_detail' pk=place.pk %}" class="LP-Link">
|
||||||
<article class="LP-PlaceTeaser">
|
<article class="LP-PlaceTeaser">
|
||||||
<div class="LP-PlaceTeaser__Image">
|
<div class="LP-PlaceTeaser__Image">
|
||||||
<img class="LP-Image" src="{{ place.images.first.filename.thumbnail.url}}" />
|
<img class="LP-Image" src="{{ place.placeimages.first.filename.thumbnail.url}}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="LP-PlaceTeaser__Meta">
|
<div class="LP-PlaceTeaser__Meta">
|
||||||
<div class="LP-PlaceTeaser__Info">
|
<div class="LP-PlaceTeaser__Info">
|
@ -34,7 +34,7 @@
|
|||||||
<a href="{% url 'place_detail' pk=place.pk %}" class="LP-Link">
|
<a href="{% url 'place_detail' pk=place.pk %}" class="LP-Link">
|
||||||
<article class="LP-PlaceTeaser">
|
<article class="LP-PlaceTeaser">
|
||||||
<div class="LP-PlaceTeaser__Image">
|
<div class="LP-PlaceTeaser__Image">
|
||||||
<img class="LP-Image" src="{{ place.images.first.filename.thumbnail.url}}" />
|
<img class="LP-Image" src="{{ place.placeimages.first.filename.thumbnail.url}}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="LP-PlaceTeaser__Meta">
|
<div class="LP-PlaceTeaser__Meta">
|
||||||
<div class="LP-PlaceTeaser__Info">
|
<div class="LP-PlaceTeaser__Info">
|
@ -0,0 +1,8 @@
|
|||||||
|
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
||||||
|
<button class="LP-Button">{% if action %}{{ action }}{% else %}Submit{% endif %}</button>
|
||||||
|
</div>
|
||||||
|
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
||||||
|
<a class="LP-Link" href="{% if referer %}{{ referer }}{% else %}{% url 'lostplaces_home' %}{% endif %}">
|
||||||
|
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
@ -1,6 +1,6 @@
|
|||||||
|
{% load static %}
|
||||||
<div id="map" class="map" style="height: 300px"></div>
|
<div tabindex="1" id="map" class="LP-Map map" style="height: 300px"></div>
|
||||||
<div id="info" class="map-popup"></div>
|
<div id="info" class="map-popup LP-Map__Popup"></div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var map = new ol.Map({
|
var map = new ol.Map({
|
||||||
@ -11,20 +11,20 @@
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
view: new ol.View({
|
view: new ol.View({
|
||||||
center: ol.proj.fromLonLat([{{place_map_center|last}}, {{place_map_center|first}}]),
|
center: ol.proj.fromLonLat([{{config.map_center.longitude}}, {{config.map_center.latitude}}]),
|
||||||
zoom: 9
|
zoom: 9
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
var vectorSource = new ol.source.Vector({
|
var vectorSource = new ol.source.Vector({
|
||||||
features: [
|
features: [
|
||||||
{% for place in place_list %}
|
{% for point in config.all_points %}
|
||||||
new ol.Feature({
|
new ol.Feature({
|
||||||
geometry: new ol.geom.Point(
|
geometry: new ol.geom.Point(
|
||||||
ol.proj.fromLonLat([{{place.longitude}},{{place.latitude}}])
|
ol.proj.fromLonLat([{{point.longitude}},{{point.latitude}}])
|
||||||
),
|
),
|
||||||
url: '{% url 'place_detail' pk=place.pk %}',
|
url: '{{point.get_absolute_url}}',
|
||||||
name: '{{place.name}}'
|
name: ' {{point.name}}'
|
||||||
}),
|
}),
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
]
|
]
|
||||||
@ -37,8 +37,8 @@
|
|||||||
anchor: [0.5, 46],
|
anchor: [0.5, 46],
|
||||||
anchorXUnits: 'fraction',
|
anchorXUnits: 'fraction',
|
||||||
anchorYUnits: 'pixels',
|
anchorYUnits: 'pixels',
|
||||||
scale: 0.3,
|
scale: 0.02,
|
||||||
src: 'http://icons.iconarchive.com/icons/paomedia/small-n-flat/128/map-marker-icon.png'
|
src: '{% static "icons/map-marker-icon.png" %}'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
@ -1,6 +1,6 @@
|
|||||||
<div class="LP-TagList">
|
<div class="LP-TagList">
|
||||||
<ul class="LP-TagList__List">
|
<ul class="LP-TagList__List">
|
||||||
{% for tag in tag_list %}
|
{% for tag in config.tagged_item.tags.all %}
|
||||||
<li class="LP-TagList__Item">
|
<li class="LP-TagList__Item">
|
||||||
<div class="LP-Tag">
|
<div class="LP-Tag">
|
||||||
<a href="#" class="LP-Link">
|
<a href="#" class="LP-Link">
|
||||||
@ -23,7 +23,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="id_tag_submit_form" class="LP-Form LP-Form--inline LP-Form--tagging" method="POST" action="{{config.submit_url}}">
|
<form id="id_tag_submit_form" class="LP-Form LP-Form--inline LP-Form--tagging" method="POST" action="{% url config.submit_url_name tagged_id=config.tagged_item.id%}">
|
||||||
<fieldset class="LP-Form__Fieldset">
|
<fieldset class="LP-Form__Fieldset">
|
||||||
<legend class="LP-Form__Legend">Tags hinzufügen</legend>
|
<legend class="LP-Form__Legend">Tags hinzufügen</legend>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@ -46,14 +46,10 @@
|
|||||||
submit_form.onsubmit = () => false
|
submit_form.onsubmit = () => false
|
||||||
|
|
||||||
const tagify = new Tagify(input, {
|
const tagify = new Tagify(input, {
|
||||||
'whitelist': [{
|
'whitelist': [
|
||||||
%
|
{% for tag in config.all_tags %}
|
||||||
for tag in all_tags %
|
|
||||||
}
|
|
||||||
'{{tag}}',
|
'{{tag}}',
|
||||||
{
|
{% endfor %}
|
||||||
% endfor %
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
@ -27,14 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="LP-Form__Composition LP-Form__Composition--buttons">
|
<div class="LP-Form__Composition LP-Form__Composition--buttons">
|
||||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER %}
|
||||||
<button class="LP-Button">Submit</button>
|
|
||||||
</div>
|
|
||||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
|
||||||
<a class="LP-Link" href="{% url 'place_detail' pk=place.id%}">
|
|
||||||
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
@ -39,14 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="LP-Form__Composition LP-Form__Composition--buttons">
|
<div class="LP-Form__Composition LP-Form__Composition--buttons">
|
||||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER action='Create' %}
|
||||||
<button class="LP-Button">Create</button>
|
|
||||||
</div>
|
|
||||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
|
||||||
<a class="LP-Link" href="{% url 'place_list' %}">
|
|
||||||
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
@ -16,14 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="LP-Form__Composition LP-Form__Composition--buttons">
|
<div class="LP-Form__Composition LP-Form__Composition--buttons">
|
||||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
{% include 'partials/form/submit.html' with referer=request.META.HTTP_REFERER action='Delete' %}
|
||||||
<button class="LP-Button">Delete</button>
|
|
||||||
</div>
|
|
||||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
|
||||||
<a class="LP-Link" href="{% url 'place_detail' pk=place.pk %}">
|
|
||||||
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
@ -23,9 +23,9 @@
|
|||||||
|
|
||||||
<header class="LP-PlaceDetail__Header">
|
<header class="LP-PlaceDetail__Header">
|
||||||
<h1 class="LP-Headline">{{ place.name }}</h1>
|
<h1 class="LP-Headline">{{ place.name }}</h1>
|
||||||
{% if place.images.first.filename.hero.url %}
|
{% if place.placeimages.first.filename.hero.url %}
|
||||||
<figure class="LP-PlaceDetail__Image">
|
<figure class="LP-PlaceDetail__Image">
|
||||||
<img src="{{ place.images.first.filename.hero.url }}" class="LP-Image" />
|
<img src="{{ place.placeimages.first.filename.hero.url }}" class="LP-Image" />
|
||||||
</figure>
|
</figure>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
@ -37,13 +37,13 @@
|
|||||||
<section class="LP-Section">
|
<section class="LP-Section">
|
||||||
|
|
||||||
{% url 'place_tag_submit' place_id=place.id as tag_submit_url%}
|
{% url 'place_tag_submit' place_id=place.id as tag_submit_url%}
|
||||||
{% include 'partials/tagging.html' with tag_list=place.tags.all config=tagging_config all_tags=all_tags %}
|
{% include 'partials/tagging.html' with config=tagging_config %}
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="LP-Section">
|
<section class="LP-Section">
|
||||||
<h1 class="LP-Headline">Map-Links</h1>
|
<h1 class="LP-Headline">Map links</h1>
|
||||||
{% include 'partials/osm_map.html' %}
|
{% include 'partials/osm_map.html' with config=mapping_config%}
|
||||||
<div class="LP-LinkList">
|
<div class="LP-LinkList">
|
||||||
<ul class="LP-LinkList__Container">
|
<ul class="LP-LinkList__Container">
|
||||||
<li class="LP-LinkList__Item"><a target="_blank" href="https://www.google.com/maps?q={{place.latitude}},{{place.longitude}}" class="LP-Link"><span class="LP-Text">Google Maps</span></a></li>
|
<li class="LP-LinkList__Item"><a target="_blank" href="https://www.google.com/maps?q={{place.latitude}},{{place.longitude}}" class="LP-Link"><span class="LP-Text">Google Maps</span></a></li>
|
||||||
@ -54,10 +54,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class=" LP-Section">
|
<section class=" LP-Section">
|
||||||
<h1 class="LP-Headline">Photoalben</h1>
|
<h1 class="LP-Headline">Photo albums</h1>
|
||||||
<div class="LP-LinkList">
|
<div class="LP-LinkList">
|
||||||
<ul class="LP-LinkList__Container">
|
<ul class="LP-LinkList__Container">
|
||||||
{% for photo_album in place.photo_albums.all %}
|
{% for photo_album in place.photoalbums.all %}
|
||||||
<li class="LP-LinkList__Item">
|
<li class="LP-LinkList__Item">
|
||||||
<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>
|
||||||
@ -89,14 +89,28 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="LP-Section">
|
<section class="LP-Section">
|
||||||
<h1 class="LP-Headline">Bilder</h1>
|
<h1 class="LP-Headline">Images</h1>
|
||||||
<div class="LP-ImageGrid">
|
<div class="LP-ImageGrid">
|
||||||
<ul class="LP-ImageGrid__Container">
|
<ul class="LP-ImageGrid__Container">
|
||||||
{% for place_image in place.images.all %}
|
{% for place_image in place.placeimages.all %}
|
||||||
<li class="LP-ImageGrid__Item">
|
<li class="LP-ImageGrid__Item">
|
||||||
<a href="{{ place_image.filename.large.url }}" class="LP-Link"><img class="LP-Image" src="{{ place_image.filename.thumbnail.url }}"></a>
|
<a href="{{ place_image.filename.large.url }}" class="LP-Link">
|
||||||
|
<img class="LP-Image" src="{{ place_image.filename.thumbnail.url }}">
|
||||||
|
</a>
|
||||||
|
{% if user.explorer == place_image.submitted_by or user.explorer == place.submitted_by %}
|
||||||
|
<span class="LP-ImageGrid__DeleteItem" title="Bild löschen">
|
||||||
|
<a href="{% url 'place_image_delete' pk=place_image.id %}" class="LP-Link">
|
||||||
|
<img class="LP-Icon" src="{% static 'icons/cancel.svg' %}" />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<li class="LP-ImageGrid__Item LP-ImageGrid__Item--add" title="Bild hinzufügen">
|
||||||
|
<a class="LP-Link" href="{% url 'place_image_create' place_id=place.id%}">
|
||||||
|
<img class="LP-Icon" src="{% static 'icons/plus.svg' %}" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
59
django_lostplaces/lostplaces/templates/place/place_list.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{% extends 'global.html'%}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block additional_head %}
|
||||||
|
<link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css">
|
||||||
|
<script src="{% static 'maps/ol.js' %}"></script>
|
||||||
|
{% endblock additional_head %}
|
||||||
|
|
||||||
|
{% block title %}Lost Places{% endblock %}
|
||||||
|
|
||||||
|
{% block maincontent %}
|
||||||
|
|
||||||
|
{% include 'partials/osm_map.html' with config=mapping_config %}
|
||||||
|
<div class="LP-PlaceList">
|
||||||
|
<h1 class="LP-Headline">Listing our places</h1>
|
||||||
|
<ul class="LP-PlaceList__List">
|
||||||
|
{% for place in place_list %}
|
||||||
|
<li class="LP-PlaceList__Item">
|
||||||
|
<a href="{% url 'place_detail' pk=place.pk %}" class="LP-Link">
|
||||||
|
<article class="LP-PlaceTeaser LP-PlaceTeaser--extended">
|
||||||
|
<div class="LP-PlaceTeaser__Image">
|
||||||
|
<img class="LP-Image" src="{{ place.placeimages.first.filename.thumbnail.url }}" />
|
||||||
|
</div>
|
||||||
|
<div class="LP-PlaceTeaser__Meta">
|
||||||
|
<div class="LP-PlaceTeaser__Info">
|
||||||
|
<span class="LP-PlaceTeaser__Title">
|
||||||
|
<h2 class="LP-Headline LP-Headline--teaser">{{place.name}}</h2>
|
||||||
|
</span>
|
||||||
|
<span class="LP-PlaceTeaser__Detail">
|
||||||
|
<p class="LP-Paragraph">{{place.location}}</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="LP-PlaceTeaser__Description">
|
||||||
|
<p class="LP-Paragraph">
|
||||||
|
{% if place.description|length > 210 %}
|
||||||
|
{{place.description|truncatechars:210|truncatewords:-1}}
|
||||||
|
{% else %}
|
||||||
|
{{place.description}}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="LP-PlaceTeaser__Icons">
|
||||||
|
<ul class="LP-Icon__List">
|
||||||
|
<li class="LP-Icon__Item"><img class="LP-Icon" src="{% static '/icons/favourite.svg' %}" /></li>
|
||||||
|
<li class="LP-Icon__Item"><img class="LP-Icon" src="{% static '/icons/location.svg' %}" /></li>
|
||||||
|
<li class="LP-Icon__Item"><img class="LP-Icon" src="{% static '/icons/flag.svg' %}" /></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% include 'partials/nav/pagination.html' %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock maincontent %}
|
@ -39,14 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="LP-Form__Composition LP-Form__Composition--buttons">
|
<div class="LP-Form__Composition LP-Form__Composition--buttons">
|
||||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER action='Update' %}
|
||||||
<button class="LP-Button">Update</button>
|
|
||||||
</div>
|
|
||||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
|
||||||
<a class="LP-Link" href="{% url 'place_detail' pk=place.pk %}">
|
|
||||||
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
@ -0,0 +1,21 @@
|
|||||||
|
{% extends 'global.html'%}
|
||||||
|
|
||||||
|
{% block maincontent %}
|
||||||
|
<form class="LP-Form" method="POST" enctype="multipart/form-data">
|
||||||
|
<fieldset class="LP-Form__Fieldset">
|
||||||
|
<legend class="LP-Form__Legend">Submit images to an place</legend>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="LP-Form__Composition">
|
||||||
|
<div class="LP-Form__Field">
|
||||||
|
{% include 'partials/form/inputField.html' with field=form.filename %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="LP-Form__Composition LP-Form__Composition--buttons">
|
||||||
|
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER %}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{% endblock maincontent %}
|
Before Width: | Height: | Size: 209 B After Width: | Height: | Size: 209 B |
3
django_lostplaces/lostplaces/templatetags/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
@ -1,3 +1,6 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
@ -1,3 +1,6 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import json, os
|
import json, os
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
@ -6,7 +9,7 @@ from django.conf import settings
|
|||||||
from django.template import Library, TemplateSyntaxError
|
from django.template import Library, TemplateSyntaxError
|
||||||
|
|
||||||
#icons_json_path = getattr(settings, 'SVG_ICONS_SOURCE_FILE')
|
#icons_json_path = getattr(settings, 'SVG_ICONS_SOURCE_FILE')
|
||||||
icons_json_path = os.path.join(settings.BASE_DIR, 'lostplaces_app', 'static', 'icons', 'icons.icomoon.json')
|
icons_json_path = os.path.join(settings.BASE_DIR, 'lostplaces', 'static', 'icons', 'icons.icomoon.json')
|
||||||
icons_json = json.load(open(icons_json_path))
|
icons_json = json.load(open(icons_json_path))
|
||||||
|
|
||||||
register = Library()
|
register = Library()
|
5
django_lostplaces/lostplaces/tests/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth.models import User
|
144
django_lostplaces/lostplaces/tests/models/__init__.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Creating a test user
|
||||||
|
|
||||||
|
class ModelTestCase(TestCase):
|
||||||
|
'''
|
||||||
|
Base class for ModelTests.
|
||||||
|
Parameters:
|
||||||
|
- model : Class to test
|
||||||
|
'''
|
||||||
|
model = None
|
||||||
|
|
||||||
|
def assertField(self, field_name, field_class, must_have={}, must_not_have={}):
|
||||||
|
'''
|
||||||
|
Tests if a field exists under the given name and
|
||||||
|
if the field is of the right type.
|
||||||
|
Also checks if the field has the given must_have attributes
|
||||||
|
and does not have any of the must_not_have attributes. If you
|
||||||
|
dont care about the value of the attribute you can just set it to
|
||||||
|
something that fullfills value == False (i.e. '' or 0)
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
field = self.model._meta.get_field(field_name)
|
||||||
|
except FieldDoesNotExist:
|
||||||
|
self.fail(
|
||||||
|
'Expecting %s to have a field named \'%s\'' % (
|
||||||
|
self.model.__name__,
|
||||||
|
field_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
type(field), field_class,
|
||||||
|
msg='Expecting type of %s to be %s' % (
|
||||||
|
str(field),
|
||||||
|
field_class.__name__
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for key, value in must_have.items():
|
||||||
|
if value:
|
||||||
|
self.assertEqual(
|
||||||
|
getattr(field, key), value,
|
||||||
|
msg='Expeting the value of %s %s to be \'%s\'' % (
|
||||||
|
str(field),
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertTrue(
|
||||||
|
hasattr(field, key),
|
||||||
|
msg='Expeting %s to have \'%s\'' % (
|
||||||
|
str(field),
|
||||||
|
key
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for key, value in must_not_have.items():
|
||||||
|
if value:
|
||||||
|
self.assertTrue(
|
||||||
|
getattr(field, key) != value,
|
||||||
|
msg='Expeting the value of %s %s to not be \'%s\'' % (
|
||||||
|
str(field),
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertFalse(
|
||||||
|
hasattr(field, value),
|
||||||
|
msg='Expeting %s to not have \'%s\'' % (
|
||||||
|
str(field),
|
||||||
|
key
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return field
|
||||||
|
|
||||||
|
def assertCharField(self, field_name, min_length, max_length, must_have={}, must_hot_have={}):
|
||||||
|
'''
|
||||||
|
Tests if the given field is a char field and if its max_length
|
||||||
|
is in min_length and max_legth
|
||||||
|
'''
|
||||||
|
field = self.assertField(
|
||||||
|
field_name, models.CharField, must_have, must_hot_have)
|
||||||
|
self.assertTrue(
|
||||||
|
field.max_length in range(min_length, max_length),
|
||||||
|
msg='Expeting %s max_length to be in the range of %d and %d' % (
|
||||||
|
str(field),
|
||||||
|
min_length,
|
||||||
|
max_length
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def assertFloatField(self, field_name, min_value=None, max_value=None, must_have={}, must_hot_have={}):
|
||||||
|
'''
|
||||||
|
Tests if the field is a floatfield. If min_value and/or max_value are passed,
|
||||||
|
the validators of the field are also checked. The validator list of the field should
|
||||||
|
look like
|
||||||
|
[MinValueValidator, MayValueValidator], if both values are passed,
|
||||||
|
[MinValueValidator] if only min_value is passed,
|
||||||
|
[MaxValueValidator] if only max_value is passed
|
||||||
|
'''
|
||||||
|
field = self.assertField(
|
||||||
|
field_name, models.FloatField, must_have, must_hot_have)
|
||||||
|
if min_value:
|
||||||
|
self.assertTrue(
|
||||||
|
len(field.validators) >= 1,
|
||||||
|
msg='Expecting the first valiator of %s to check the minimum' % (
|
||||||
|
str(field)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
field.validators[0].limit_value,
|
||||||
|
min_value,
|
||||||
|
msg='Expecting the min value of %s min to be at least %d' % (
|
||||||
|
str(field),
|
||||||
|
min_value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if max_value:
|
||||||
|
index = 0
|
||||||
|
if min_value:
|
||||||
|
index += 1
|
||||||
|
self.assertTrue(
|
||||||
|
len(field.validators) >= index+1,
|
||||||
|
msg='Expecting the second valiator of %s to check the maximum' % (
|
||||||
|
str(field)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
field.validators[1].limit_value,
|
||||||
|
max_value,
|
||||||
|
msg='Expecting the max value of %s min to be at most %d' % (
|
||||||
|
str(field),
|
||||||
|
max_value
|
||||||
|
)
|
||||||
|
)
|
BIN
django_lostplaces/lostplaces/tests/models/im_a_image.jpeg
Normal file
After Width: | Height: | Size: 81 KiB |
@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from lostplaces.models import (
|
||||||
|
Taggable,
|
||||||
|
Mapable,
|
||||||
|
Submittable,
|
||||||
|
PlaceAsset,
|
||||||
|
Expireable
|
||||||
|
)
|
||||||
|
from lostplaces.tests.models import ModelTestCase
|
||||||
|
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
|
|
||||||
|
class TaggableTestCase(ModelTestCase):
|
||||||
|
|
||||||
|
model = Taggable
|
||||||
|
|
||||||
|
def test_tags(self):
|
||||||
|
self.assertField('tags', TaggableManager)
|
||||||
|
|
||||||
|
|
||||||
|
class MapableTestCase(ModelTestCase):
|
||||||
|
|
||||||
|
model = Mapable
|
||||||
|
|
||||||
|
def test_name(self):
|
||||||
|
self.assertCharField(
|
||||||
|
field_name='name',
|
||||||
|
min_length=10,
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_latitude(self):
|
||||||
|
self.assertFloatField(
|
||||||
|
field_name='latitude',
|
||||||
|
min_value=-90,
|
||||||
|
max_value=90
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_longitude(self):
|
||||||
|
self.assertFloatField(
|
||||||
|
field_name='longitude',
|
||||||
|
min_value=-180,
|
||||||
|
max_value=180
|
||||||
|
)
|
||||||
|
|
||||||
|
class SubmittableTestCase(ModelTestCase):
|
||||||
|
model = Submittable
|
||||||
|
|
||||||
|
def test_submitted_when(self):
|
||||||
|
self.assertField(
|
||||||
|
field_name='submitted_when',
|
||||||
|
field_class=models.DateTimeField,
|
||||||
|
must_have={'auto_now_add': True}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_submitted_by(self):
|
||||||
|
submitted_by = self.assertField(
|
||||||
|
field_name='submitted_by',
|
||||||
|
field_class=models.ForeignKey
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
submitted_by.remote_field.related_name,
|
||||||
|
'%(class)ss',
|
||||||
|
msg='Expecting the related_name of %s to be \'%%(class)ss\', got %s' % (
|
||||||
|
str(submitted_by),
|
||||||
|
submitted_by.remote_field.related_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
submitted_by.null,
|
||||||
|
msg='Expecting %s to has null=True' % (
|
||||||
|
str(submitted_by)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
submitted_by.blank,
|
||||||
|
msg='Expecting %s to has blank=True' % (
|
||||||
|
str(submitted_by)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
submitted_by.remote_field.on_delete,
|
||||||
|
models.SET_NULL,
|
||||||
|
msg='Expecting %s to be null when reference is delete (models.SET_NULL)' % (
|
||||||
|
str(submitted_by)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class PlaceAssetTestCase(ModelTestCase):
|
||||||
|
model = PlaceAsset
|
||||||
|
|
||||||
|
def test_place(self):
|
||||||
|
field = self.assertField('place', models.ForeignKey)
|
||||||
|
self.assertEqual(field.remote_field.on_delete, models.CASCADE,
|
||||||
|
msg='Expecting the deletion of %s to be cascading' % (
|
||||||
|
str(field)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_related_name = '%(class)ss'
|
||||||
|
self.assertEqual(field.remote_field.related_name, expected_related_name,
|
||||||
|
msg='Expecting the related name of %s to be %s' % (
|
||||||
|
str(field),
|
||||||
|
expected_related_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class ExpireableTestCase(ModelTestCase):
|
||||||
|
model = Expireable
|
@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from lostplaces.models import Explorer
|
||||||
|
|
||||||
|
class ExplorerTestCase(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(self):
|
||||||
|
User.objects.create_user(
|
||||||
|
username='testpeter',
|
||||||
|
password='Develop123'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_epxlorer_creation(self):
|
||||||
|
'''
|
||||||
|
Tests if the explorer profile will be automticly
|
||||||
|
created when a user is created
|
||||||
|
'''
|
||||||
|
|
||||||
|
user = User.objects.get(id=1)
|
||||||
|
explorer_list = Explorer.objects.all()
|
||||||
|
self.assertTrue(len(explorer_list) > 0,
|
||||||
|
msg='Expecting at least one Exlorer object, none found'
|
||||||
|
)
|
||||||
|
self.assertTrue(hasattr(user, 'explorer'),
|
||||||
|
msg='''Expecting the User instance to have an \'explorer\' attribute.
|
||||||
|
Check the Explorer model and the related name.'''
|
||||||
|
)
|
||||||
|
|
||||||
|
explorer = Explorer.objects.get(id=1)
|
||||||
|
self.assertEqual(explorer, user.explorer,
|
||||||
|
msg='''The Explorer object of the User did not match.
|
||||||
|
Expecting User with id 1 to have Explorer with id 1'''
|
||||||
|
)
|
||||||
|
|
||||||
|
explorer = Explorer.objects.get(id=1)
|
||||||
|
self.assertEqual(explorer.user, user,
|
||||||
|
msg='''The User object of the Explorer did not match.
|
||||||
|
Expecting Explorer with id 1 to have User with id 1'''
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_explorer_deletion(self):
|
||||||
|
'''
|
||||||
|
Tests if the Explorer objects get's deleted when the User instance is deleted
|
||||||
|
'''
|
||||||
|
|
||||||
|
user = User.objects.get(username='testpeter')
|
||||||
|
explorer_id = user.explorer.id
|
||||||
|
user.delete()
|
||||||
|
with self.assertRaises(models.ObjectDoesNotExist,
|
||||||
|
msg='Expecting explorer objec to be deleted when the corresponding User object is deleted'
|
||||||
|
):
|
||||||
|
Explorer.objects.get(id=explorer_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
101
django_lostplaces/lostplaces/tests/models/test_link_model.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.db import models
|
||||||
|
from django.core.files import File
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from lostplaces.models import ExternalLink, PhotoAlbum, Place
|
||||||
|
from lostplaces.tests.models import ModelTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalLinkTestCase(ModelTestCase):
|
||||||
|
model = ExternalLink
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.albumlink = ExternalLink.objects.get(id=1)
|
||||||
|
|
||||||
|
def test_label(self):
|
||||||
|
self.assertField('label', models.CharField)
|
||||||
|
|
||||||
|
def test_url(self):
|
||||||
|
self.assertField('url', models.URLField)
|
||||||
|
|
||||||
|
class PhotoAlbumTestCase(ModelTestCase):
|
||||||
|
model = PhotoAlbum
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username='testpeter',
|
||||||
|
password='Develop123'
|
||||||
|
)
|
||||||
|
|
||||||
|
place = Place.objects.create(
|
||||||
|
name='Im a place',
|
||||||
|
submitted_when=timezone.now(),
|
||||||
|
submitted_by=User.objects.get(username='testpeter').explorer,
|
||||||
|
location='Testtown',
|
||||||
|
latitude=50.5,
|
||||||
|
longitude=7.0,
|
||||||
|
description='This is just a test, do not worry'
|
||||||
|
)
|
||||||
|
place.tags.add('I am a tag', 'testlocation')
|
||||||
|
place.save()
|
||||||
|
|
||||||
|
PhotoAlbum.objects.create(
|
||||||
|
url='https://lostplaces.example.com/album/',
|
||||||
|
label='TestLink',
|
||||||
|
submitted_by=user.explorer,
|
||||||
|
place=place,
|
||||||
|
submitted_when=timezone.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.albumlink = PhotoAlbum.objects.get(id=1)
|
||||||
|
self.place = Place.objects.get(id=1)
|
||||||
|
|
||||||
|
def test_place(self):
|
||||||
|
field = self.assertField('place', models.ForeignKey)
|
||||||
|
self.assertEqual(field.remote_field.on_delete, models.CASCADE,
|
||||||
|
msg='Expecting the deletion of %s to be cascading' % (
|
||||||
|
str(field)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_related_name = 'photoalbums'
|
||||||
|
self.assertEqual(field.remote_field.related_name, expected_related_name,
|
||||||
|
msg='Expecting the related name of %s to be %s' % (
|
||||||
|
str(field),
|
||||||
|
expected_related_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_label(self):
|
||||||
|
albumlink = self.albumlink
|
||||||
|
|
||||||
|
self.assertTrue('TestLink' in albumlink.label,
|
||||||
|
msg='Expecting albumlink.label to contain \'TestLink\' string'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_url(self):
|
||||||
|
albumlink = self.albumlink
|
||||||
|
|
||||||
|
self.assertTrue('lostplaces.example.com' in albumlink.url,
|
||||||
|
msg='Expecting albumlink.url to contain \'lostplaces.example.com\' string'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_linked_place(self):
|
||||||
|
albumlink = self.albumlink
|
||||||
|
place = self.place
|
||||||
|
self.assertTrue(str(albumlink.place) in str(place.name),
|
||||||
|
msg='Expecting %s.__str__ to contain the name' % (
|
||||||
|
self.model.__name__
|
||||||
|
)
|
||||||
|
)
|
@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.db import models
|
||||||
|
from django.core.files import File
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from lostplaces.models import PlaceImage, Place
|
||||||
|
from lostplaces.tests.models import ModelTestCase
|
||||||
|
|
||||||
|
from easy_thumbnails.fields import ThumbnailerImageField
|
||||||
|
|
||||||
|
class PlaceImageTestCase(ModelTestCase):
|
||||||
|
model = PlaceImage
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username='testpeter',
|
||||||
|
password='Develop123'
|
||||||
|
)
|
||||||
|
|
||||||
|
place = Place.objects.create(
|
||||||
|
name='Im a place',
|
||||||
|
submitted_when=timezone.now(),
|
||||||
|
submitted_by=User.objects.get(username='testpeter').explorer,
|
||||||
|
location='Testtown',
|
||||||
|
latitude=50.5,
|
||||||
|
longitude=7.0,
|
||||||
|
description='This is just a test, do not worry'
|
||||||
|
)
|
||||||
|
place.tags.add('I a tag', 'testlocation')
|
||||||
|
place.save()
|
||||||
|
|
||||||
|
if not os.path.isdir(settings.MEDIA_ROOT):
|
||||||
|
os.mkdir(settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if not os.path.isfile(os.path.join(settings.MEDIA_ROOT, 'im_a_image_copy.jpeg')):
|
||||||
|
shutil.copyfile(
|
||||||
|
os.path.join(current_dir, 'im_a_image.jpeg'),
|
||||||
|
os.path.join(settings.MEDIA_ROOT, 'im_a_image_copy.jpeg')
|
||||||
|
)
|
||||||
|
|
||||||
|
shutil.copyfile(
|
||||||
|
os.path.join(current_dir, 'im_a_image.jpeg'),
|
||||||
|
os.path.join(settings.MEDIA_ROOT, 'im_a_image_changed.jpeg')
|
||||||
|
)
|
||||||
|
|
||||||
|
PlaceImage.objects.create(
|
||||||
|
description='Im a description',
|
||||||
|
filename=os.path.join(settings.MEDIA_ROOT, 'im_a_image_copy.jpeg'),
|
||||||
|
place=place,
|
||||||
|
submitted_when=timezone.now(),
|
||||||
|
submitted_by=user.explorer
|
||||||
|
)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.place_image = PlaceImage.objects.get(id=1)
|
||||||
|
|
||||||
|
def test_description(self):
|
||||||
|
self.assertField('description', models.TextField)
|
||||||
|
|
||||||
|
def test_filename(self):
|
||||||
|
self.assertField('filename',ThumbnailerImageField)
|
||||||
|
|
||||||
|
def test_place(self):
|
||||||
|
field = self.assertField('place', models.ForeignKey)
|
||||||
|
self.assertEqual(field.remote_field.on_delete, models.CASCADE,
|
||||||
|
msg='Expecting the deletion of %s to be cascading' % (
|
||||||
|
str(field)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_related_name = 'placeimages'
|
||||||
|
self.assertEqual(field.remote_field.related_name, expected_related_name,
|
||||||
|
msg='Expecting the related name of %s to be %s' % (
|
||||||
|
str(field),
|
||||||
|
expected_related_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_change_filename(self):
|
||||||
|
path = self.place_image.filename.path
|
||||||
|
self.place_image.filename = os.path.join(settings.MEDIA_ROOT, 'im_a_image_changed.jpeg')
|
||||||
|
self.place_image.save()
|
||||||
|
self.assertFalse(
|
||||||
|
os.path.isfile(path),
|
||||||
|
msg='Expecting the old file of an place_image to be deleteed when an place_image file is changed'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_deletion(self):
|
||||||
|
path = self.place_image.filename.path
|
||||||
|
self.place_image.delete()
|
||||||
|
self.assertFalse(
|
||||||
|
os.path.isfile(path),
|
||||||
|
msg='Expecting the file of an place_image to be deleteed when an place_image is deleted'
|
||||||
|
)
|