Compare commits

...

132 Commits

Author SHA1 Message Date
a2004bd789 #24 Ordering of places in list 2020-09-29 21:41:36 +02:00
b3b8ee3104 Styling fixes 2020-09-29 20:40:41 +02:00
9613f553e9 #19 OSM-Map Styling 2020-09-29 20:21:27 +02:00
1a8a7cc4f0 Merge branch 'feature/submit_cancel_partial' into develop 2020-09-28 22:14:42 +02:00
702ebd9137 Implanted submit/cancel partial into templates. 2020-09-28 22:12:39 +02:00
aa3922da50 Added custom button text variable. 2020-09-28 22:10:07 +02:00
d6c609d107 Added submit/cancel partial. Cancel redirects to referror or home. 2020-09-28 21:54:09 +02:00
61566714a3 Merge branch 'develop' of mowoe.com:reverend/lostplaces-backend into develop 2020-09-28 21:00:20 +02:00
f4ae5c9f77 Merge branch 'temp/migration' into develop 2020-09-28 21:00:06 +02:00
cfe3d222e6 Added a few last tags to DB. 2020-09-28 20:59:10 +02:00
e0d6d81907 Adapted testdata doc. 2020-09-28 20:51:41 +02:00
5e8e865a99 New testdata. Changed geo coords. 2020-09-28 20:48:51 +02:00
3061540396 Fuck me hard, git. 2020-09-28 20:40:09 +02:00
bb119e8636 Fresh and clean initial migration. 2020-09-28 20:30:41 +02:00
14a98deff3 delete testdata. 2020-09-28 20:09:17 +02:00
4919c90ae8 Completely new and fresh initial migration. 2020-09-28 20:05:30 +02:00
5c47587189 Pruned ALL THE SHIT. 2020-09-28 20:05:01 +02:00
3223637471 dirty-save 2020-09-28 19:21:09 +02:00
389e3eac43 Typos 2020-09-27 23:58:25 +02:00
302e8bb994 More Documentation 2020-09-27 23:56:45 +02:00
992f8a7b37 Moved Delete and Alter after datamigration. 2020-09-27 21:01:11 +02:00
c8c3852323 Added basic (empty) 3-way migration. 2020-09-27 20:34:39 +02:00
8d9ab8b088 Add migrations back into repo - preparation for release. 2020-09-27 20:33:56 +02:00
45185b9236 Recent css 2020-09-27 17:13:15 +02:00
5f33ec5824 Deletion of place images 2020-09-26 16:08:56 +02:00
e217d3254e Small Teaak 2020-09-26 16:08:47 +02:00
263416a401 Abstraction for PlaceImageCreation (multiple images) 2020-09-26 16:08:37 +02:00
b4bd86ed44 View for submitting additionale place images 2020-09-26 09:48:06 +02:00
277cac323e CSS for adding and deleting images 2020-09-26 09:47:34 +02:00
5f304b91f3 Squashed commit of the following:
commit 0d62e72d72922a84e41c9f2cc21977b794784d1c
Merge: 79fee63 85f2a81
Author: reverend <reverend@reverend2048.de>
Date:   Tue Sep 22 21:55:18 2020 +0200

    Merge branch 'develop' into refactor/models

commit 79fee631d7ac28509067ecdd74078f1a2f6e0be2
Author: reverend <reverend@reverend2048.de>
Date:   Tue Sep 22 21:54:32 2020 +0200

    Updating references for related name

commit 8e07e79df2de2601f2e2eadfdd37eb7c719c51b0
Author: reverend <reverend@reverend2048.de>
Date:   Tue Sep 22 21:53:31 2020 +0200

    Generating of related names fix

commit 5fd804f37a805ae4707e13c3d941bdde3660afea
Merge: 8cc1d3e 3b526c9
Author: reverend <reverend@reverend2048.de>
Date:   Tue Sep 22 21:01:48 2020 +0200

    Merge branch 'develop' into refactor/models

commit 8cc1d3e690211dba6451e86569f00078b23e0621
Author: reverend <reverend@reverend2048.de>
Date:   Tue Sep 22 20:21:08 2020 +0200

    Tests

commit 7c0591e5397f892b1f6fb80725a693c21f90468a
Author: reverend <reverend@reverend2048.de>
Date:   Fri Sep 18 23:53:39 2020 +0200

    Testing PlaceAsset

commit 2e7b49ad1a15173565c81e7eb8bb3f35b9f622a6
Author: reverend <reverend@reverend2048.de>
Date:   Fri Sep 18 22:25:08 2020 +0200

    Restructuring models

commit eb7d03b08b326f9115e70d0fd9ed5d0fc229a362
Author: reverend <reverend@reverend2048.de>
Date:   Fri Sep 18 22:01:54 2020 +0200

    Abstract class Expireable

commit 2b51e741bb5734c5a578beeadef7819fe58b2223
Author: reverend <reverend@reverend2048.de>
Date:   Fri Sep 18 21:54:07 2020 +0200

    Abstract Model for PlaceAsset (i.e. Photoalbums)
2020-09-22 21:56:51 +02:00
85f2a81ebb Merge branch 'develop' of mowoe.com:reverend/lostplaces-backend into develop 2020-09-22 20:49:00 +02:00
8de23a3f3b Test if linked place matches. 2020-09-22 20:48:56 +02:00
3b526c9c6f Merge branch 'develop' of mowoe.com:reverend/lostplaces-backend into develop 2020-09-22 20:10:06 +02:00
516e7a04ad More documentation 2020-09-22 20:10:03 +02:00
84f13ced80 Obsoleted file with info now in Readme.md 2020-09-22 19:43:32 +02:00
dcd92f7d2a Imported from frontend with changed file / fontnames. 2020-09-22 19:41:28 +02:00
2a944eeefe Updated fonts. 2020-09-22 19:41:04 +02:00
dc93539e23 Added a list of included ressources. 2020-09-22 19:40:48 +02:00
dd58c76aba Fix #13 by allowing map zoom only on focus. 2020-09-22 18:13:42 +02:00
106ef6dac8 Updated OpenLayers to 6.4.3 - incl. source hint. 2020-09-22 13:07:30 +02:00
2bc99c78c2 Translated headings to English. 2020-09-22 12:51:08 +02:00
0185c13a95 New testdata dump with 11 places. 2020-09-21 21:37:45 +02:00
9632040ade Typos and missing docstrings. 2020-09-21 21:37:28 +02:00
85a34b20f9 Added additional tests for album links. 2020-09-20 02:19:05 +02:00
e25f3da5a5 Fixed syntax error, that broke pipenv (update) 2020-09-19 23:31:25 +02:00
4c9234d07a Text formatting. 2020-09-19 23:29:39 +02:00
f5853f7777 Added python docstrings. 2020-09-19 22:50:07 +02:00
eb7fe88d59 Renamed PlaceImageCreateForm to PlaceImageForm. 2020-09-18 23:50:25 +02:00
2a3ee3de8d #9 Using morge 404 2020-09-18 23:32:52 +02:00
00ab65d648 Added working tests. 2020-09-18 23:02:59 +02:00
79854f3fa1 Basic fist tests for PhotoAlbum model. 2020-09-18 22:39:18 +02:00
618a152f23 Switched to timezone-aware now() function. 2020-09-18 22:04:19 +02:00
ce91a19068 Merge commit '17b531e181e691055797ec95a030c399f4dbcb8f' into develop 2020-09-18 21:26:55 +02:00
17b531e181 Removed __str__ repr of Voucher and PlaceImage. 2020-09-18 21:26:05 +02:00
222c97d63c Added voucher validity boolean icon display. 2020-09-18 21:19:31 +02:00
6a1229a4ba Added valid column to VouchersAdmin. 2020-09-18 21:03:05 +02:00
2bff4db1d7 Added Voucher expiration check in form using model property. 2020-09-18 21:02:45 +02:00
a3b123a207 Added valid property to VoucherModel. 2020-09-18 21:02:07 +02:00
e698f3b224 Added VouchersAdmin and PlaceImagesAdmin. 2020-09-18 20:40:58 +02:00
c66baaa765 Simplified PlaceImage __str__ repr. 2020-09-18 20:37:54 +02:00
815be3126e Issue #16 Tag Suggestion 2020-09-18 20:35:01 +02:00
715e953182 Changed __str__ repr of Vouchers. 2020-09-18 20:30:25 +02:00
86915f6ec5 Added list display of Places and PhotoAlbums admin pages. 2020-09-18 20:21:30 +02:00
30b42de86f Delete PlaceImage files including thumbnails on deletion. 2020-09-18 13:40:54 +02:00
21217b067f Add comments and relocate thumbnails, set quality. 2020-09-18 10:47:41 +02:00
9b160d2df0 Sharpen thumbnails. 2020-09-18 10:47:16 +02:00
74a9ee4f39 Resize original images, name files with placename in filename, sharpen. 2020-09-18 10:47:00 +02:00
b0e775d299 Removed session data from testdata dump. 2020-09-17 22:21:25 +02:00
d6b82f1556 Fixed missing "latest locations" on Home when logged in. 2020-09-17 22:02:37 +02:00
3f642daf89 Added first sample testdata + doc doc hints. 2020-09-17 21:27:19 +02:00
858cfe1d3c minor settings 2020-09-17 17:06:44 +02:00
943d647a47 Added basic setup.cfg with source dir for coverage. 2020-09-17 16:14:47 +02:00
d96cf9470a Developer Documentatino for models 2020-09-15 21:07:53 +02:00
c8b3cff5a6 Updated Readme according to change of name and user model. 2020-09-14 17:37:21 +02:00
547179b0ca Squashed commit of the following:
commit 97b044cafb7f17f23b3b1beedcf70af209a60ddc
Author: reverend <reverend@reverend2048.de>
Date:   Mon Sep 14 17:25:40 2020 +0200

    Updating gitignore

commit 4891d80486e1f95db8ae66385c7c97426a3ca1a4
Author: reverend <reverend@reverend2048.de>
Date:   Mon Sep 14 17:25:20 2020 +0200

    Updating Readme

commit f05c43abbdc7eb30896ad6d10fe80fd6483338d9
Author: reverend <reverend@reverend2048.de>
Date:   Mon Sep 14 17:23:30 2020 +0200

    Renaming Module

commit fd5ad2ee9f8cbacd565da45b257928192ffc651c
Author: reverend <reverend@reverend2048.de>
Date:   Mon Sep 14 17:23:16 2020 +0200

    Renaming module references

commit 828a0dd5dd73723b84b77908497903ed26b6966b
Author: reverend <reverend@reverend2048.de>
Date:   Mon Sep 14 17:21:20 2020 +0200

    Renaming Project
2020-09-14 17:26:17 +02:00
0765f6606f Pipenv scripts and not pinning python version 2020-09-14 15:29:26 +02:00
12881d9345 Renaming MapablePoint 2020-09-14 15:18:21 +02:00
79ed029db0 Rephrasing 2020-09-14 15:16:31 +02:00
1a8da002cf Formatting 2020-09-13 20:39:32 +02:00
c828d04f05 Mixins for testing abstract model/partials in views 2020-09-13 20:27:05 +02:00
f919fe30fa Renaming map config 2020-09-13 20:15:49 +02:00
b3db6643b9 Refactoring 2020-09-13 19:43:47 +02:00
cea3a909b5 Using abstract modls 2020-09-13 19:29:30 +02:00
b52d96a55e More and better test failure messages 2020-09-13 19:17:04 +02:00
1fb71a172e Refactoring 2020-09-13 19:12:32 +02:00
27520c7ca4 Refactored ModelTestCaseMixin 2020-09-13 18:37:21 +02:00
af14cce3f8 Minor code restructuring 2020-09-13 15:49:15 +02:00
75bcc91037 Refactoring places images related name 2020-09-13 14:39:21 +02:00
c78ff60231 Refactoring ModelTestCase 2020-09-13 13:31:41 +02:00
09eb8794b8 Refactoring test_float_field 2020-09-13 13:30:47 +02:00
470e54da8d Refactoring test_char_field 2020-09-13 13:30:11 +02:00
19299598c3 Refactoring test_field 2020-09-13 13:29:27 +02:00
f1c51ab8a7 Testing messages 2020-09-13 13:28:22 +02:00
b77c5d1d7f Comments and refactoring viewtestmixin 2020-09-13 12:49:26 +02:00
05481fc0c8 Refactoring _has_context_key 2020-09-13 12:41:31 +02:00
6b00452830 Refactoring 2020-09-13 12:39:46 +02:00
7e4c5dcf24 Adding more methods to viewtestcase 2020-09-13 12:39:02 +02:00
c0f30e56f7 Small tweak 2020-09-13 11:02:53 +02:00
9852646fff Renaming IsPlaceSubmitter 2020-09-13 10:57:53 +02:00
c2d678847e Renaming IsAuthenticated 2020-09-13 10:56:18 +02:00
e77edf18ac Testing Abstract classes 2020-09-13 10:27:01 +02:00
3780aa6cf1 Abtract classes 2020-09-12 12:24:27 +02:00
21124ec2ad adapting list views 2020-09-12 12:02:25 +02:00
0ee5fc59d3 Adapting tests 2020-09-12 12:02:17 +02:00
b8dfef691e Fixing bug; Place did not show up on map 2020-09-12 11:58:45 +02:00
a1886b0b60 Adapting home view 2020-09-12 11:47:42 +02:00
e1002b5315 Adapting place detail view 2020-09-12 11:42:31 +02:00
fed90d4f7b Refactoring osm map 2020-09-12 11:42:18 +02:00
dcfb329c5a Adapted template 2020-09-12 11:35:30 +02:00
b8a21a8baa Changed average_latlon 2020-09-12 11:34:49 +02:00
7f73035b02 Refactoring tagging 2020-09-12 11:24:16 +02:00
317437fedc Testing PlaceListView 2020-09-12 11:02:39 +02:00
26286984c2 Base functions for testing views 2020-09-12 11:02:23 +02:00
9ae31c0146 Removing obsolote code 2020-09-12 11:02:01 +02:00
87fd8fa96f Small fix 2020-09-12 11:01:34 +02:00
c3401e732f Removed obosolete code 2020-09-12 11:01:24 +02:00
4ee7373b3f Testing Placeimage file change 2020-09-12 08:48:53 +02:00
64c0c5f8e6 Setting Up Test Data mor in a Unit way 2020-09-12 08:39:06 +02:00
18a597c726 Merge branch 'develop' into testing 2020-09-12 08:38:37 +02:00
Leonhard Strohmidel
baca596603 sync 2020-09-11 23:07:19 +02:00
d993387216 Fixed text repr of Explorer model due to user model change. 2020-09-11 22:43:03 +02:00
aed2856df3 Made the test timezone aware, DateTimeFiled(auto_now_add) already was. 2020-09-11 22:22:03 +02:00
c78858c152 Changed class attibutes to match test expectation. 2020-09-11 22:03:20 +02:00
f49581259e Typo in class name. 2020-09-11 19:32:07 +02:00
f5bf642cd6 Ups, not for the settings file. 2020-09-11 19:23:13 +02:00
7687acb366 Changed scope (?) of remaining local imports. 2020-09-11 19:15:39 +02:00
e655e1598a Trying to test deletion, wip 2020-09-11 12:09:51 +02:00
64ed38332f Refactoring and testing __str__ 2020-09-11 12:09:34 +02:00
d438303aec file for model tests 2020-09-11 12:08:43 +02:00
38b3736951 New Model tests 2020-09-11 12:08:27 +02:00
6be060ea40 Refactoring and more keywords to test 2020-09-11 12:08:15 +02:00
5c5756150f More dev dep 2020-09-11 12:07:57 +02:00
140 changed files with 11506 additions and 961 deletions

6
.gitignore vendored
View File

@ -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

18
LICENSE
View File

@ -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.

View File

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

12
Pipfile
View File

@ -10,7 +10,8 @@ autopep8 = "*"
pipenv = "*"
wheel = "*"
twine = "*"
pandoc ="*"
pandoc = "*"
pylint-django = "*"
[packages]
django = "*"
@ -18,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"

View File

@ -2,14 +2,14 @@
lostplaces-backend is a django (3.x) based webproject. It once wants to become a software which allows a group of urban explorers to manage, document and share the locations of lost places while not exposing too much / any information to the public.
The software is currently in early development status, neither scope, datalmodel(s) nor features are finalized yet. Therefore we would not recommend to download or install this piece of software anywhere - except your local django dev server.
The software is currently in early development status, neither scope, datamodel(s) nor features are finalized yet. Therefore we would not recommend to download or install this piece of software anywhere - except your local django dev server.
We value privacy as a whole, all ressources the frontend requires will be shipped with lostplace's distribution. We also try to minimze the use of JavaScript as far as we can and try to offer JS-less alternatives where we can.
We value privacy as a whole, all resources the frontend requires will be shipped with lostplace's distribution. We also try to minimize the use of JavaScript as far as we can and try to offer JS-less alternatives where we can.
## Features
- Manage lost places with lots of usefull information.
- Manage lost places with lots of useful information.
- OSM-Maps
- Sensitive information is not accesiable for anonymous (not logged in) users.
- Sensitive information is not accessible for anonymous (not logged in) users.
- User self registration using a voucher system, only people you invite can join your instance.
- Collaboration, every user can add informations like tags, photos and external links to your place.
@ -21,6 +21,13 @@ Right now it depends on the following non-core Python 3 libraries. These can be
* [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
@ -34,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
@ -45,9 +52,9 @@ $ pipenv shell
$ cd lostplaces-backend
$ pipenv shell
(lostplaces-backend) $ pipenv update # If dependencies changed, or updates available
(lostplaces-backend) $ lostplaces/manage.py makemigrations # If datamodels changed
(lostplaces-backend) $ lostplaces/manage.py migrate # If datamodels changed
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
(lostplaces-backend) $ django_lostplaces/manage.py makemigrations # If datamodels changed
(lostplaces-backend) $ django_lostplaces/manage.py migrate # If datamodels changed
(lostplaces-backend) $ django_lostplaces/manage.py runserver --ipv6
```
Visit: [admin](http://localhost:8000/admin) for administrative backend or
@ -63,25 +70,25 @@ Currently there are two ways to deploy the lostplaces project:
## Cloning the repository
Essentially, this is the same as installing a development instance, but without the development server (manage.py runserver) and something powerfull (Apache, NGINX) instead. You have to configure the webserve to work with the *SGI Api respectivly, reference [django's guide for deployment](https://docs.djangoproject.com/en/3.1/howto/deployment/) for further information.
Essentially, this is the same as installing a development instance, but without the development server (manage.py runserver) and something powerful (Apache, NGINX) instead. You have to configure the webserver to work with the *SGI Api respectively, reference [django's guide for deployment](https://docs.djangoproject.com/en/3.1/howto/deployment/) for further information.
You also should setup a dedicated database server, the built-in SQLite file is not recommened for production use. Reference [django's guide for databases](https://docs.djangoproject.com/en/3.1/ref/databases/) for further information.
You also should setup a dedicated database server, the built-in SQLite file is not recommended for production use. Reference [django's guide for databases](https://docs.djangoproject.com/en/3.1/ref/databases/) for further information.
Before making the django instance public, you should tweak the config `settings.py`:
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64)).
2. Turn off debug mode by setting `DEBUG = False`.
3. Tune the localization settings, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/).
Run `lostplaces/managy.py collectstatic` and you should be ready to go.
Run `django_lostplaces/managy.py collectstatic` and you should be ready to go.
## Installing the lostplaces_app to an existing django instance
## Installing lostplaces to an existing django instance
### Installing django and the lostplaces app
### Installing django and the django_lostplaces app
If you haven't already setup a django instance, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/install/).
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.*
@ -93,7 +100,7 @@ Now configure your `settings.py` as follows:
```python
INSTALLED_APPS = [
...
'lostplaces_app',
'django_lostplaces',
'easy_thumbnails',
'widget_tweaks',
'django_taggit'
@ -110,28 +117,22 @@ 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.
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.
```
@ -139,5 +140,7 @@ Before making the django instance public, you should tweak the config `settings.
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64)).
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.
Run `django_lostplaces/manage.py collectstatic` you should be ready to go.

View File

@ -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()

View File

@ -38,7 +38,7 @@ ALLOWED_HOSTS = ['localhost']
# Application definition
INSTALLED_APPS = [
'lostplaces_app',
'lostplaces',
'easy_thumbnails',
'widget_tweaks',
'taggit',
@ -60,7 +60,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'lostplaces.urls'
ROOT_URLCONF = 'django_lostplaces.urls'
TEMPLATES = [
{
@ -78,7 +78,7 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = 'lostplaces.wsgi.application'
WSGI_APPLICATION = 'django_lostplaces.wsgi.application'
# Database
@ -131,9 +131,15 @@ USE_TZ = True
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
# Upload directory
MEDIA_URL = '/uploads/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
# Thumbnails
THUMBNAIL_MEDIA_ROOT = os.path.join(MEDIA_ROOT, 'thumbs/')
THUMBNAIL_MEDIA_URL = os.path.join(MEDIA_URL, 'thumbs/')
THUMBNAIL_QUALITY = 75
# Templates to use for authentication
LOGIN_URL = reverse_lazy('login')
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')

View File

@ -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)

View File

@ -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()

View File

@ -0,0 +1,155 @@
# django lostplaces documentation for developer
Greetings,
this documentation is for anyone who wants to work with this projects codebase or is just curious about how it works.
## Models
The models contain an custom user profile, some abstract base models and the models holding the actual data for this project
### Explorer user profile
Location: `lostplaces.models.models.Explorer`
Import from: `lostplaces.models.Explorer`
#### Super Classes
- django's `models.Model`
The class `lostplaces.models.Explorer` is our custom user profile. It has an ForeignKey pointing at django's default user model instead of providing an entire custom user model. That way this django app does not conflict with any other app that brings it's own user model.
You can access the explorer profile by accessing the 'explorer' attribute of any user instance
```python
user.explorer
```
Currently the explorer profile is used by the abstract model 'Submittable' and has the following realated names/fiels:
- [places](###place) A list containing all (lost) places the user has submitted
- [placeimages](###placeimages) A list containing all images relating a place that a user has submitted
- [photoalbums](###photoalbums) A list of all photo albums a explorere has submitted
### Taggable
Location: `lostplaces.models.abstract_models.Taggable`
Import from: `lostplaces.models.Taggable`
#### Super Classes
- django's `models.Model`
The abstract model Taggable represents an model that is taggable. It depends on the django app [taggit](https://github.com/jazzband/django-taggit). It only consists of one field:
- `tag`: TaggableManager, allows the sub class to be tagged, blank=True allows the admin form to be submitted without any tags
### Mapable
Location: `lostplaces.models.abstract_models.Mapable`
Import from: `lostplaces.models.Mapable`
#### Super Classes
- django's `models.Model`
The abstract model Mapable represents an model that can be displayed on a map. It consists of tree attributes
`name`
Name of the object, displayed on the map, max length 50 characters
`latitude`
Latitude of the referenced location, -90 <= value <= 90
`longitude`
Longitude of the referenced location -180 <= value <= 180
A mapable model has to provide its own get_absolute_url, in order to provide a link when clicked.
### Submittable
Location: `lostplaces.models.abstract_models.Submittable`
Import from: `lostplaces.models.Submittable`
#### Super Classes
- django's `models.Model`
The abstract model Submittable represents an model that can be submitted by an user. It knows who submitted something and when:
`submitted_by`
Referencing the explorer profile, see [Explorer](##explorer-user-profile). If the explorer profile is deleted, this instance is kept (on_delete=models.SET_NULL). The related_name is set to the class name, lower case appending an s (%(class)ss)
`submitted_when`
When the object was submitted, automatically set by django (auto_now_add=True)
### Expireable
Location: `lostplaces.models.abstract_models.Expireable`
Import from: `lostplaces.models.Expireable`
#### Super Classes
- django's `models.Model`
This abstract model represents an object that can expire. It is currently used by the Voucher model. I consists of two fields:
`created_when`
The date the object was created, automatically set by django (auto_now_add=True)
`expires_when`
The date the object expires.
### Voucher
Locatoin: `lostplaces.models.Voucher`
Import from: `lostplaces.models.Voucher`
#### Super Classes
- django's `models.Model`
- [lostplaces.models.Expireable](###expireable)
A voucher code is needed to sign up using lostplaces sign up form. The model contains
`code`
The voucher code, max length 30 characters
`created_when`
When the voucher was created automatically set by django (auto_now_add=True)
`expires_when`
Till what date the voucher remains valid
### Place
Location: `lostplaces.models.place.Place`
Import from: `lostplaces.models.Place`
#### Super Classes
- django's `models.Model`
- [lostplaces.models.Submittable](###submittable)-
- [lostplaces.models.Taggable](###taggable)
- [lostlaces.models.Mapable]
The place model is the heart of this project. It stores all information about a place needed.
`location`
Human readable location description (town, village, street), max length 50 characters
`description`
Describing the place in detail
The place model uses these abstract super classes
- Submittable, see [Submittable](##submittable) for additional fields
- Taggable, see [Taggable](##taggable) for additional fields
- Mapable, see [Mapable](##mapable) for additional fields
The average_latlon function takes a list of places and returns the center point of all these places.
### PlaceAsset
Location: `lostplaces.models.PlaceAsset`
Import from: `lostplaces.models.place.PlaceAsset`
#### Super Classes
- django's `models.Model`
- [lostplaces.models.Submittable](###submittable)
A PlaceAsset is anything that belongs to a place. The PlaceImage model for exmpaple is a subclass of PlaceAsset. A PlaceAsset is supposed to be submittable by every user. Only the user who has submitted the asset, the place the asset belongs to or both can delete a asset.
An PlaceAsset has the following fields
`place`
The Place this PlaceAsset belongs to
The PlaceAsset model is a subclass of Submittable, see [Submittable](###submittable) for more details.
### External Link
Loacation: `lostplaces.models.external_link.ExternalLink`
Import From: `lostplaces.models.ExternalLink`
#### Super Classes
- django's `models.Model`
- [lostplaces.models.Submittable](###submittable)
- [lostplaces.models.PlaceAsset](###placeasset)
This model represents an URL to an external website. External Link is an PlaceAsset, see [above](###placeasset) for more details. External Link has to fields
`url`
the URL to the target
`label`
the label that is shown on the website
### PhotoAlbum
Loacation: `lostplaces.models.external_link.PhototAlbum`
Import From: `lostplaces.models.PhotoAlbum`
#### Super Classes
- django's `models.Model`
- [lostplaces.models.Submittable](###submittable)
- [lostplaces.models.PlaceAsset](###placeasset)
- [lostplaces.models.ExternalLink](###externallink)
A photo album is a link to an external site that is meant to contain photos of the place it is referenced in. It
does not have any fields, just the ones inherited from it's super class [ExternalLink](###externallink).

View 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},
},
}

View 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)

View File

@ -1,4 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django.apps import AppConfig
class LostplacesAppConfig(AppConfig):
name = 'lostplaces_app'
name = 'lostplaces'

View File

@ -6,7 +6,7 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.contrib.auth.models import User
from lostplaces_app.models import Place, PlaceImage, Voucher
from lostplaces.models import Place, PlaceImage, Voucher
class ExplorerCreationForm(UserCreationForm):
class Meta:
@ -19,13 +19,17 @@ 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
@ -40,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']

View File

@ -0,0 +1,90 @@
# Generated by Django 3.1.1 on 2020-09-28 18:39
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import easy_thumbnails.fields
import lostplaces.models.place
import taggit.managers
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('taggit', '0003_taggeditem_add_unique_index'),
]
operations = [
migrations.CreateModel(
name='Expireable',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_when', models.DateTimeField(auto_now_add=True)),
('expires_when', models.DateTimeField()),
],
),
migrations.CreateModel(
name='Explorer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='explorer', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Place',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('latitude', models.FloatField(validators=[django.core.validators.MinValueValidator(-90), django.core.validators.MaxValueValidator(90)])),
('longitude', models.FloatField(validators=[django.core.validators.MinValueValidator(-180), django.core.validators.MaxValueValidator(180)])),
('submitted_when', models.DateTimeField(auto_now_add=True, null=True)),
('location', models.CharField(max_length=50)),
('description', models.TextField()),
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='places', to='lostplaces.explorer')),
('tags', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Voucher',
fields=[
('expireable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='lostplaces.expireable')),
('code', models.CharField(max_length=30, unique=True)),
],
bases=('lostplaces.expireable',),
),
migrations.CreateModel(
name='PlaceImage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('submitted_when', models.DateTimeField(auto_now_add=True, null=True)),
('description', models.TextField(blank=True)),
('filename', easy_thumbnails.fields.ThumbnailerImageField(upload_to=lostplaces.models.place.generate_image_upload_path)),
('place', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='placeimages', to='lostplaces.place')),
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='placeimages', to='lostplaces.explorer')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PhotoAlbum',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('submitted_when', models.DateTimeField(auto_now_add=True, null=True)),
('url', models.URLField()),
('label', models.CharField(max_length=100)),
('place', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='photoalbums', to='lostplaces.place')),
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='photoalbums', to='lostplaces.explorer')),
],
options={
'abstract': False,
},
),
]

View 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 *

View 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()

View 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

View 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)

View 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)

View File

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB

View File

@ -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:

View 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.

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 816 B

After

Width:  |  Height:  |  Size: 816 B

View File

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 351 B

View File

Before

Width:  |  Height:  |  Size: 641 B

After

Width:  |  Height:  |  Size: 641 B

View File

Before

Width:  |  Height:  |  Size: 850 B

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 914 B

After

Width:  |  Height:  |  Size: 914 B

View File

Before

Width:  |  Height:  |  Size: 488 B

After

Width:  |  Height:  |  Size: 488 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 717 B

After

Width:  |  Height:  |  Size: 717 B

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -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,78 @@ 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-Map {
margin-bottom: 25px; }
.LP-Map .ol-attribution {
font-family: "Montserrat", Helvetica, sans-serif;
color: #565656; }
.LP-Map .ol-attribution a {
color: #C09F80; }
.LP-Map .ol-attribution a:hover, .LP-Map .ol-attribution a:focus {
color: #D7CEC7; }
.LP-Map .ol-zoom-in, .LP-Map .ol-zoom-out {
background-color: #C09F80; }
.LP-Map .ol-zoom-in:hover, .LP-Map .ol-zoom-in:focus, .LP-Map .ol-zoom-out:hover, .LP-Map .ol-zoom-out:focus {
background-color: #565656; }
.LP-Map .LP-Map__Popup {
font-family: "Montserrat", Helvetica, sans-serif;
color: #565656;
background-color: #f9f9f9;
padding: .5em;
border-radius: 2px; }
.LP-MainContainer {
margin: 0 auto;
@ -1642,7 +1697,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;

View 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 */

View 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"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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">

View File

@ -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">

View File

@ -0,0 +1,8 @@
<div class="LP-Form__Field LP-Form__Button LP-Input">
<button class="LP-Button">{% if action %}{{ action }}{% else %}Submit{% endif %}</button>
</div>
<div class="LP-Form__Field LP-Form__Button LP-Input">
<a class="LP-Link" href="{% if referer %}{{ referer }}{% else %}{% url 'lostplaces_home' %}{% endif %}">
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
</a>
</div>

View File

@ -1,6 +1,6 @@
<div id="map" class="map" style="height: 300px"></div>
<div id="info" class="map-popup"></div>
{% load static %}
<div tabindex="1" id="map" class="LP-Map map" style="height: 300px"></div>
<div id="info" class="map-popup LP-Map__Popup"></div>
<script type="text/javascript">
var map = new ol.Map({
@ -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 %}
]
@ -37,8 +37,8 @@
anchor: [0.5, 46],
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
scale: 0.3,
src: 'http://icons.iconarchive.com/icons/paomedia/small-n-flat/128/map-marker-icon.png'
scale: 0.02,
src: '{% static "icons/map-marker-icon.png" %}'
})
})
});

View File

@ -1,6 +1,6 @@
<div class="LP-TagList">
<ul class="LP-TagList__List">
{% for tag in tag_list %}
{% for tag in config.tagged_item.tags.all %}
<li class="LP-TagList__Item">
<div class="LP-Tag">
<a href="#" class="LP-Link">
@ -23,7 +23,7 @@
</ul>
</div>
<form id="id_tag_submit_form" class="LP-Form LP-Form--inline LP-Form--tagging" method="POST" action="{{config.submit_url}}">
<form id="id_tag_submit_form" class="LP-Form LP-Form--inline LP-Form--tagging" method="POST" action="{% url config.submit_url_name tagged_id=config.tagged_item.id%}">
<fieldset class="LP-Form__Fieldset">
<legend class="LP-Form__Legend">Tags hinzufügen</legend>
{% csrf_token %}
@ -46,14 +46,10 @@
submit_form.onsubmit = () => false
const tagify = new Tagify(input, {
'whitelist': [{
%
for tag in all_tags %
}
'whitelist': [
{% for tag in config.all_tags %}
'{{tag}}',
{
% endfor %
}
{% endfor %}
]
})

View File

@ -27,14 +27,7 @@
</div>
<div class="LP-Form__Composition LP-Form__Composition--buttons">
<div class="LP-Form__Field LP-Form__Button LP-Input">
<button class="LP-Button">Submit</button>
</div>
<div class="LP-Form__Field LP-Form__Button LP-Input">
<a class="LP-Link" href="{% url 'place_detail' pk=place.id%}">
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
</a>
</div>
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER %}
</div>
</fieldset>
</form>

View File

@ -39,14 +39,7 @@
</div>
<div class="LP-Form__Composition LP-Form__Composition--buttons">
<div class="LP-Form__Field LP-Form__Button LP-Input">
<button class="LP-Button">Create</button>
</div>
<div class="LP-Form__Field LP-Form__Button LP-Input">
<a class="LP-Link" href="{% url 'place_list' %}">
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
</a>
</div>
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER action='Create' %}
</div>
</fieldset>

View File

@ -16,14 +16,7 @@
</div>
<div class="LP-Form__Composition LP-Form__Composition--buttons">
<div class="LP-Form__Field LP-Form__Button LP-Input">
<button class="LP-Button">Delete</button>
</div>
<div class="LP-Form__Field LP-Form__Button LP-Input">
<a class="LP-Link" href="{% url 'place_detail' pk=place.pk %}">
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
</a>
</div>
{% include 'partials/form/submit.html' with referer=request.META.HTTP_REFERER action='Delete' %}
</div>
</fieldset>
</form>

View File

@ -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,10 +54,10 @@
</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>
@ -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>

View 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 %}

View File

@ -39,14 +39,7 @@
</div>
<div class="LP-Form__Composition LP-Form__Composition--buttons">
<div class="LP-Form__Field LP-Form__Button LP-Input">
<button class="LP-Button">Update</button>
</div>
<div class="LP-Form__Field LP-Form__Button LP-Input">
<a class="LP-Link" href="{% url 'place_detail' pk=place.pk %}">
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
</a>
</div>
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER action='Update' %}
</div>
</fieldset>

View File

@ -0,0 +1,21 @@
{% extends 'global.html'%}
{% block maincontent %}
<form class="LP-Form" method="POST" enctype="multipart/form-data">
<fieldset class="LP-Form__Fieldset">
<legend class="LP-Form__Legend">Submit images to an place</legend>
{% csrf_token %}
<div class="LP-Form__Composition">
<div class="LP-Form__Field">
{% include 'partials/form/inputField.html' with field=form.filename %}
</div>
</div>
<div class="LP-Form__Composition LP-Form__Composition--buttons">
{% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER %}
</div>
</fieldset>
</form>
{% endblock maincontent %}

View File

Before

Width:  |  Height:  |  Size: 209 B

After

Width:  |  Height:  |  Size: 209 B

View File

@ -0,0 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

View File

@ -1,3 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django import template
register = template.Library()

View File

@ -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()

View File

@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django.test import TestCase
from django.contrib.auth.models import User

View 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
)
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -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

View File

@ -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)

View 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__
)
)

View File

@ -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'
)

Some files were not shown because too many files have changed in this diff Show More