Compare commits

..

No commits in common. "a2004bd789c6109b3598b5c2eefcc8b05c11c393" and "6e6f4ced7bd21b24fffc728a9bafaf99dccc5ed1" have entirely different histories.

140 changed files with 1054 additions and 11599 deletions

6
.gitignore vendored
View File

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

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

View File

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

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

View File

@ -2,14 +2,14 @@
lostplaces-backend is a django (3.x) based webproject. It once wants to become a software which allows a group of urban explorers to manage, document and share the locations of lost places while not exposing too much / any information to the public.
The software is currently in early development status, neither scope, 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.
@ -21,13 +21,6 @@ Right now it depends on the following non-core Python 3 libraries. These can be
* [image](https://github.com/francescortiz/image) Image cropping for django.
* [django-widget-tweaks](https://github.com/jazzband/django-widget-tweaks) Tweak the form field rendering in templates, not in python-level form definitions.
* [django-taggit](https://github.com/jazzband/django-taggit) A simpler approach to tagging with Django.
### Bundled Dependencies
We also leverage some other great OpenSource projects' code. We bundle those in the distribution to obsolete the need to pull those files from monitored CDNs.
* [OpenLayers](https://openlayers.org/) [6.4.3] OpenLayers makes it easy to put a dynamic map in any web page. It can display map tiles, vector data and markers loaded from any source.
* [Crimson Pro font](https://github.com/Fonthausen/CrimsonPro) [1.002] Crimson Pro is a serif typeface family: Contemporary, clear, classic and rounded/open.
* [Montserrat](https://github.com/JulietaUla/Montserrat) [7.210] A beautiful sans serif typeface.
# Installing a development instance
@ -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.

View File

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

View File

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

View File

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

View File

@ -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,
},
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

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

View File

@ -1 +0,0 @@
{"version":3,"sources":["src/ol/ol.css"],"names":[],"mappings":"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"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -23,11 +23,11 @@ from django.conf.urls.static import static
from django.urls import path, include
from django.views.generic.base import TemplateView
from lostplaces.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)

View File

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

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

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

View File

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

View File

@ -6,7 +6,7 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.contrib.auth.models import User
from lostplaces.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']

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

View File

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB

View File

@ -1,93 +1,94 @@
Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
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:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 816 B

After

Width:  |  Height:  |  Size: 816 B

View File

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 351 B

View File

Before

Width:  |  Height:  |  Size: 641 B

After

Width:  |  Height:  |  Size: 641 B

View File

Before

Width:  |  Height:  |  Size: 850 B

After

Width:  |  Height:  |  Size: 850 B

View File

Before

Width:  |  Height:  |  Size: 914 B

After

Width:  |  Height:  |  Size: 914 B

View File

Before

Width:  |  Height:  |  Size: 488 B

After

Width:  |  Height:  |  Size: 488 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 717 B

After

Width:  |  Height:  |  Size: 717 B

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -424,46 +424,46 @@
object-position: center; }
@font-face {
font-family: 'Crimson 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 * {
overflow: hidden;
word-break: break-all; }
.LP-ImageGrid__Item img {
width: 100%;
height: 100%;
object-fit: cover; }
.LP-ImageGrid__Item--left img {
object-position: left; }
.LP-ImageGrid__Item--center img {
object-position: center; }
.LP-ImageGrid__Item--top img {
object-position: top; }
.LP-ImageGrid__Item--bottom img {
object-position: botom; }
.LP-ImageGrid__Item--center img {
object-position: center; }
.LP-ImageGrid__Item--add .LP-Link {
width: 100%;
height: 100%;
position: relative;
display: block;
background: #f9f9f9; }
.LP-ImageGrid__Item--add .LP-Link .LP-Icon {
width: 35px;
height: 35px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); }
.LP-ImageGrid__Item--add:hover .LP-Link {
background-color: #D7CEC7; }
.LP-ImageGrid__DeleteItem {
opacity: 0.7;
visibility: hidden;
position: absolute;
top: 10px;
right: 10px;
background-color: #C09F80;
border-radius: 50%;
height: 35px;
width: 35px; }
.LP-ImageGrid__DeleteItem .LP-Link .LP-Icon {
height: 20px;
width: 20px;
position: relative;
left: 7.8125px;
top: 7.8125px; }
.LP-ImageGrid__Item > .LP-Link:hover + .LP-ImageGrid__DeleteItem, .LP-ImageGrid__DeleteItem:hover {
visibility: visible; }
.LP-Map {
margin-bottom: 25px; }
.LP-Map .ol-attribution {
font-family: "Montserrat", Helvetica, sans-serif;
color: #565656; }
.LP-Map .ol-attribution a {
color: #C09F80; }
.LP-Map .ol-attribution a:hover, .LP-Map .ol-attribution a:focus {
color: #D7CEC7; }
.LP-Map .ol-zoom-in, .LP-Map .ol-zoom-out {
background-color: #C09F80; }
.LP-Map .ol-zoom-in:hover, .LP-Map .ol-zoom-in:focus, .LP-Map .ol-zoom-out:hover, .LP-Map .ol-zoom-out:focus {
background-color: #565656; }
.LP-Map .LP-Map__Popup {
font-family: "Montserrat", Helvetica, sans-serif;
color: #565656;
background-color: #f9f9f9;
padding: .5em;
border-radius: 2px; }
.LP-ImageGrid .LP-ImageGrid__Item, .LP-ImageGrid .LP-ImageGrid__Item * {
overflow: hidden;
word-break: break-all; }
.LP-ImageGrid .LP-ImageGrid__Item img {
width: 100%;
height: 100%;
object-fit: cover; }
.LP-ImageGrid .LP-ImageGrid__Item--left img {
object-position: left; }
.LP-ImageGrid .LP-ImageGrid__Item--center img {
object-position: center; }
.LP-ImageGrid .LP-ImageGrid__Item--top img {
object-position: top; }
.LP-ImageGrid .LP-ImageGrid__Item--bottom img {
object-position: botom; }
.LP-ImageGrid .LP-ImageGrid__Item--center img {
object-position: center; }
.LP-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;

View File

@ -0,0 +1,2 @@
.ol-box{box-sizing:border-box;border-radius:2px;border:2px solid #00f}.ol-mouse-position{top:8px;right:8px;position:absolute}.ol-scale-line{background:rgba(0,60,136,.3);border-radius:4px;bottom:8px;left:8px;padding:2px;position:absolute}.ol-scale-line-inner{border:1px solid #eee;border-top:none;color:#eee;font-size:10px;text-align:center;margin:1px;will-change:contents,width;transition:all .25s}.ol-scale-bar{position:absolute;bottom:8px;left:8px}.ol-scale-step-marker{width:1px;height:15px;background-color:#000;float:right;z-Index:10}.ol-scale-step-text{position:absolute;bottom:-5px;font-size:12px;z-Index:11;color:#000;text-shadow:-2px 0 #fff,0 2px #fff,2px 0 #fff,0 -2px #fff}.ol-scale-text{position:absolute;font-size:14px;text-align:center;bottom:25px;color:#000;text-shadow:-2px 0 #fff,0 2px #fff,2px 0 #fff,0 -2px #fff}.ol-scale-singlebar{position:relative;height:10px;z-Index:9;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 */

View File

@ -0,0 +1 @@
{"version":3,"sources":["src/ol/ol.css"],"names":[],"mappings":"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"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

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