Compare commits
No commits in common. "a2004bd789c6109b3598b5c2eefcc8b05c11c393" and "6e6f4ced7bd21b24fffc728a9bafaf99dccc5ed1" have entirely different histories.
a2004bd789
...
6e6f4ced7b
6
.gitignore
vendored
@ -65,6 +65,12 @@ 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
@ -1,19 +1,7 @@
|
||||
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 Pipfile
|
||||
recursive-include lostplaces/static *
|
||||
recursive-include lostplaces/templates *
|
||||
recursive-include lostplaces_app/static *
|
||||
recursive-include lostplaces_app/templates *
|
12
Pipfile
@ -10,8 +10,7 @@ autopep8 = "*"
|
||||
pipenv = "*"
|
||||
wheel = "*"
|
||||
twine = "*"
|
||||
pandoc = "*"
|
||||
pylint-django = "*"
|
||||
pandoc ="*"
|
||||
|
||||
[packages]
|
||||
django = "*"
|
||||
@ -19,9 +18,6 @@ easy-thumbnails = "*"
|
||||
image = "*"
|
||||
django-widget-tweaks = "*"
|
||||
django-taggit = "*"
|
||||
|
||||
[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"
|
||||
# Commented out to not explicitly specify Python 3 subversion.
|
||||
# [requires]
|
||||
# python_version = "3.8"
|
||||
|
63
Readme.md
@ -2,14 +2,14 @@
|
||||
|
||||
lostplaces-backend is a django (3.x) based webproject. It once wants to become a software which allows a group of urban explorers to manage, document and share the locations of lost places while not exposing too much / any information to the public.
|
||||
|
||||
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.
|
||||
The software is currently in early development status, neither scope, datalmodel(s) nor features are finalized yet. Therefore we would not recommend to download or install this piece of software anywhere - except your local django dev server.
|
||||
|
||||
We value privacy as a whole, all 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.
|
||||
We value privacy as a whole, all ressources the frontend requires will be shipped with lostplace's distribution. We also try to minimze the use of JavaScript as far as we can and try to offer JS-less alternatives where we can.
|
||||
|
||||
## Features
|
||||
- Manage lost places with lots of useful information.
|
||||
- Manage lost places with lots of usefull information.
|
||||
- OSM-Maps
|
||||
- Sensitive information is not accessible for anonymous (not logged in) users.
|
||||
- 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.
|
||||
|
||||
@ -22,13 +22,6 @@ Right now it depends on the following non-core Python 3 libraries. These can be
|
||||
* [django-widget-tweaks](https://github.com/jazzband/django-widget-tweaks) Tweak the form field rendering in templates, not in python-level form definitions.
|
||||
* [django-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
|
||||
## Clone the repository
|
||||
@ -41,10 +34,10 @@ After having obtained the repository contents (either via .zip download or git c
|
||||
$ cd lostplaces-backend
|
||||
$ pipenv install
|
||||
$ pipenv shell
|
||||
(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
|
||||
(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
|
||||
@ -52,9 +45,9 @@ $ pipenv shell
|
||||
$ cd lostplaces-backend
|
||||
$ pipenv shell
|
||||
(lostplaces-backend) $ pipenv update # If dependencies changed, or updates available
|
||||
(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
|
||||
(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
|
||||
@ -70,25 +63,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 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.
|
||||
Essentially, this is the same as installing a development instance, but without the development server (manage.py runserver) and something powerfull (Apache, NGINX) instead. You have to configure the webserve to work with the *SGI Api respectivly, reference [django's guide for deployment](https://docs.djangoproject.com/en/3.1/howto/deployment/) for further information.
|
||||
|
||||
You also should setup a dedicated database server, the built-in SQLite file is not recommended for production use. Reference [django's guide for databases](https://docs.djangoproject.com/en/3.1/ref/databases/) for further information.
|
||||
You also should setup a dedicated database server, the built-in SQLite file is not recommened for production use. Reference [django's guide for databases](https://docs.djangoproject.com/en/3.1/ref/databases/) for further information.
|
||||
|
||||
Before making the django instance public, you should tweak the config `settings.py`:
|
||||
1. Change the secret key, the one found in the config is already public. Choose something secure (i.e. [this](https://duckduckgo.com/?q=password+generator+64)).
|
||||
2. Turn off debug mode by setting `DEBUG = False`.
|
||||
3. Tune the localization settings, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/).
|
||||
|
||||
Run `django_lostplaces/managy.py collectstatic` and you should be ready to go.
|
||||
Run `lostplaces/managy.py collectstatic` and you should be ready to go.
|
||||
|
||||
|
||||
## Installing lostplaces to an existing django instance
|
||||
## Installing the lostplaces_app to an existing django instance
|
||||
|
||||
### Installing django and the django_lostplaces app
|
||||
### Installing django and the lostplaces app
|
||||
|
||||
If you haven't already setup a django instance, see [django's documentation](https://docs.djangoproject.com/en/3.1/topics/install/).
|
||||
|
||||
After that, download the desired release (probably the latest one) [from the releases page](https://git.mowoe.com/reverend/lostplaces-backend/releases) and install it using `pip install --user name-of-the-file.tar.gz`
|
||||
After that, download the desired release (probably the latest one) [from the realeases page](https://git.mowoe.com/reverend/lostplaces-backend/releases) and install it using `pip install --user name-of-the-file.tar.gz`
|
||||
|
||||
*Note: You can run pip install without the --user flag, which will require root privileges and introduces potential security issues.*
|
||||
|
||||
@ -100,7 +93,7 @@ Now configure your `settings.py` as follows:
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'django_lostplaces',
|
||||
'lostplaces_app',
|
||||
'easy_thumbnails',
|
||||
'widget_tweaks',
|
||||
'django_taggit'
|
||||
@ -117,22 +110,28 @@ MEDIA_URL = '/uploads/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
||||
```
|
||||
|
||||
3. Set the URL's for login, for example:
|
||||
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:
|
||||
|
||||
```python
|
||||
LOGIN_URL = reverse_lazy('login')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('django_lostplaces_home')
|
||||
LOGOUT_REDIRECT_URL = reverse_lazy('django_lostplaces_home')
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
||||
LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
|
||||
```
|
||||
|
||||
### Configuring the URL's
|
||||
In the `urls.py` configure the `urlpatterns` like this:
|
||||
In the `urls.py` configure the `urlpatter` like this:
|
||||
```python
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('signup/', SignUpView.as_view(), name='signup'), # If you want to use lostplaces' sign up view.
|
||||
path('explorers/', include('django.contrib.auth.urls')), # You can change the 'explorers/' to whatever you desire.
|
||||
path('', include('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.
|
||||
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.
|
||||
```
|
||||
|
||||
@ -140,7 +139,5 @@ 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 `django_lostplaces/manage.py collectstatic` you should be ready to go.
|
||||
Run `lostplaces/managy.py collectstatic` you should be ready to go.
|
||||
|
@ -1,155 +0,0 @@
|
||||
# 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).
|
@ -1,12 +0,0 @@
|
||||
#!/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},
|
||||
},
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
#!/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,90 +0,0 @@
|
||||
# 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,
|
||||
},
|
||||
),
|
||||
]
|
@ -1,4 +0,0 @@
|
||||
from lostplaces.models.abstract_models import *
|
||||
from lostplaces.models.place import *
|
||||
from lostplaces.models.external_links import *
|
||||
from lostplaces.models.models import *
|
@ -1,61 +0,0 @@
|
||||
|
||||
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()
|
@ -1,14 +0,0 @@
|
||||
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
|
@ -1,55 +0,0 @@
|
||||
#!/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)
|
||||
|
@ -1,129 +0,0 @@
|
||||
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)
|
@ -1,93 +0,0 @@
|
||||
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:
|
||||
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: 22 KiB |
@ -1,2 +0,0 @@
|
||||
.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 +0,0 @@
|
||||
{"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"}
|
@ -1,8 +0,0 @@
|
||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
||||
<button class="LP-Button">{% if action %}{{ action }}{% else %}Submit{% endif %}</button>
|
||||
</div>
|
||||
<div class="LP-Form__Field LP-Form__Button LP-Input">
|
||||
<a class="LP-Link" href="{% if referer %}{{ referer }}{% else %}{% url 'lostplaces_home' %}{% endif %}">
|
||||
<button type="button" class="LP-Button LP-Button--cancel">Cancel</button>
|
||||
</a>
|
||||
</div>
|
@ -1,59 +0,0 @@
|
||||
{% 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 %}
|
@ -1,21 +0,0 @@
|
||||
{% 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 %}
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
@ -1,144 +0,0 @@
|
||||
#!/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
|
||||
)
|
||||
)
|
Before Width: | Height: | Size: 81 KiB |
@ -1,115 +0,0 @@
|
||||
#!/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
|
@ -1,61 +0,0 @@
|
||||
#!/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)
|
||||
|
||||
|
||||
|
@ -1,101 +0,0 @@
|
||||
#!/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__
|
||||
)
|
||||
)
|
@ -1,104 +0,0 @@
|
||||
#!/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'
|
||||
)
|
@ -1,126 +0,0 @@
|
||||
#!/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__
|
||||
)
|
||||
)
|
@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
from lostplaces.models import Voucher
|
||||
from lostplaces.tests.models import ModelTestCase
|
||||
|
||||
|
||||
class VoucherTestCase(ModelTestCase):
|
||||
model = Voucher
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
Voucher.objects.create(
|
||||
code='ayDraJCCwfhcFiYmSR5GrcjcchDfcahv',
|
||||
expires_when=timezone.now() + datetime.timedelta(days=1)
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.voucher = Voucher.objects.get(id=1)
|
||||
|
||||
def test_voucher_code(self):
|
||||
self.assertCharField(
|
||||
field_name='code',
|
||||
min_length=10,
|
||||
max_length=100,
|
||||
must_have={'unique': True}
|
||||
)
|
||||
|
||||
def test_voucher_created(self):
|
||||
self.assertField(
|
||||
field_name='created_when',
|
||||
field_class=models.DateTimeField,
|
||||
must_have={'auto_now_add': True}
|
||||
)
|
||||
|
||||
def test_voucher_expires(self):
|
||||
self.assertField(
|
||||
field_name='expires_when',
|
||||
field_class=models.DateTimeField,
|
||||
must_not_have={'auto_now_add': True}
|
||||
)
|
@ -1,227 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from lostplaces.models import Taggable, Mapable
|
||||
|
||||
from taggit.models import Tag
|
||||
|
||||
class ViewTestCase(TestCase):
|
||||
'''
|
||||
This is a Mixin for testing views. It provides functionality to
|
||||
test the context, forms and HTTP Response of responses.
|
||||
All methods take responses, so this base class can be used
|
||||
with django's RequestFactory and Test-Client
|
||||
'''
|
||||
view = None
|
||||
|
||||
def assertContext(self, response, key, value=None):
|
||||
'''
|
||||
Checks weather the response's context has the given key
|
||||
and, if passed, checks the value
|
||||
'''
|
||||
self.assertTrue(
|
||||
key in response.context,
|
||||
msg='Expecting the context of %s to have an attribute \'%s\'' % (
|
||||
self.view.__name__,
|
||||
key
|
||||
)
|
||||
)
|
||||
|
||||
if value:
|
||||
self.assertEqual(
|
||||
value,
|
||||
response.context[key],
|
||||
msg='Expecting the context of %s to have %s set to \'%s\'' % (
|
||||
self.view.__name__,
|
||||
key,
|
||||
str(value)
|
||||
)
|
||||
)
|
||||
|
||||
def assertHasForm(self, response, key, form_class):
|
||||
'''
|
||||
Checks if response has a form under the given key and if
|
||||
the forms class matches.
|
||||
'''
|
||||
self.assertContext(response, key)
|
||||
self.assertEqual(
|
||||
type(response.context[key]),
|
||||
form_class,
|
||||
msg='Expecting %s\'s context.%s to be of the type %s' % (
|
||||
self.view.__name__,
|
||||
key,
|
||||
form_class.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def assertHttpCode(self, response, code):
|
||||
'''
|
||||
Checks if the response has the given status code
|
||||
'''
|
||||
self.assertEqual(
|
||||
response.status_code, code,
|
||||
msg='Expecting an HTTP %s response, but got HTTP %s' % (
|
||||
code,
|
||||
response.status_code
|
||||
)
|
||||
)
|
||||
|
||||
def assertHttpRedirect(self, response, redirect_to=None):
|
||||
'''
|
||||
Checks weather the response redirected, and if passed,
|
||||
if it redirected to the expected location
|
||||
'''
|
||||
|
||||
self.assertTrue(
|
||||
300 <= response.status_code < 400,
|
||||
'Expected an HTTP 3XX (redirect) response, but got HTTP %s' %
|
||||
response.status_code
|
||||
)
|
||||
self.assertTrue(
|
||||
'location' in response,
|
||||
msg='Expecting a redirect to have an location, got none'
|
||||
)
|
||||
if redirect_to:
|
||||
self.assertEqual(
|
||||
response['location'],
|
||||
redirect_to,
|
||||
msg='Expecting the response to redirect to %s, where redirected to %s instea' % (
|
||||
str(redirect_to),
|
||||
str(response['location'])
|
||||
)
|
||||
)
|
||||
|
||||
def assertHttpOK(self, response):
|
||||
self.assertHttpCode(response, 200)
|
||||
|
||||
def assertHttpCreated(self, response):
|
||||
self.assertHttpCode(response, 201)
|
||||
|
||||
def assertHttpBadRequest(self, response):
|
||||
self.assertHttpCode(response, 400)
|
||||
|
||||
def assertHttpUnauthorized(self, response):
|
||||
self.assertHttpCode(response, 401)
|
||||
|
||||
def assertHttpForbidden(self, response):
|
||||
self.assertHttpCode(response, 403)
|
||||
|
||||
def assertHttpNotFound(self, response):
|
||||
self.assertHttpCode(response, 404)
|
||||
|
||||
def assertHttpMethodNotAllowed(self, response):
|
||||
self.assertHttpCode(response, 405)
|
||||
|
||||
class TaggableViewTestCaseMixin:
|
||||
|
||||
def assertTaggableContext(self, context):
|
||||
self.assertTrue(
|
||||
'all_tags' in context,
|
||||
msg='Expecting the context for taggable to contain an \'all_tags\' attribute'
|
||||
)
|
||||
|
||||
for tag in context['all_tags']:
|
||||
self.assertTrue(
|
||||
isinstance(tag, Tag),
|
||||
msg='Expecting all entries to be an instance of %s, got %s' % (
|
||||
str(Tag),
|
||||
str(type(tag))
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'submit_form' in context,
|
||||
msg='Expecting the context for taggable to contain \'submit_form\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'tagged_item' in context,
|
||||
msg='Expecting the context for taggable to contain \'tagged_item\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
isinstance(context['tagged_item'], Taggable),
|
||||
msg='Expecting the tagged_item to be an instance of %s' % (
|
||||
str(Taggable)
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'submit_url_name' in context,
|
||||
msg='Expecting the context for taggable to contain \'submit_url_name\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
type(context['submit_url_name']) == str,
|
||||
msg='Expecting submit_url_name to be of type string'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'delete_url_name' in context,
|
||||
msg='Expecting the context for taggable to contain \'delete_url_name\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
type(context['delete_url_name']) == str,
|
||||
msg='Expecting delete_url_name to be of type string'
|
||||
)
|
||||
|
||||
class MapableViewTestCaseMixin:
|
||||
|
||||
def assertMapableContext(self, context):
|
||||
self.assertTrue(
|
||||
'all_points' in context,
|
||||
msg='Expecting the context for mapable point to contain \'all_points\' attribute'
|
||||
)
|
||||
|
||||
for point in context['all_points']:
|
||||
self.assertTrue(
|
||||
isinstance(point, Mapable),
|
||||
msg='Expecting all entries to be an instance of %s, got %s' % (
|
||||
str(Mapable),
|
||||
str(type(point))
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'map_center' in context,
|
||||
msg='Expecting the context for mapable point to contain \'map_center\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'latitude' in context['map_center'],
|
||||
msg='Expecting the map center to contain an \'latitude\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
isinstance(context['map_center']['latitude'], float) or isinstance(context['map_center']['latitude'], int),
|
||||
msg='Expecting the latitude of the map center to be numeric, type %s given' % (
|
||||
str(type(context['map_center']['latitude']))
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
-90 <= context['map_center']['latitude'] <= 90,
|
||||
msg='Expecting the latitude of map center to be in the range of -90 and 90'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
'longitude' in context['map_center'],
|
||||
msg='Expecting the map center to contain an \'longitude\' attribute'
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
isinstance(context['map_center']['longitude'], float) or isinstance(context['map_center']['longitude'], int),
|
||||
msg='Expecting the longitude of the map center to be numeric, type %s given' % (
|
||||
str(type(context['map_center']['longitude']))
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
-180 <= context['map_center']['longitude'] <= 180,
|
||||
msg='Expecting the longitude of map center to be in the range of -180 and 180'
|
||||
)
|
||||
|
||||
|
@ -1,89 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.test import TestCase, RequestFactory, Client
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||
from django.utils import timezone
|
||||
|
||||
from lostplaces.models import Place
|
||||
from lostplaces.views import IsAuthenticatedMixin
|
||||
from lostplaces.tests.views import ViewTestCase
|
||||
|
||||
class TestIsAuthenticated(ViewTestCase):
|
||||
view = IsAuthenticatedMixin
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
user = User.objects.create_user(
|
||||
username='testpeter',
|
||||
password='Develop123'
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_logged_in(self):
|
||||
request = RequestFactory().get('/')
|
||||
request.user = User.objects.get(id=1)
|
||||
|
||||
response = IsAuthenticatedMixin.as_view()(request)
|
||||
# Expecting a 405 because IsAuthenticatedMixin has no 'get' method
|
||||
self.assertHttpMethodNotAllowed(response)
|
||||
|
||||
def test_not_logged_in(self):
|
||||
request = RequestFactory().get('/someurl1234')
|
||||
request.user = AnonymousUser()
|
||||
request.session = 'session'
|
||||
messages = FallbackStorage(request)
|
||||
request._messages = messages
|
||||
|
||||
response = IsAuthenticatedMixin.as_view()(request)
|
||||
self.assertHttpRedirect(response, '?'.join([str(reverse_lazy('login')), 'next=/someurl1234']))
|
||||
|
||||
response = self.client.get(response['Location'])
|
||||
self.assertTrue(len(messages) > 0)
|
||||
|
||||
class TestIsPlaceSubmitterMixin(TestCase):
|
||||
|
||||
@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.client = Client()
|
||||
|
||||
def setUp(self):
|
||||
self. client = Client()
|
||||
|
||||
def test_is_submitter(self):
|
||||
self.client.login(username='testpeter', password='Develop123')
|
||||
response = self.client.get(reverse_lazy('place_edit', kwargs={'pk': 1}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_is_no_submitter(self):
|
||||
User.objects.create_user(
|
||||
username='manfred',
|
||||
password='Develop123'
|
||||
)
|
||||
self.client.login(username='manfred', password='Develop123')
|
||||
response = self.client.get(reverse_lazy('place_edit', kwargs={'pk': 1}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertTrue(response.context['messages'])
|
||||
self.assertTrue(len(response.context['messages']) > 0)
|
@ -1,129 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
from lostplaces.models import Place
|
||||
from lostplaces.views import (
|
||||
PlaceCreateView,
|
||||
PlaceListView,
|
||||
PlaceDetailView
|
||||
)
|
||||
from lostplaces.forms import PlaceImageForm, PlaceForm
|
||||
from lostplaces.tests.views import (
|
||||
ViewTestCase,
|
||||
TaggableViewTestCaseMixin,
|
||||
MapableViewTestCaseMixin
|
||||
)
|
||||
|
||||
|
||||
class TestPlaceCreateView(ViewTestCase):
|
||||
view = PlaceCreateView
|
||||
|
||||
@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.client = Client()
|
||||
|
||||
def test_has_forms(self):
|
||||
self.client.login(username='testpeter', password='Develop123')
|
||||
response = self.client.get(reverse('place_create'))
|
||||
|
||||
self.assertHasForm(response, 'place_image_form', PlaceImageForm)
|
||||
self.assertHasForm(response, 'place_form', PlaceForm)
|
||||
|
||||
|
||||
class TestPlaceListView(ViewTestCase):
|
||||
view = PlaceListView
|
||||
|
||||
@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.client = Client()
|
||||
|
||||
def test_list_view(self):
|
||||
self.client.login(username='testpeter', password='Develop123')
|
||||
response = self.client.get(reverse('place_list'))
|
||||
|
||||
self.assertContext(response, 'mapping_config')
|
||||
|
||||
|
||||
class PlaceDetailViewTestCase(TaggableViewTestCaseMixin, MapableViewTestCaseMixin, ViewTestCase):
|
||||
view = PlaceDetailView
|
||||
|
||||
@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 test_context(self):
|
||||
self.client.login(username='testpeter', password='Develop123')
|
||||
response = self.client.get(reverse('place_detail', kwargs={'pk': 1}))
|
||||
|
||||
self.assertTrue(
|
||||
'tagging_config' in response.context,
|
||||
msg='Expecting the context of %s to have an \'tagging_config\'' % (
|
||||
str(self.view)
|
||||
)
|
||||
)
|
||||
self.assertTaggableContext(response.context['tagging_config'])
|
||||
|
||||
self.assertTrue(
|
||||
'mapping_config' in response.context,
|
||||
msg='Expecting the context of %s to have an \'mapping_config\'' % (
|
||||
str(self.view)
|
||||
)
|
||||
)
|
||||
self.assertMapableContext(response.context['mapping_config'])
|
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from lostplaces.views.base_views import *
|
||||
from lostplaces.views.views import *
|
||||
from lostplaces.views.place_views import *
|
||||
from lostplaces.views.place_image_views import *
|
@ -1,43 +0,0 @@
|
||||
from django.views import View
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
|
||||
from lostplaces.views.base_views import PlaceAssetCreateView, PlaceAssetDeleteView
|
||||
from lostplaces.models import PlaceImage, Place
|
||||
from lostplaces.forms import PlaceImageForm
|
||||
|
||||
class MultiplePlaceImageUploadMixin:
|
||||
|
||||
def handle_place_images(self, request, place):
|
||||
if request.FILES:
|
||||
submitted_by = request.user.explorer
|
||||
for image in request.FILES.getlist('filename'):
|
||||
place_image = PlaceImage.objects.create(
|
||||
filename=image,
|
||||
place=place,
|
||||
submitted_by=submitted_by
|
||||
)
|
||||
place_image.save()
|
||||
|
||||
class PlaceImageCreateView(MultiplePlaceImageUploadMixin, PlaceAssetCreateView):
|
||||
model = PlaceImage
|
||||
form_class = PlaceImageForm
|
||||
template_name = 'place_image/place_image_create.html'
|
||||
success_message = 'Place Images submitted'
|
||||
commit = False
|
||||
|
||||
def post(self, request, place_id, *args, **kwargs):
|
||||
self.place = get_object_or_404(Place, pk=place_id)
|
||||
self.handle_place_images(request, self.place)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.place = self.place
|
||||
form.instance.submitted_by = self.request.user.explorer
|
||||
return super().form_valid(form)
|
||||
|
||||
class PlaceImageDeleteView(PlaceAssetDeleteView):
|
||||
model = PlaceImage
|
||||
success_message = 'Images deleted successfully'
|
||||
permission_denied_message = 'You\'r not allowed to delete this image'
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
# Config options for coverage
|
||||
# Docs: https://coverage.readthedocs.io/en/latest/config.html
|
||||
|
||||
[coverage:run]
|
||||
source = .
|
||||
|
||||
[coverage:report]
|
||||
show_missing = True
|
9216
django_lostplaces/testdata/testdata.json
vendored
24
django_lostplaces/testdata/testdata.md
vendored
@ -1,24 +0,0 @@
|
||||
# testdata
|
||||
|
||||
This testdata is provided for you to see the database filled with some dummycontent.
|
||||
|
||||
## Database content
|
||||
testdata is provided in this repository / directory in testdata.json. It has been
|
||||
dumped using:
|
||||
|
||||
```
|
||||
manage.py dumpdata --all --exclude=auth --exclude=sessions --indent 4 --o testdata/testdata.json
|
||||
```
|
||||
|
||||
To successfully import this testdata, you have to create 2 users. ID=1 is your normal superuser, ID=2 is an unprivileged user.
|
||||
You can import it using:
|
||||
|
||||
```
|
||||
manage.py loaddata testdata.json
|
||||
```
|
||||
|
||||
## Images
|
||||
Although I created pretty small testimages, I think they are still too clunky to
|
||||
mindlessly dump it into the code repository, so I provide an
|
||||
[archive](https://www.commander1024.de/lostplaces-testdata.zip) containing
|
||||
a folder structure of images to be extracted into the uploads/ folder.
|
@ -14,6 +14,6 @@ import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_lostplaces.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lostplaces.settings')
|
||||
|
||||
application = get_asgi_application()
|
@ -38,7 +38,7 @@ ALLOWED_HOSTS = ['localhost']
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'lostplaces',
|
||||
'lostplaces_app',
|
||||
'easy_thumbnails',
|
||||
'widget_tweaks',
|
||||
'taggit',
|
||||
@ -60,7 +60,7 @@ MIDDLEWARE = [
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'django_lostplaces.urls'
|
||||
ROOT_URLCONF = 'lostplaces.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
@ -78,7 +78,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'django_lostplaces.wsgi.application'
|
||||
WSGI_APPLICATION = 'lostplaces.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
@ -131,15 +131,9 @@ 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')
|
@ -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.views import SignUpView
|
||||
from lostplaces_app.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.urls')),
|
||||
path('', include('lostplaces_app.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', 'django_lostplaces.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lostplaces.settings')
|
||||
|
||||
application = get_wsgi_application()
|
11
lostplaces/lostplaces_app/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.conf import settings
|
||||
|
||||
settings.THUMBNAIL_ALIASES = {
|
||||
'': {
|
||||
'thumbnail': {'size': (300, 200), 'crop': True},
|
||||
'hero': {'size': (700, 466), 'crop': True},
|
||||
'large': {'size': (1920, 1920), 'crop': False},
|
||||
},
|
||||
}
|
23
lostplaces/lostplaces_app/admin.py
Normal file
@ -0,0 +1,23 @@
|
||||
#!/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 .models import *
|
||||
|
||||
from .forms import ExplorerCreationForm, ExplorerChangeForm
|
||||
|
||||
# Register your models here.
|
||||
|
||||
class VoucherAdmin(admin.ModelAdmin):
|
||||
fields = ['code', 'expires', 'created']
|
||||
readonly_fields = ['created']
|
||||
|
||||
admin.site.register(Explorer)
|
||||
admin.site.register(Voucher, VoucherAdmin)
|
||||
admin.site.register(Place)
|
||||
admin.site.register(PlaceImage)
|
||||
admin.site.register(PhotoAlbum)
|
@ -1,7 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
class LostplacesAppConfig(AppConfig):
|
||||
name = 'lostplaces'
|
||||
name = 'lostplaces_app'
|
@ -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.models import Place, PlaceImage, Voucher
|
||||
from lostplaces_app.models import Place, PlaceImage, Voucher
|
||||
|
||||
class ExplorerCreationForm(UserCreationForm):
|
||||
class Meta:
|
||||
@ -19,17 +19,13 @@ class ExplorerCreationForm(UserCreationForm):
|
||||
|
||||
def is_valid(self):
|
||||
super().is_valid()
|
||||
submitted_voucher = self.cleaned_data.get('voucher')
|
||||
sumitted_voucher = self.cleaned_data.get('voucher')
|
||||
try:
|
||||
fetched_voucher = Voucher.objects.get(code=submitted_voucher)
|
||||
fetched_voucher = Voucher.objects.get(code=sumitted_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
|
||||
|
||||
@ -44,7 +40,7 @@ class PlaceForm(forms.ModelForm):
|
||||
fields = '__all__'
|
||||
exclude = ['submitted_by']
|
||||
|
||||
class PlaceImageForm(forms.ModelForm):
|
||||
class PlaceImageCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = PlaceImage
|
||||
fields = ['filename']
|
207
lostplaces/lostplaces_app/models.py
Normal file
@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
(Data)models which describe the structure of data to be saved into
|
||||
database.
|
||||
'''
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from easy_thumbnails.fields import ThumbnailerImageField
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
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.name
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
Explorer.objects.create(user=instance)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
instance.explorer.save()
|
||||
|
||||
|
||||
class Voucher(models.Model):
|
||||
"""
|
||||
Vouchers are authorization tokens 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)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
expires = models.DateField()
|
||||
|
||||
def __str__(self):
|
||||
return "Voucher " + str(self.pk)
|
||||
|
||||
|
||||
class Place (models.Model):
|
||||
"""
|
||||
Place defines a lost place (location, name, description etc.).
|
||||
"""
|
||||
|
||||
name = models.CharField(max_length=50)
|
||||
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='places'
|
||||
)
|
||||
location = models.CharField(max_length=50)
|
||||
latitude = models.FloatField(
|
||||
validators=[
|
||||
MinValueValidator(-90),
|
||||
MaxValueValidator(90)
|
||||
]
|
||||
)
|
||||
longitude = models.FloatField(
|
||||
validators=[
|
||||
MinValueValidator(-180),
|
||||
MaxValueValidator(180)
|
||||
]
|
||||
)
|
||||
description = models.TextField()
|
||||
|
||||
tags = TaggableManager(blank=True)
|
||||
|
||||
# Get center position of LP-geocoordinates.
|
||||
def average_latlon(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 / amount, longitude / amount)
|
||||
|
||||
return (latitude, longitude)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
def generate_image_upload_path(instance, filename):
|
||||
"""
|
||||
Callback for generating path for uploaded images.
|
||||
"""
|
||||
|
||||
return 'places/' + str(uuid.uuid4())+'.'+filename.split('.')[-1]
|
||||
|
||||
|
||||
class PlaceImage (models.Model):
|
||||
"""
|
||||
PlaceImage defines an image file object that points to a file in uploads/.
|
||||
Intermediate image sizes are generated as defined in SIZES.
|
||||
PlaceImage references a Place to which it belongs.
|
||||
"""
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
filename = ThumbnailerImageField(upload_to=generate_image_upload_path)
|
||||
place = models.ForeignKey(
|
||||
Place,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='images'
|
||||
)
|
||||
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='images'
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Returning the name of the corresponding place + id
|
||||
of this image as textual represntation of this instance
|
||||
"""
|
||||
|
||||
return ' '.join([self.place.name, str(self.pk)])
|
||||
|
||||
# These two auto-delete files from filesystem when they are unneeded:
|
||||
|
||||
|
||||
@receiver(models.signals.post_delete, sender=PlaceImage)
|
||||
def auto_delete_file_on_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes file from filesystem
|
||||
when corresponding `PlaceImage` object is deleted.
|
||||
"""
|
||||
if instance.filename:
|
||||
if os.path.isfile(instance.filename.path):
|
||||
os.remove(instance.filename.path)
|
||||
|
||||
|
||||
@receiver(models.signals.pre_save, sender=PlaceImage)
|
||||
def auto_delete_file_on_change(sender, instance, **kwargs):
|
||||
"""
|
||||
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
|
||||
|
||||
new_file = instance.filename
|
||||
if not old_file == new_file:
|
||||
if os.path.isfile(old_file.path):
|
||||
os.remove(old_file.path)
|
||||
|
||||
|
||||
class ExternalLink(models.Model):
|
||||
url = models.URLField(max_length=200)
|
||||
label = models.CharField(max_length=100)
|
||||
submitted_by = models.ForeignKey(
|
||||
Explorer,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='external_links'
|
||||
)
|
||||
submitted_when = models.DateTimeField(auto_now_add=True, null=True)
|
||||
|
||||
|
||||
class PhotoAlbum(ExternalLink):
|
||||
place = models.ForeignKey(
|
||||
Place,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='photo_albums',
|
||||
null=True
|
||||
)
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 264 KiB |
@ -1,4 +1,5 @@
|
||||
Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)
|
||||
Copyright (c) 2010, Sebastian Kosch (sebastian@aldusleaf.org),
|
||||
with Reserved Font Name "Crimson" and "Crimson Text".
|
||||
|
||||
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:
|
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 Pro';
|
||||
font-family: 'Crimson Text';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local("Crimson Pro Regular"), local("CrimsonPro-Regular"), url(fonts/Crimson/CrimsonPro-Regular.ttf) format("truetype"); }
|
||||
src: local("Crimson Text Regular"), local("CrimsonText-Regular"), url(fonts/Crimson/CrimsonText-Regular.ttf) format("truetype"); }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Crimson Pro';
|
||||
font-family: 'Crimson Text';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local("Crimson Pro Italic"), local("CrimsonPro-Italic"), url(fonts/Crimson/CrimsonPro-Italic.ttf) format("truetype"); }
|
||||
src: local("Crimson Text Italic"), local("CrimsonText-Italic"), url(fonts/Crimson/CrimsonText-Italic.ttf) format("truetype"); }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Crimson Pro';
|
||||
font-family: 'Crimson Text';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: local("Crimson Pro Bold"), local("CrimsonPro-Bold"), url(fonts/Crimson/CrimsonPro-Bold.ttf) format("truetype"); }
|
||||
src: local("Crimson Text Bold"), local("CrimsonText-Bold"), url(fonts/Crimson/CrimsonText-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.woff2) format("woff2"); }
|
||||
src: local("Montserrat Regular"), local("Montserrat-Regular"), url(fonts/Montserrat/Montserrat-Regular.ttf) format("truetype"); }
|
||||
|
||||
@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.woff2) format("woff2"); }
|
||||
src: local("Montserrat Italic"), local("Montserrat-Italic"), url(fonts/Montserrat/Montserrat-Italic.ttf) format("truetype"); }
|
||||
|
||||
@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.woff2) format("woff2"); }
|
||||
src: local("Montserrat Bold"), local("Montserrat-Bold"), url(fonts/Montserrat/Montserrat-Bold.ttf) format("truetype"); }
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
@ -583,7 +583,7 @@ body {
|
||||
|
||||
.LP-Paragraph {
|
||||
color: black;
|
||||
font-family: "Crimson Pro", Times, serif;
|
||||
font-family: "Crimson Text", Times, serif;
|
||||
font-size: 1.4rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@ -856,7 +856,7 @@ body {
|
||||
padding: 15px; } }
|
||||
|
||||
.LP-TextSection__Text {
|
||||
font-family: "Crimson Pro", Times, serif;
|
||||
font-family: "Crimson Text", Times, serif;
|
||||
font-size: 1.4rem; }
|
||||
.LP-TextSection__Text .LP-Link {
|
||||
margin: 0 3px; }
|
||||
@ -1588,78 +1588,23 @@ body {
|
||||
border: none; }
|
||||
.LP-ImageGrid__Container {
|
||||
gap: 10px; }
|
||||
.LP-ImageGrid__Item {
|
||||
position: relative; }
|
||||
.LP-ImageGrid__Item, .LP-ImageGrid__Item * {
|
||||
.LP-ImageGrid .LP-ImageGrid__Item, .LP-ImageGrid .LP-ImageGrid__Item * {
|
||||
overflow: hidden;
|
||||
word-break: break-all; }
|
||||
.LP-ImageGrid__Item img {
|
||||
.LP-ImageGrid .LP-ImageGrid__Item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; }
|
||||
.LP-ImageGrid__Item--left img {
|
||||
.LP-ImageGrid .LP-ImageGrid__Item--left img {
|
||||
object-position: left; }
|
||||
.LP-ImageGrid__Item--center img {
|
||||
.LP-ImageGrid .LP-ImageGrid__Item--center img {
|
||||
object-position: center; }
|
||||
.LP-ImageGrid__Item--top img {
|
||||
.LP-ImageGrid .LP-ImageGrid__Item--top img {
|
||||
object-position: top; }
|
||||
.LP-ImageGrid__Item--bottom img {
|
||||
.LP-ImageGrid .LP-ImageGrid__Item--bottom img {
|
||||
object-position: botom; }
|
||||
.LP-ImageGrid__Item--center img {
|
||||
.LP-ImageGrid .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;
|
||||
@ -1697,7 +1642,7 @@ body {
|
||||
margin-bottom: 25px; } }
|
||||
|
||||
.LP-TextSection .LP-UnorderedList {
|
||||
font-family: "Crimson Pro", Times, serif;
|
||||
font-family: "Crimson Text", Times, serif;
|
||||
font-size: 1.4rem; }
|
||||
.LP-TextSection .LP-UnorderedList li {
|
||||
margin-bottom: 0.75em;
|
2
lostplaces/lostplaces_app/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;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-overlaycontainer,.ol-overlaycontainer-stopevent{pointer-events:none}.ol-overlaycontainer-stopevent>*,.ol-overlaycontainer>*{pointer-events:auto}.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-control button span{pointer-events:none}.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
lostplaces/lostplaces_app/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,OAAQ,IAAI,MAAM,KAGpB,gBACE,QAAS,KAEG,iBAAd,aACE,sBAAuB,KACvB,oBAAqB,KACrB,iBAAkB,KAClB,gBAAiB,KACjB,YAAa,KACb,4BAA6B,YAE/B,qBAAsB,+BACpB,eAAgB,KAEQ,iCAA1B,uBACE,eAAgB,KAElB,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,wBACE,eAAgB,KAElB,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"}
|
2
lostplaces/lostplaces_app/static/maps/ol.js
Normal file
1
lostplaces/lostplaces_app/static/maps/ol.js.map
Normal file
@ -0,0 +1,7 @@
|
||||
# Keeping these files up-to-date is something for CI / release management.
|
||||
# But for now I noted the source urls down here for later reference.
|
||||
|
||||
https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.3.1/css/ol.css
|
||||
https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.3.1/css/ol.css.map
|
||||
https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.3.1/build/ol.js
|
||||
https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.3.1/build/ol.js.map
|