Compare commits
124 Commits
0.1.a2
...
14a98deff3
Author | SHA1 | Date | |
---|---|---|---|
14a98deff3 | |||
4919c90ae8 | |||
5c47587189 | |||
3223637471 | |||
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 | |||
6e6f4ced7b | |||
b806f7f9e3 | |||
98c5ddad65 | |||
654cd87006 | |||
533efb328c | |||
5b1b23d3e6 | |||
bcd003a685 | |||
c4adc3f434 |
11
.gitignore
vendored
@@ -65,12 +65,6 @@ coverage.xml
|
||||
# Translations
|
||||
*.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
|
||||
.python-version
|
||||
|
||||
@@ -94,4 +88,7 @@ env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Editors
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
# twine / pypi
|
||||
.pypirc
|
18
LICENSE
@@ -1,7 +1,19 @@
|
||||
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 README.rst
|
||||
include Readme.rst
|
||||
include Pipfile
|
||||
recursive-include lostplaces_app/static *
|
||||
recursive-include lostplaces_app/templates *
|
||||
recursive-include lostplaces/static *
|
||||
recursive-include lostplaces/templates *
|
13
Pipfile
@@ -8,6 +8,10 @@ pylint = "*"
|
||||
coverage = "*"
|
||||
autopep8 = "*"
|
||||
pipenv = "*"
|
||||
wheel = "*"
|
||||
twine = "*"
|
||||
pandoc = "*"
|
||||
pylint-django = "*"
|
||||
|
||||
[packages]
|
||||
django = "*"
|
||||
@@ -15,6 +19,9 @@ easy-thumbnails = "*"
|
||||
image = "*"
|
||||
django-widget-tweaks = "*"
|
||||
django-taggit = "*"
|
||||
# Commented out to not explicitly specify Python 3 subversion.
|
||||
# [requires]
|
||||
# python_version = "3.8"
|
||||
|
||||
[scripts]
|
||||
test = "django_lostplaces/manage.py test lostplaces"
|
||||
server = "django_lostplaces/manage.py runserver --ipv6"
|
||||
dbshell = "django_lostplaces/manage.py dbshell"
|
||||
showmigrations = "django_lostplaces/manage.py showmigrations"
|
||||
|
108
Readme.md
@@ -2,22 +2,32 @@
|
||||
|
||||
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 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
|
||||
- manage lost places with lots of usefull information
|
||||
- sensitive information is not accesiable for anonymous (not logged in) users
|
||||
- user self registration using a voucher system, only people you invite can join your instance
|
||||
- collaboration, every user can add informations like tags, photos and external links to your place
|
||||
- Manage lost places with lots of useful information.
|
||||
- OSM-Maps
|
||||
- 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.
|
||||
- Collaboration, every user can add informations like tags, photos and external links to your place.
|
||||
|
||||
## Dependencies
|
||||
Right now it depends on the following non-core Python 3 libraries. These can be installed using the package manager of your distribution or into the venv locally.
|
||||
|
||||
* [django](https://www.djangoproject.com/) django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.
|
||||
* [easy-thumbnails](https://github.com/SmileyChris/easy-thumbnails) A powerful, yet easy to implement thumbnailing application for Django 1.11+
|
||||
* [image](https://github.com/francescortiz/image) Image cropping for django
|
||||
* [django](https://www.djangoproject.com/) is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.
|
||||
* [easy-thumbnails](https://github.com/SmileyChris/easy-thumbnails) A powerful, yet easy to implement thumbnailing application for Django 1.11+.
|
||||
* [image](https://github.com/francescortiz/image) Image cropping for django.
|
||||
* [django-widget-tweaks](https://github.com/jazzband/django-widget-tweaks) Tweak the form field rendering in templates, not in python-level form definitions.
|
||||
* [django-taggit](https://github.com/jazzband/django-taggit) A simpler approach to tagging with Django.
|
||||
|
||||
### 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
|
||||
@@ -31,10 +41,10 @@ After having obtained the repository contents (either via .zip download or git c
|
||||
$ cd lostplaces-backend
|
||||
$ pipenv install
|
||||
$ pipenv shell
|
||||
(lostplaces-backend) $ lostplaces/manage.py makemigrations
|
||||
(lostplaces-backend) $ lostplaces/manage.py migrate
|
||||
(lostplaces-backend) $ lostplaces/manage.py createsuperuser
|
||||
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py makemigrations
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py migrate
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py createsuperuser
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py runserver --ipv6
|
||||
```
|
||||
|
||||
## Returning to the venv
|
||||
@@ -42,60 +52,62 @@ $ pipenv shell
|
||||
$ cd lostplaces-backend
|
||||
$ pipenv shell
|
||||
(lostplaces-backend) $ pipenv update # If dependencies changed, or updates available
|
||||
(lostplaces-backend) $ lostplaces/manage.py makemigrations # If datamodels changed
|
||||
(lostplaces-backend) $ lostplaces/manage.py migrate # If datamodels changed
|
||||
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py makemigrations # If datamodels changed
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py migrate # If datamodels changed
|
||||
(lostplaces-backend) $ django_lostplaces/manage.py runserver --ipv6
|
||||
```
|
||||
|
||||
Visit: [admin](http://localhost:8000/admin) for administrative backend or
|
||||
[frontend](http://localhost:8000/)
|
||||
[frontend](http://localhost:8000/).
|
||||
|
||||
Happy developing ;-)
|
||||
|
||||
# Installing a productive instance
|
||||
|
||||
Currently there are two ways to deploy the lostplaces project:
|
||||
1. Cloning this repository, including the configured django instance
|
||||
2. Install the package and setup the django instance your self
|
||||
1. Cloning this repository, including the configured django instance.
|
||||
2. Install the package and setup the django instance yourself.
|
||||
|
||||
## Cloning the repository
|
||||
|
||||
Essently, this is the same as installing an development instance, but without the development server (manage.py runserver) and something powerfull (Apache, NGINX) instead. You have to configure the webserve to work with the *SGI Api respectivly, reference [django's guide for deployment](https://docs.djangoproject.com/en/3.1/howto/deployment/) for further information.
|
||||
Essentially, this is the same as installing a development instance, but without the development server (manage.py runserver) and something 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 an dedicated database server, the build in SQLite file is not recommened for production use. Reference [django's guide for databases](https://docs.djangoproject.com/en/3.1/ref/databases/) for further information.
|
||||
You also should setup a dedicated database server, the built-in SQLite file is not 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`:
|
||||
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64))
|
||||
2. Turn off debug mode by setting `DEBUG = False`
|
||||
3. Tune the localization settings, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/)
|
||||
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64)).
|
||||
2. Turn off debug mode by setting `DEBUG = False`.
|
||||
3. Tune the localization settings, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/).
|
||||
|
||||
Run `lostplaces/managy.py collectstatic` you should be ready to go.
|
||||
Run `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 an django instance, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/install/).
|
||||
If you haven't already setup a django instance, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/install/).
|
||||
|
||||
After that, download the desired release (probably the latest one) [from the realeases page](https://git.mowoe.com/reverend/lostplaces-backend/releases) and install it using `pip install --user name-of-the-file.tar.gz`.
|
||||
After that, download the desired release (probably the latest one) [from the 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.*
|
||||
|
||||
### Configuring the django instance
|
||||
|
||||
Now configure your `settings.py` as follows:
|
||||
1. Add the following apps to the django project
|
||||
1. Add the following apps to the django project.
|
||||
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'lostplaces_app',
|
||||
'django_lostplaces',
|
||||
'easy_thumbnails',
|
||||
'widget_tweaks',
|
||||
'django_taggit'
|
||||
]
|
||||
```
|
||||
|
||||
2. Set the URL's and Root-directories for file handling, for example
|
||||
2. Set the URL's and Root-directories for file handling, for example:
|
||||
|
||||
```python
|
||||
STATIC_URL = '/static/'
|
||||
@@ -105,36 +117,30 @@ MEDIA_URL = '/uploads/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
||||
```
|
||||
|
||||
3. Set the user model (this will be changed in the next release)
|
||||
|
||||
```python
|
||||
AUTH_USER_MODEL = 'lostplaces_app.Explorer'
|
||||
```
|
||||
|
||||
4. Set the URL's for login, for example
|
||||
3. Set the URL's for login, for example:
|
||||
|
||||
```python
|
||||
LOGIN_URL = reverse_lazy('login')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
||||
LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('django_lostplaces_home')
|
||||
LOGOUT_REDIRECT_URL = reverse_lazy('django_lostplaces_home')
|
||||
```
|
||||
|
||||
### Configuring the URL's
|
||||
In the `urls.py` configure the `urlpatter` like this
|
||||
In the `urls.py` configure the `urlpatterns` like this:
|
||||
```python
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('signup/', SignUpView.as_view(), name='signup'), # If you want to use lostplaces sign up view
|
||||
path('explorers/', include('django.contrib.auth.urls')), # You can change the 'explorers/' to whatever you desire
|
||||
path('', include('lostplaces_app.urls')), # In this configuration lostplaces will be at the top level of you website, change '' to 'lostplaces/', if you don't want this
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # So django can deliver user uploaded files
|
||||
path('signup/', SignUpView.as_view(), name='signup'), # If you want to use lostplaces' sign up view.
|
||||
path('explorers/', include('django.contrib.auth.urls')), # You can change the 'explorers/' to whatever you desire.
|
||||
path('', include('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.
|
||||
```
|
||||
|
||||
Before making the django instance public, you should tweak the config `settings.py`:
|
||||
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64))
|
||||
2. Turn off debug mode by setting `DEBUG = False`
|
||||
3. Tune the localization settings, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/)
|
||||
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64)).
|
||||
2. Turn off debug mode by setting `DEBUG = False`.
|
||||
3. Tune the localization settings, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/).
|
||||
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.
|
||||
|
||||
Happy developing ;-)
|
||||
Run `django_lostplaces/manage.py collectstatic` you should be ready to go.
|
||||
|
199
Readme.rst
@@ -1,199 +0,0 @@
|
||||
lostplaces-backend
|
||||
==================
|
||||
|
||||
lostplaces-backend is a django (3.x) based webproject. It once wants to
|
||||
become a software which allows a group of urban explorers to manage,
|
||||
document and share the locations of lost places while not exposing too
|
||||
much / any information to the public.
|
||||
|
||||
The software is currently in early development status, neither scope,
|
||||
datalmodel(s) nor features are finalized yet. Therefore we would not
|
||||
recommend to download or install this piece of software anywhere -
|
||||
except your local django dev server.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- manage lost places with lots of usefull information
|
||||
- sensitive information is not accesiable for anonymous (not logged in)
|
||||
users
|
||||
- user self registration using a voucher system, only people you invite
|
||||
can join your instance
|
||||
- collaboration, every user can add informations like tags, photos and
|
||||
external links to your place
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
Right now it depends on the following non-core Python 3 libraries. These
|
||||
can be installed using the package manager of your distribution or into
|
||||
the venv locally.
|
||||
|
||||
- `django <https://www.djangoproject.com/>`__ django is a high-level
|
||||
Python Web framework that encourages rapid development and clean,
|
||||
pragmatic design.
|
||||
- `easy-thumbnails <https://github.com/SmileyChris/easy-thumbnails>`__
|
||||
A powerful, yet easy to implement thumbnailing application for Django
|
||||
1.11+
|
||||
- `image <https://github.com/francescortiz/image>`__ Image cropping for
|
||||
django
|
||||
- `django-widget-tweaks <https://github.com/jazzband/django-widget-tweaks>`__
|
||||
Tweak the form field rendering in templates, not in python-level form
|
||||
definitions.
|
||||
- `django-taggit <https://github.com/jazzband/django-taggit>`__ A
|
||||
simpler approach to tagging with Django.
|
||||
|
||||
Installing a development instance
|
||||
=================================
|
||||
|
||||
Clone the repository
|
||||
--------------------
|
||||
|
||||
``git clone https://git.mowoe.com/reverend/lostplaces-backend.git`` ##
|
||||
Setting up a (pipenv) virtual environment for development
|
||||
|
||||
After having obtained the repository contents (either via .zip download
|
||||
or git clone), you can easily setup a
|
||||
`pipenv <https://docs.pipenv.org/>`__ virtual environment. The repo
|
||||
provides a Pipfile for easy dependency management that does not mess
|
||||
with your system.
|
||||
|
||||
.. code:: shell
|
||||
|
||||
$ cd lostplaces-backend
|
||||
$ pipenv install
|
||||
$ pipenv shell
|
||||
(lostplaces-backend) $ lostplaces/manage.py makemigrations
|
||||
(lostplaces-backend) $ lostplaces/manage.py migrate
|
||||
(lostplaces-backend) $ lostplaces/manage.py createsuperuser
|
||||
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
|
||||
|
||||
Returning to the venv
|
||||
---------------------
|
||||
|
||||
.. code:: shell
|
||||
|
||||
$ cd lostplaces-backend
|
||||
$ pipenv shell
|
||||
(lostplaces-backend) $ pipenv update # If dependencies changed, or updates available
|
||||
(lostplaces-backend) $ lostplaces/manage.py makemigrations # If datamodels changed
|
||||
(lostplaces-backend) $ lostplaces/manage.py migrate # If datamodels changed
|
||||
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
|
||||
|
||||
Visit: `admin <http://localhost:8000/admin>`__ for administrative
|
||||
backend or `frontend <http://localhost:8000/>`__
|
||||
|
||||
Installing a productive instance
|
||||
================================
|
||||
|
||||
Currently there are two ways to deploy the lostplaces project: 1.
|
||||
Cloning this repository, including the configured django instance 2.
|
||||
Install the package and setup the django instance your self
|
||||
|
||||
Cloning the repository
|
||||
----------------------
|
||||
|
||||
Essently, this is the same as installing an development instance, but
|
||||
without the development server (manage.py runserver) and something
|
||||
powerfull (Apache, NGINX) instead. You have to configure the webserve to
|
||||
work with the \*SGI Api respectivly, reference `django's guide for
|
||||
deployment <https://docs.djangoproject.com/en/3.1/howto/deployment/>`__
|
||||
for further information.
|
||||
|
||||
You also should setup an dedicated database server, the build in SQLite
|
||||
file is not recommened for production use. Reference `django's guide for
|
||||
databases <https://docs.djangoproject.com/en/3.1/ref/databases/>`__ for
|
||||
further information.
|
||||
|
||||
Before making the django instance public, you should tweak the config
|
||||
``settings.py``: 1. Change the secret key, the one found in the config
|
||||
is already public. Choose something secure (i.e.
|
||||
`this <https://duckduckgo.com/?q=password+generator+64>`__) 2. Turn off
|
||||
debug mode by setting ``DEBUG = False`` 3. Tune the localization
|
||||
settings, see `django's
|
||||
documentation <https://docs.djangoproject.com/en/3.1/topics/i18n/>`__
|
||||
|
||||
Run ``lostplaces/managy.py collectstatic`` you should be ready to go.
|
||||
|
||||
Installing the lostplaces\_app to an existing django instance
|
||||
-------------------------------------------------------------
|
||||
|
||||
Installing django and the lostplaces app
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you haven't already setup an django instance, see `django's
|
||||
documentation <https://docs.djangoproject.com/en/3.1/topics/install/>`__.
|
||||
|
||||
After that, download the desired release (probably the latest one) `from
|
||||
the realeases
|
||||
page <https://git.mowoe.com/reverend/lostplaces-backend/releases>`__ and
|
||||
install it using ``pip install --user name-of-the-file.tar.gz``.
|
||||
|
||||
*Note: You can run pip install without the --user flag, which will
|
||||
require root privileges and introduces potential security issues*
|
||||
|
||||
Configuring the django instance
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Now configure your ``settings.py`` as follows: 1. Add the following apps
|
||||
to the django project
|
||||
|
||||
.. code:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'lostplaces_app',
|
||||
'easy_thumbnails',
|
||||
'widget_tweaks',
|
||||
'django_taggit'
|
||||
]
|
||||
|
||||
2. Set the URL's and Root-directories for file handling, for example
|
||||
|
||||
.. code:: python
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
|
||||
|
||||
MEDIA_URL = '/uploads/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
||||
|
||||
3. Set the user model (this will be changed in the next release)
|
||||
|
||||
.. code:: python
|
||||
|
||||
AUTH_USER_MODEL = 'lostplaces_app.Explorer'
|
||||
|
||||
4. Set the URL's for login, for example
|
||||
|
||||
.. code:: python
|
||||
|
||||
LOGIN_URL = reverse_lazy('login')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
||||
LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
||||
|
||||
Configuring the URL's
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In the ``urls.py`` configure the ``urlpatter`` like this
|
||||
|
||||
.. code:: python
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('signup/', SignUpView.as_view(), name='signup'), # If you want to use lostplaces sign up view
|
||||
path('explorers/', include('django.contrib.auth.urls')), # You can change the 'explorers/' to whatever you desire
|
||||
path('', include('lostplaces_app.urls')), # In this configuration lostplaces will be at the top level of you website, change '' to 'lostplaces/', if you don't want this
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # So django can deliver user uploaded files
|
||||
|
||||
Before making the django instance public, you should tweak the config
|
||||
``settings.py``: 1. Change the secret key, the one found in the config
|
||||
is already public. Choose something secure (i.e.
|
||||
`this <https://duckduckgo.com/?q=password+generator+64>`__) 2. Turn off
|
||||
debug mode by setting ``DEBUG = False`` 3. Tune the localization
|
||||
settings, see `django's
|
||||
documentation <https://docs.djangoproject.com/en/3.1/topics/i18n/>`__
|
||||
|
||||
Run ``lostplaces/managy.py collectstatic`` you should be ready to go.
|
||||
|
||||
Happy developing ;-)
|
@@ -14,6 +14,6 @@ import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lostplaces.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_lostplaces.settings')
|
||||
|
||||
application = get_asgi_application()
|
@@ -4,13 +4,16 @@
|
||||
"""
|
||||
Django settings for lostplaces project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.0.8.
|
||||
!!! Instead of changing this file, you can override settings in !!!
|
||||
!!! local_settings.py. That way it won't cause merge conflicts on pull. !!!
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.1.8.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/topics/settings/
|
||||
https://docs.djangoproject.com/en/3.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -21,7 +24,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'n$(bx8(^)*wz1ygn@-ekt7rl^1km*!_c+fwwjiua8m@-x_rpl0'
|
||||
@@ -35,7 +38,7 @@ ALLOWED_HOSTS = ['localhost']
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'lostplaces_app',
|
||||
'lostplaces',
|
||||
'easy_thumbnails',
|
||||
'widget_tweaks',
|
||||
'taggit',
|
||||
@@ -57,7 +60,7 @@ MIDDLEWARE = [
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'lostplaces.urls'
|
||||
ROOT_URLCONF = 'django_lostplaces.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
@@ -75,11 +78,11 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'lostplaces.wsgi.application'
|
||||
WSGI_APPLICATION = 'django_lostplaces.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
@@ -90,7 +93,7 @@ DATABASES = {
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
@@ -109,7 +112,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
@@ -123,18 +126,29 @@ USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
|
||||
|
||||
# Upload directory
|
||||
MEDIA_URL = '/uploads/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
||||
|
||||
# Use custom user model
|
||||
AUTH_USER_MODEL = 'lostplaces_app.Explorer'
|
||||
# Thumbnails
|
||||
THUMBNAIL_MEDIA_ROOT = os.path.join(MEDIA_ROOT, 'thumbs/')
|
||||
THUMBNAIL_MEDIA_URL = os.path.join(MEDIA_URL, 'thumbs/')
|
||||
THUMBNAIL_QUALITY = 75
|
||||
|
||||
# Templates to use for authentication
|
||||
LOGIN_URL = reverse_lazy('login')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
||||
LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
||||
|
||||
|
||||
# Load and override settings from local_settings.py and don't fail on errors.
|
||||
|
||||
try:
|
||||
from .local_settings import *
|
||||
except ImportError:
|
||||
pass
|
@@ -23,11 +23,11 @@ from django.conf.urls.static import static
|
||||
from django.urls import path, include
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from lostplaces_app.views import SignUpView
|
||||
from lostplaces.views import SignUpView
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('signup/', SignUpView.as_view(), name='signup'),
|
||||
path('explorers/', include('django.contrib.auth.urls')),
|
||||
path('', include('lostplaces_app.urls')),
|
||||
path('', include('lostplaces.urls')),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
@@ -14,6 +14,6 @@ import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lostplaces.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_lostplaces.settings')
|
||||
|
||||
application = get_wsgi_application()
|
75
django_lostplaces/docs/developer.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# django lostplaces documentation for developer
|
||||
|
||||
Greetings,
|
||||
this documentation is for anyone who wants to work with this projects codebase or is just curious about our project.
|
||||
|
||||
|
||||
## Models
|
||||
The models contain an custom user profile, some abstract base models and the models holding the actual data for this project
|
||||
|
||||
|
||||
### Explorer user profile
|
||||
The class `lostplaces.models.Explorer` is our custom user profile. It has an ForeignKey pointing at django's default user model instead of providing an entire custom user model. That way this django app does not conflict with any other app that has to bring it's own user model.
|
||||
|
||||
You can access the explorer profile by accessing the 'explorer' attribute of any user instance
|
||||
```python
|
||||
user.explorer
|
||||
```
|
||||
|
||||
Currently the explorer profile is used by the abstract model 'Submittable' and thus referenced by 'Place' and 'PlaceImage'. The explorer profile therefore has two attributes
|
||||
```python
|
||||
user.explorer.places
|
||||
user.explorer.placeimages
|
||||
```
|
||||
`places`
|
||||
A list containing all (lost) places the user has submitted
|
||||
`placeimages`
|
||||
A list containing all images relating a place that a user has submitted
|
||||
|
||||
|
||||
### Taggable
|
||||
The abstract model Taggable represents an model that is taggable. It depends on the django app [taggit](https://github.com/jazzband/django-taggit). It only consists of one field:
|
||||
`tag`
|
||||
TaggableManager, allows the sub class to be tagged, blank=True allows the admin form to be submitted without any tags
|
||||
|
||||
|
||||
### Mapable
|
||||
The abstract model Mapable represents an model that can be displayed on a map. It consists of tree members
|
||||
`name`
|
||||
Name of the object, displayed on the map, max length 50 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
|
||||
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)
|
||||
|
||||
|
||||
### Voucher
|
||||
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
|
||||
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
|
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
|
||||
|
||||
class LostplacesAppConfig(AppConfig):
|
||||
name = 'lostplaces_app'
|
||||
name = 'lostplaces'
|
@@ -5,11 +5,12 @@
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
|
||||
from .models import Explorer, Place, PlaceImage, Voucher
|
||||
from django.contrib.auth.models import User
|
||||
from lostplaces.models import Place, PlaceImage, Voucher
|
||||
|
||||
class ExplorerCreationForm(UserCreationForm):
|
||||
class Meta:
|
||||
model = Explorer
|
||||
model = User
|
||||
fields = ('username', 'email')
|
||||
voucher = forms.CharField(
|
||||
max_length=30,
|
||||
@@ -18,19 +19,23 @@ class ExplorerCreationForm(UserCreationForm):
|
||||
|
||||
def is_valid(self):
|
||||
super().is_valid()
|
||||
sumitted_voucher = self.cleaned_data.get('voucher')
|
||||
submitted_voucher = self.cleaned_data.get('voucher')
|
||||
try:
|
||||
fetched_voucher = Voucher.objects.get(code=sumitted_voucher)
|
||||
fetched_voucher = Voucher.objects.get(code=submitted_voucher)
|
||||
except Voucher.DoesNotExist:
|
||||
self.add_error('voucher', 'Invalid voucher')
|
||||
return False
|
||||
|
||||
if not submitted_voucher.valid:
|
||||
self.add_error('voucher', 'Expired voucher')
|
||||
return False
|
||||
|
||||
fetched_voucher.delete()
|
||||
return True
|
||||
|
||||
class ExplorerChangeForm(UserChangeForm):
|
||||
class Meta:
|
||||
model = Explorer
|
||||
model = User
|
||||
fields = ('username', 'email')
|
||||
|
||||
class PlaceForm(forms.ModelForm):
|
||||
@@ -39,7 +44,7 @@ class PlaceForm(forms.ModelForm):
|
||||
fields = '__all__'
|
||||
exclude = ['submitted_by']
|
||||
|
||||
class PlaceImageCreateForm(forms.ModelForm):
|
||||
class PlaceImageForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = PlaceImage
|
||||
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 17:54
|
||||
|
||||
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),
|
||||
with Reserved Font Name "Crimson" and "Crimson Text".
|
||||
Copyright 2018 The Crimson Pro Project Authors (https://github.com/Fonthausen/CrimsonPro)
|
||||
|
||||
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:
|
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 |
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; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Crimson Text';
|
||||
font-family: 'Crimson Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
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-family: 'Crimson Text';
|
||||
font-family: 'Crimson Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
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-family: 'Crimson Text';
|
||||
font-family: 'Crimson Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
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-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
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-family: 'Montserrat';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
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-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
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 {
|
||||
height: 100%;
|
||||
@@ -583,7 +583,7 @@ body {
|
||||
|
||||
.LP-Paragraph {
|
||||
color: black;
|
||||
font-family: "Crimson Text", Times, serif;
|
||||
font-family: "Crimson Pro", Times, serif;
|
||||
font-size: 1.4rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -856,7 +856,7 @@ body {
|
||||
padding: 15px; } }
|
||||
|
||||
.LP-TextSection__Text {
|
||||
font-family: "Crimson Text", Times, serif;
|
||||
font-family: "Crimson Pro", Times, serif;
|
||||
font-size: 1.4rem; }
|
||||
.LP-TextSection__Text .LP-Link {
|
||||
margin: 0 3px; }
|
||||
@@ -1588,23 +1588,58 @@ body {
|
||||
border: none; }
|
||||
.LP-ImageGrid__Container {
|
||||
gap: 10px; }
|
||||
.LP-ImageGrid .LP-ImageGrid__Item, .LP-ImageGrid .LP-ImageGrid__Item * {
|
||||
overflow: hidden;
|
||||
word-break: break-all; }
|
||||
.LP-ImageGrid .LP-ImageGrid__Item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
.LP-ImageGrid .LP-ImageGrid__Item--left img {
|
||||
object-position: left; }
|
||||
.LP-ImageGrid .LP-ImageGrid__Item--center img {
|
||||
object-position: center; }
|
||||
.LP-ImageGrid .LP-ImageGrid__Item--top img {
|
||||
object-position: top; }
|
||||
.LP-ImageGrid .LP-ImageGrid__Item--bottom img {
|
||||
object-position: botom; }
|
||||
.LP-ImageGrid .LP-ImageGrid__Item--center img {
|
||||
object-position: center; }
|
||||
.LP-ImageGrid__Item {
|
||||
position: relative; }
|
||||
.LP-ImageGrid__Item, .LP-ImageGrid__Item * {
|
||||
overflow: hidden;
|
||||
word-break: break-all; }
|
||||
.LP-ImageGrid__Item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
.LP-ImageGrid__Item--left img {
|
||||
object-position: left; }
|
||||
.LP-ImageGrid__Item--center img {
|
||||
object-position: center; }
|
||||
.LP-ImageGrid__Item--top img {
|
||||
object-position: top; }
|
||||
.LP-ImageGrid__Item--bottom img {
|
||||
object-position: botom; }
|
||||
.LP-ImageGrid__Item--center img {
|
||||
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-MainContainer {
|
||||
margin: 0 auto;
|
||||
@@ -1642,7 +1677,7 @@ body {
|
||||
margin-bottom: 25px; } }
|
||||
|
||||
.LP-TextSection .LP-UnorderedList {
|
||||
font-family: "Crimson Text", Times, serif;
|
||||
font-family: "Crimson Pro", Times, serif;
|
||||
font-size: 1.4rem; }
|
||||
.LP-TextSection .LP-UnorderedList li {
|
||||
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
@@ -2,8 +2,8 @@
|
||||
|
||||
{% load static %}
|
||||
{% block additional_head %}
|
||||
<link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css">
|
||||
<script src="{% static 'maps/ol.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css">
|
||||
<script src="{% static 'maps/ol.js' %}"></script>
|
||||
{% endblock additional_head %}
|
||||
|
||||
# {% block title %}Start{% endblock %}
|
||||
@@ -14,7 +14,7 @@
|
||||
<article class="LP-TextSection">
|
||||
</article>
|
||||
|
||||
{% include 'partials/osm_map.html' %}
|
||||
{% include 'partials/osm_map.html' with config=mapping_config %}
|
||||
<div class="LP-PlaceGrid">
|
||||
<h1 class="LP-Headline LP-Headline">Explore the latest locations</h1>
|
||||
<ul class="LP-PlaceGrid__Grid">
|
||||
@@ -23,7 +23,7 @@
|
||||
<a href="{% url 'place_detail' pk=place.pk %}" class="LP-Link">
|
||||
<article class="LP-PlaceTeaser">
|
||||
<div class="LP-PlaceTeaser__Image">
|
||||
<img class="LP-Image" src="{{ place.images.first.filename.thumbnail.url}}" />
|
||||
<img class="LP-Image" src="{{ place.placeimages.first.filename.thumbnail.url}}" />
|
||||
</div>
|
||||
<div class="LP-PlaceTeaser__Meta">
|
||||
<div class="LP-PlaceTeaser__Info">
|
@@ -34,7 +34,7 @@
|
||||
<a href="{% url 'place_detail' pk=place.pk %}" class="LP-Link">
|
||||
<article class="LP-PlaceTeaser">
|
||||
<div class="LP-PlaceTeaser__Image">
|
||||
<img class="LP-Image" src="{{ place.images.first.filename.thumbnail.url}}" />
|
||||
<img class="LP-Image" src="{{ place.placeimages.first.filename.thumbnail.url}}" />
|
||||
</div>
|
||||
<div class="LP-PlaceTeaser__Meta">
|
||||
<div class="LP-PlaceTeaser__Info">
|
@@ -1,5 +1,5 @@
|
||||
|
||||
<div id="map" class="map" style="height: 300px"></div>
|
||||
<div tabindex="1" id="map" class="map" style="height: 300px"></div>
|
||||
<div id="info" class="map-popup"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
@@ -11,20 +11,20 @@
|
||||
}),
|
||||
],
|
||||
view: new ol.View({
|
||||
center: ol.proj.fromLonLat([{{place_map_center|last}}, {{place_map_center|first}}]),
|
||||
center: ol.proj.fromLonLat([{{config.map_center.longitude}}, {{config.map_center.latitude}}]),
|
||||
zoom: 9
|
||||
})
|
||||
});
|
||||
|
||||
var vectorSource = new ol.source.Vector({
|
||||
features: [
|
||||
{% for place in place_list %}
|
||||
{% for point in config.all_points %}
|
||||
new ol.Feature({
|
||||
geometry: new ol.geom.Point(
|
||||
ol.proj.fromLonLat([{{place.longitude}},{{place.latitude}}])
|
||||
ol.proj.fromLonLat([{{point.longitude}},{{point.latitude}}])
|
||||
),
|
||||
url: '{% url 'place_detail' pk=place.pk %}',
|
||||
name: '{{place.name}}'
|
||||
url: '{{point.get_absolute_url}}',
|
||||
name: ' {{point.name}}'
|
||||
}),
|
||||
{% endfor %}
|
||||
]
|
@@ -1,12 +1,12 @@
|
||||
<div class="LP-TagList">
|
||||
<ul class="LP-TagList__List">
|
||||
{% for tag in tag_list %}
|
||||
{% for tag in config.tagged_item.tags.all %}
|
||||
<li class="LP-TagList__Item">
|
||||
<div class="LP-Tag">
|
||||
<a href="#" class="LP-Link">
|
||||
<span class="LP-Link__Text">{{tag}}</span>
|
||||
</a>
|
||||
{% if request.user and request.user == config.tagged_item.submitted_by %}
|
||||
{% if request.user and request.user.explorer == config.tagged_item.submitted_by %}
|
||||
<a href="{% url config.delete_url_name tagged_id=config.tagged_item.id tag_id=tag.id %}" class="LP-Link">
|
||||
<span class="LP-Tag__Remove RV-Iconized__Container RV-Iconized__Container--extraSmall">
|
||||
<svg class="RV-Iconized__Icon" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -23,12 +23,12 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form id="id_tag_submit_form" class="LP-Form LP-Form--inline LP-Form--tagging" method="POST" action="{{config.submit_url}}">
|
||||
<form id="id_tag_submit_form" class="LP-Form LP-Form--inline LP-Form--tagging" method="POST" action="{% url config.submit_url_name tagged_id=config.tagged_item.id%}">
|
||||
<fieldset class="LP-Form__Fieldset">
|
||||
<legend class="LP-Form__Legend">Tags hinzufügen</legend>
|
||||
{% csrf_token %}
|
||||
<div class="LP-Form__Composition LP-Form__Composition--breakable">
|
||||
<div class="LP-Form__Field LP-Form__Button LP-Input LP-Input--tagging">
|
||||
<div class="LP-Form__Field LP-Form__Button LP-Input LP-Input--tagging">
|
||||
<button id="id_tag_submit_button" class="LP-Button"> Tags hinzufügen</button>
|
||||
</div>
|
||||
<div class="LP-Form__Field">
|
||||
@@ -47,10 +47,10 @@
|
||||
|
||||
const tagify = new Tagify(input, {
|
||||
'whitelist': [
|
||||
{% for tag in all_tags %}
|
||||
'{{tag}}',
|
||||
{% endfor %}
|
||||
]
|
||||
{% for tag in config.all_tags %}
|
||||
'{{tag}}',
|
||||
{% endfor %}
|
||||
]
|
||||
})
|
||||
|
||||
const on_form_submit = function() {
|
@@ -23,9 +23,9 @@
|
||||
|
||||
<header class="LP-PlaceDetail__Header">
|
||||
<h1 class="LP-Headline">{{ place.name }}</h1>
|
||||
{% if place.images.first.filename.hero.url %}
|
||||
{% if place.placeimages.first.filename.hero.url %}
|
||||
<figure class="LP-PlaceDetail__Image">
|
||||
<img src="{{ place.images.first.filename.hero.url }}" class="LP-Image" />
|
||||
<img src="{{ place.placeimages.first.filename.hero.url }}" class="LP-Image" />
|
||||
</figure>
|
||||
{% endif %}
|
||||
</header>
|
||||
@@ -37,13 +37,13 @@
|
||||
<section class="LP-Section">
|
||||
|
||||
{% url 'place_tag_submit' place_id=place.id as tag_submit_url%}
|
||||
{% include 'partials/tagging.html' with tag_list=place.tags.all config=tagging_config all_tags=all_tags %}
|
||||
{% include 'partials/tagging.html' with config=tagging_config %}
|
||||
|
||||
</section>
|
||||
|
||||
<section class="LP-Section">
|
||||
<h1 class="LP-Headline">Map-Links</h1>
|
||||
{% include 'partials/osm_map.html' %}
|
||||
<h1 class="LP-Headline">Map links</h1>
|
||||
{% include 'partials/osm_map.html' with config=mapping_config%}
|
||||
<div class="LP-LinkList">
|
||||
<ul class="LP-LinkList__Container">
|
||||
<li class="LP-LinkList__Item"><a target="_blank" href="https://www.google.com/maps?q={{place.latitude}},{{place.longitude}}" class="LP-Link"><span class="LP-Text">Google Maps</span></a></li>
|
||||
@@ -54,15 +54,15 @@
|
||||
</section>
|
||||
|
||||
<section class=" LP-Section">
|
||||
<h1 class="LP-Headline">Photoalben</h1>
|
||||
<h1 class="LP-Headline">Photo albums</h1>
|
||||
<div class="LP-LinkList">
|
||||
<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">
|
||||
<a target="_blank" href="{{photo_album.url}}" class="LP-Link">
|
||||
<span class="LP-Text">{{photo_album.label}}</span>
|
||||
</a>
|
||||
{% if user == photo_album.submitted_by or user == place.submitted_by %}
|
||||
{% if user.explorer == photo_album.submitted_by or user.explorer == place.submitted_by %}
|
||||
<a href="{% url 'photo_album_delete' pk=photo_album.pk%}" class="LP-Link LP-LinkList__ItemHover" title="Delete Photo Album">
|
||||
<div class="RV-Iconized__Container RV-Iconized__Container--small">
|
||||
{% icon 'trash' className="RV-Iconized__Icon" %}
|
||||
@@ -89,14 +89,28 @@
|
||||
</section>
|
||||
|
||||
<section class="LP-Section">
|
||||
<h1 class="LP-Headline">Bilder</h1>
|
||||
<h1 class="LP-Headline">Images</h1>
|
||||
<div class="LP-ImageGrid">
|
||||
<ul class="LP-ImageGrid__Container">
|
||||
{% for place_image in place.images.all %}
|
||||
{% for place_image in place.placeimages.all %}
|
||||
<li class="LP-ImageGrid__Item">
|
||||
<a href="{{ place_image.filename.large.url }}" class="LP-Link"><img class="LP-Image" src="{{ place_image.filename.thumbnail.url }}"></a>
|
||||
<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>
|
||||
{% 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>
|
||||
</div>
|
||||
</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 %}
|
@@ -0,0 +1,28 @@
|
||||
{% 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">
|
||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
||||
<button class="LP-Button">Submitt</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>
|
||||
</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
|
||||
|
||||
register = template.Library()
|
@@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json, os
|
||||
from importlib import import_module
|
||||
|
||||
@@ -6,7 +9,7 @@ from django.conf import settings
|
||||
from django.template import Library, TemplateSyntaxError
|
||||
|
||||
#icons_json_path = getattr(settings, 'SVG_ICONS_SOURCE_FILE')
|
||||
icons_json_path = os.path.join(settings.BASE_DIR, 'lostplaces_app', 'static', 'icons', 'icons.icomoon.json')
|
||||
icons_json_path = os.path.join(settings.BASE_DIR, 'lostplaces', 'static', 'icons', 'icons.icomoon.json')
|
||||
icons_json = json.load(open(icons_json_path))
|
||||
|
||||
register = Library()
|
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'
|
||||
)
|
126
django_lostplaces/lostplaces/tests/models/test_place_model.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/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 django.utils import timezone
|
||||
|
||||
from lostplaces.models import Place
|
||||
from lostplaces.tests.models import ModelTestCase
|
||||
|
||||
class PlaceTestCase(ModelTestCase):
|
||||
model = Place
|
||||
related_name = 'places'
|
||||
nullable = True
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
|
||||
place = Place.objects.create(
|
||||
name='Im a place',
|
||||
submitted_when=timezone.now(),
|
||||
submitted_by=user.explorer,
|
||||
location='Testtown',
|
||||
latitude=50.5,
|
||||
longitude=7.0,
|
||||
description='This is just a test, do not worry'
|
||||
)
|
||||
place.tags.add('I a tag', 'testlocation')
|
||||
place.save()
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.place = Place.objects.get(id=1)
|
||||
|
||||
def test_location(self):
|
||||
self.assertCharField(
|
||||
field_name='location',
|
||||
min_length=10,
|
||||
max_length=100
|
||||
)
|
||||
|
||||
def test_description(self):
|
||||
self.assertField('description', models.TextField)
|
||||
|
||||
def test_average_latlon(self):
|
||||
'''
|
||||
Tests the average latitude/longitude calculation of a list
|
||||
of 10 places
|
||||
'''
|
||||
place_list = []
|
||||
for i in range(10):
|
||||
place = Place.objects.get(id=1)
|
||||
place.id = None
|
||||
place.latitude = i+1
|
||||
place.longitude = i+10
|
||||
place.save()
|
||||
place_list.append(place)
|
||||
|
||||
avg_latlon = Place.average_latlon(place_list)
|
||||
|
||||
self.assertTrue('latitude' in avg_latlon,
|
||||
msg='Expecting avg_latlon dict to have an \'latitude\' key'
|
||||
)
|
||||
self.assertTrue('longitude' in avg_latlon,
|
||||
msg='Expecting avg_latlon dict to have an \'longitude\' key'
|
||||
)
|
||||
|
||||
self.assertEqual(avg_latlon['latitude'], 5.5,
|
||||
msg='%s: average latitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
self.assertEqual(avg_latlon['longitude'], 14.5,
|
||||
msg='%s: average longitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def test_average_latlon_one_place(self):
|
||||
'''
|
||||
Tests the average latitude/longitude calculation of a list
|
||||
of one place
|
||||
'''
|
||||
place = Place.objects.get(id=1)
|
||||
avg_latlon = Place.average_latlon([place])
|
||||
self.assertEqual(avg_latlon['latitude'], place.latitude,
|
||||
msg='%s:(one place) average latitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
self.assertEqual(avg_latlon['longitude'], place.longitude,
|
||||
msg='%s: (one place) average longitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def test_average_latlon_no_places(self):
|
||||
'''
|
||||
Tests the average latitude/longitude calculation of
|
||||
an empty list
|
||||
'''
|
||||
avg_latlon = Place.average_latlon([])
|
||||
self.assertEqual(avg_latlon['latitude'], 0,
|
||||
msg='%s: (no places) average latitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
self.assertEqual(avg_latlon['longitude'], 0,
|
||||
msg='%s: (no places) average longitude missmatch' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def test_str(self):
|
||||
place = self.place
|
||||
self.assertTrue(place.name.lower() in str(place).lower(),
|
||||
msg='Expecting %s.__str__ to contain the name' % (
|
||||
self.model.__name__
|
||||
)
|
||||
)
|