Compare commits

..

27 Commits

Author SHA1 Message Date
96af47c539 Fixed syntax error. 2020-09-09 20:26:49 +02:00
e17f55d7d3 Merge remote-tracking branch 'origin/master' into feature/unauth_content 2020-09-09 20:22:44 +02:00
f8cae76f05 testing base views 2020-09-04 21:48:05 +02:00
17637ff0b9 more test 2020-09-03 22:32:28 +02:00
b055c5f891 Tests for place_image 2020-09-03 22:22:04 +02:00
1dee72cd15 Commenting tests 2020-09-03 20:24:20 +02:00
2837999a5b Bufix, i guess 2020-09-03 20:11:02 +02:00
bf07795f4d Wrong file 2020-09-03 20:07:11 +02:00
015cd768e1 Tests for the place model 2020-09-03 20:06:06 +02:00
8ab0e1b177 only submitted can delete tags 2020-09-02 00:25:31 +02:00
47718ce17b Merge branch 'feature/tags' of mowoe.com:reverend/lostplaces-backend into feature/tags 2020-09-02 00:08:04 +02:00
6d89bca033 Deletion of tags 2020-09-02 00:08:00 +02:00
69f0f4ccfd Added documentation, links, missing information. 2020-09-01 22:35:39 +02:00
75f8ac4a95 Mentioned new taggit dependency in Readme.md 2020-09-01 22:24:05 +02:00
c3eede548c Added comment ot POST-only url. 2020-09-01 22:14:19 +02:00
0eea31a0af Removed obsolete jquery files. 2020-09-01 22:14:06 +02:00
2509c669f9 Merge branch 'feature/tags' of mowoe.com:reverend/lostplaces-backend into feature/tags 2020-09-01 21:33:07 +02:00
e00c9318fa Tagify dropdown styling 2020-09-01 18:31:46 +02:00
6ed6c2c990 Better Tagggin integration 2020-09-01 18:20:02 +02:00
328e6899a6 Merge branch 'master' into feature/tags 2020-08-31 18:28:21 +02:00
e4cd8bb301 Tagging using JS using Partials 2020-08-31 18:18:24 +02:00
66581a9d2d Adding tags is now possible 2020-08-30 18:39:45 +02:00
490a0e9f3e Merge branch 'master' into feature/tags 2020-08-30 17:53:34 +02:00
fc39b46c52 Merge branch 'master' into feature/tags 2020-08-27 17:17:39 +02:00
36744c65fb Merge branch 'master' into feature/tags 2020-08-27 17:16:20 +02:00
8198c43451 Adding taggit to the dependencies 2020-08-27 17:00:41 +02:00
78f0e80136 First tries on using tags 2020-08-27 15:46:45 +02:00
24 changed files with 1231 additions and 135 deletions

View File

@ -5,13 +5,14 @@ verify_ssl = true
[dev-packages] [dev-packages]
pylint = "*" pylint = "*"
coverage = "*"
[packages] [packages]
django = "*" django = "*"
easy-thumbnails = "*" easy-thumbnails = "*"
image = "*" image = "*"
django-widget-tweaks = "*" django-widget-tweaks = "*"
django-svg-icons = "*" django-taggit = "*"
# Commented out to not explicitly specify Python 3 subversion. # Commented out to not explicitly specify Python 3 subversion.
# [requires] # [requires]

View File

@ -11,12 +11,13 @@ Right now it depends on the following non-core Python 3 libraries. These can be
* [easy-thumbnails](https://github.com/SmileyChris/easy-thumbnails) A powerful, yet easy to implement thumbnailing application for Django 1.11+ * [easy-thumbnails](https://github.com/SmileyChris/easy-thumbnails) A powerful, yet easy to implement thumbnailing application for Django 1.11+
* [image](https://github.com/francescortiz/image) Image cropping for django * [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-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.
## Development ## Development
### Setting up a (pipenv) virtual environment for development ### Setting up a (pipenv) virtual environment for development
After having obtained the repository contents (either via .zip download or git clone), you can easily setup a pipenv virtual environment. The repo provides a Pipfile for easy dependency management that does not mess with your system. After having obtained the repository contents (either via .zip download or git clone), you can easily setup a [pipenv](https://docs.pipenv.org/) virtual environment. The repo provides a Pipfile for easy dependency management that does not mess with your system.
``` ```
$ cd lostplaces-backend $ cd lostplaces-backend
@ -25,14 +26,17 @@ $ pipenv shell
(lostplaces-backend) $ lostplaces/manage.py makemigrations (lostplaces-backend) $ lostplaces/manage.py makemigrations
(lostplaces-backend) $ lostplaces/manage.py migrate (lostplaces-backend) $ lostplaces/manage.py migrate
(lostplaces-backend) $ lostplaces/manage.py createsuperuser (lostplaces-backend) $ lostplaces/manage.py createsuperuser
(lostplaces-backend) $ lostplaces/manage.py runserver (lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
``` ```
### Returning to the venv ### Returning to the venv
``` ```
$ cd lostplaces-backend $ cd lostplaces-backend
$ pipenv shell $ pipenv shell
(lostplaces-backend) $ lostplaces/manage.py runserver (lostplaces-backend) $ pipenv update # If dependencies changed, or updates available
(lostplaces-backend) $ lostplaces/manage.py makemigrations # If datamodels changed
(lostplaces-backend) $ lostplaces/manage.py migrate # If datamodels changed
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
``` ```
Visit: [admin](http://localhost:8000/admin) for administrative backend or Visit: [admin](http://localhost:8000/admin) for administrative backend or
@ -41,16 +45,15 @@ Visit: [admin](http://localhost:8000/admin) for administrative backend or
## Installing lostplaces ## Installing lostplaces
### Install dependencies ### Install dependencies
Python3, Django3, easy-thumbnails, image, django-widget-tweaks Python3, Django3, easy-thumbnails, image, django-widget-tweaks, django-taggit
``` ```
pip install --user django easy-thumbnails image django-widget-tweaks pip install --user django easy-thumbnails image django-widget-tweaks django-taggit
``` ```
Or, if you use pipenv Or, if you use pipenv
``` ```
pipenv install pipenv install / update
``` ```
### Add 'lostplaces_app' to your INSTALLED_APPS setting like this ### Add 'lostplaces_app' to your INSTALLED_APPS setting like this
``` ```
@ -59,6 +62,7 @@ INSTALLED_APPS = [
'lostplaces_app', 'lostplaces_app',
'easy_thumbnails', 'easy_thumbnails',
'widget_tweaks', 'widget_tweaks',
'django_taggit'
] ]
``` ```
@ -92,11 +96,11 @@ urlpatterns = [
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
``` ```
Run ``python manage.py migrate`` to create the lost places models. Run ``./manage.py migrate`` to create the lost places models.
Start the development server and visit http://127.0.0.1:8000/admin/ Start the development server and visit http://localhost:8000/admin/
Visit http://127.0.0.1:8000/lostplaces/ to CRUD lost places. Visit http://localhost:8000/lostplaces/ to CRUD lost places.
Happy developing ;-) Happy developing ;-)

View File

@ -29,7 +29,7 @@ SECRET_KEY = 'n$(bx8(^)*wz1ygn@-ekt7rl^1km*!_c+fwwjiua8m@-x_rpl0'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = ['localhost', '192.168.178.49']
# Application definition # Application definition
@ -43,7 +43,8 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'easy_thumbnails', 'easy_thumbnails',
'widget_tweaks' 'widget_tweaks',
'taggit'
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -48,3 +48,11 @@ class PlaceImageCreateForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['filename'].required = False self.fields['filename'].required = False
class TagSubmitForm(forms.Form):
tag_list = forms.CharField(
max_length=500,
required=False,
widget=forms.TextInput(attrs={'autocomplete':'off'})
)

View File

@ -9,7 +9,9 @@ import uuid
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.validators import MaxValueValidator, MinValueValidator
from easy_thumbnails.fields import ThumbnailerImageField from easy_thumbnails.fields import ThumbnailerImageField
from taggit.managers import TaggableManager
# Create your models here. # Create your models here.
@ -51,10 +53,21 @@ class Place (models.Model):
related_name='places' related_name='places'
) )
location = models.CharField(max_length=50) location = models.CharField(max_length=50)
latitude = models.FloatField() latitude = models.FloatField(
longitude = models.FloatField() validators=[
MinValueValidator(-90),
MaxValueValidator(90)
]
)
longitude = models.FloatField(
validators=[
MinValueValidator(-180),
MaxValueValidator(180)
]
)
description = models.TextField() description = models.TextField()
tags = TaggableManager(blank=True)
# Get center position of LP-geocoordinates. # Get center position of LP-geocoordinates.
def average_latlon(place_list): def average_latlon(place_list):
@ -95,6 +108,7 @@ class PlaceImage (models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='images' related_name='images'
) )
submitted_when = models.DateTimeField(auto_now_add=True, null=True)
submitted_by = models.ForeignKey( submitted_by = models.ForeignKey(
Explorer, Explorer,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -145,21 +159,21 @@ def auto_delete_file_on_change(sender, instance, **kwargs):
class ExternalLink(models.Model): class ExternalLink(models.Model):
url = models.URLField(max_length=200) url = models.URLField(max_length=200)
label = models.CharField(max_length=100) label = models.CharField(max_length=100)
submitted_by = models.ForeignKey( submitted_by = models.ForeignKey(
Explorer, Explorer,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
related_name='external_links' related_name='external_links'
) )
submitted_when = models.DateTimeField(auto_now_add=True, null=True) submitted_when = models.DateTimeField(auto_now_add=True, null=True)
class PhotoAlbum(ExternalLink): class PhotoAlbum(ExternalLink):
place = models.ForeignKey( place = models.ForeignKey(
Place, Place,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='photo_albums', related_name='photo_albums',
null=True null=True
) )

View File

@ -0,0 +1,548 @@
:root {
--tagify-dd-color-primary: rgb(53, 149, 246);
--tagify-dd-bg-color: white
}
.tagify {
--tags-border-color: #DDD;
--tags-hover-border-color: #CCC;
--tags-focus-border-color: #3595f6;
--tag-bg: #E5E5E5;
--tag-hover: #D3E2E2;
--tag-text-color: black;
--tag-text-color--edit: black;
--tag-pad: 0.3em 0.5em;
--tag-inset-shadow-size: 1.1em;
--tag-invalid-color: #D39494;
--tag-invalid-bg: rgba(211, 148, 148, 0.5);
--tag-remove-bg: rgba(211, 148, 148, 0.3);
--tag-remove-btn-color: black;
--tag-remove-btn-bg: none;
--tag-remove-btn-bg--hover: #c77777;
--input-color: inherit;
--tag--min-width: 1ch;
--tag--max-width: auto;
--tag-hide-transition: .3s;
--placeholder-color: rgba(0, 0, 0, 0.4);
--placeholder-color-focus: rgba(0, 0, 0, 0.25);
--loader-size: .8em;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
border: 1px solid #ddd;
border: 1px solid var(--tags-border-color);
padding: 0;
line-height: 1.1;
cursor: text;
outline: 0;
position: relative;
transition: .1s
}
@keyframes tags--bump {
30% {
transform: scale(1.2)
}
}
@keyframes rotateLoader {
to {
transform: rotate(1turn)
}
}
.tagify:hover {
border-color: #ccc;
border-color: var(--tags-hover-border-color)
}
.tagify.tagify--focus {
transition: 0s;
border-color: #3595f6;
border-color: var(--tags-focus-border-color)
}
.tagify[readonly]:not(.tagify--mix) {
cursor: default
}
.tagify[readonly]:not(.tagify--mix)>.tagify__input {
visibility: hidden;
width: 0;
margin: 5px 0
}
.tagify[readonly]:not(.tagify--mix) .tagify__tag__removeBtn {
display: none
}
.tagify[readonly]:not(.tagify--mix) .tagify__tag>div {
padding: .3em .5em;
padding: var(--tag-pad)
}
.tagify[readonly]:not(.tagify--mix) .tagify__tag>div::before {
background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/5px 5px;
box-shadow: none;
filter: brightness(.95)
}
.tagify--loading .tagify__input::before {
content: none
}
.tagify--loading .tagify__input::after {
content: '';
vertical-align: middle;
opacity: 1;
width: .7em;
height: .7em;
width: var(--loader-size);
height: var(--loader-size);
border: 3px solid;
border-color: #eee #bbb #888 transparent;
border-radius: 50%;
animation: rotateLoader .4s infinite linear;
margin: -2px 0 -2px .5em
}
.tagify--loading .tagify__input:empty::after {
margin-left: 0
}
.tagify+input,
.tagify+textarea {
display: none !important
}
.tagify__tag {
display: inline-flex;
align-items: center;
margin: 5px 0 5px 5px;
position: relative;
z-index: 1;
outline: 0;
cursor: default;
transition: .13s ease-out
}
.tagify__tag>div {
vertical-align: top;
box-sizing: border-box;
max-width: 100%;
padding: .3em .5em;
padding: var(--tag-pad);
color: #000;
color: var(--tag-text-color);
line-height: inherit;
border-radius: 3px;
-webkit-user-select: none;
user-select: none;
white-space: nowrap;
transition: .13s ease-out
}
.tagify__tag>div>* {
white-space: pre-wrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
vertical-align: top;
min-width: var(--tag--min-width);
max-width: var(--tag--max-width);
transition: .8s ease, .1s color
}
.tagify__tag>div>[contenteditable] {
outline: 0;
-webkit-user-select: text;
user-select: text;
cursor: text;
margin: -2px;
padding: 2px;
max-width: 350px
}
.tagify__tag>div::before {
content: '';
position: absolute;
border-radius: inherit;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
transition: 120ms ease;
animation: tags--bump .3s ease-out 1;
box-shadow: 0 0 0 1.1em #e5e5e5 inset;
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-bg) inset
}
.tagify__tag:hover:not([readonly]) div::before {
top: -2px;
right: -2px;
bottom: -2px;
left: -2px;
box-shadow: 0 0 0 1.1em #d3e2e2 inset;
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-hover) inset
}
.tagify__tag--loading {
pointer-events: none
}
.tagify__tag--loading .tagify__tag__removeBtn {
display: none
}
.tagify__tag--loading::after {
--loader-size: .4em;
content: '';
vertical-align: middle;
opacity: 1;
width: .7em;
height: .7em;
width: var(--loader-size);
height: var(--loader-size);
border: 3px solid;
border-color: #eee #bbb #888 transparent;
border-radius: 50%;
animation: rotateLoader .4s infinite linear;
margin: 0 .5em 0 -.1em
}
.tagify__tag--flash div::before {
animation: none
}
.tagify__tag--hide {
width: 0 !important;
padding-left: 0;
padding-right: 0;
margin-left: 0;
margin-right: 0;
opacity: 0;
transform: scale(0);
transition: .3s;
transition: var(--tag-hide-transition);
pointer-events: none
}
.tagify__tag.tagify--noAnim>div::before {
animation: none
}
.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div>span {
opacity: .5
}
.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div::before {
box-shadow: 0 0 0 1.1em rgba(211, 148, 148, .5) inset !important;
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-invalid-bg) inset !important;
transition: .2s
}
.tagify__tag[readonly] .tagify__tag__removeBtn {
display: none
}
.tagify__tag[readonly]>div::before {
background: linear-gradient(45deg, var(--tag-bg) 25%, transparent 25%, transparent 50%, var(--tag-bg) 50%, var(--tag-bg) 75%, transparent 75%, transparent) 0/5px 5px;
box-shadow: none;
filter: brightness(.95)
}
.tagify__tag--editable>div {
color: #000;
color: var(--tag-text-color--edit)
}
.tagify__tag--editable>div::before {
box-shadow: 0 0 0 2px #d3e2e2 inset !important;
box-shadow: 0 0 0 2px var(--tag-hover) inset !important
}
.tagify__tag--editable.tagify--invalid>div::before {
box-shadow: 0 0 0 2px #d39494 inset !important;
box-shadow: 0 0 0 2px var(--tag-invalid-color) inset !important
}
.tagify__tag__removeBtn {
order: 5;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50px;
cursor: pointer;
font: 14px/1 Arial;
background: 0 0;
background: var(--tag-remove-btn-bg);
color: #000;
color: var(--tag-remove-btn-color);
width: 14px;
height: 14px;
margin-right: 4.66667px;
margin-left: -4.66667px;
transition: .2s ease-out
}
.tagify__tag__removeBtn::after {
content: "\00D7"
}
.tagify__tag__removeBtn:hover {
color: #fff;
background: #c77777;
background: var(--tag-remove-btn-bg--hover)
}
.tagify__tag__removeBtn:hover+div>span {
opacity: .5
}
.tagify__tag__removeBtn:hover+div::before {
box-shadow: 0 0 0 1.1em rgba(211, 148, 148, .3) inset !important;
box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-remove-bg) inset !important;
transition: .2s
}
.tagify:not(.tagify--mix) .tagify__input br {
display: none
}
.tagify:not(.tagify--mix) .tagify__input * {
display: inline;
white-space: nowrap
}
.tagify__input {
flex-grow: 1;
display: inline-block;
min-width: 110px;
margin: 5px;
padding: .3em .5em;
padding: var(--tag-pad, .3em .5em);
line-height: inherit;
position: relative;
white-space: pre-wrap;
color: inherit;
color: var(--input-color)
}
.tagify__input:empty::before {
transition: .2s ease-out;
opacity: 1;
transform: none;
display: inline-block;
width: auto
}
.tagify--mix .tagify__input:empty::before {
display: inline-block
}
.tagify__input:focus {
outline: 0
}
.tagify__input:focus::before {
transition: .2s ease-out;
opacity: 0;
transform: translatex(6px)
}
@media all and (-ms-high-contrast:none),
(-ms-high-contrast:active) {
.tagify__input:focus::before {
display: none
}
}
@supports (-ms-ime-align:auto) {
.tagify__input:focus::before {
display: none
}
}
.tagify__input:focus:empty::before {
transition: .2s ease-out;
opacity: 1;
transform: none;
color: rgba(0, 0, 0, .25);
color: var(--placeholder-color-focus)
}
@-moz-document url-prefix() {
.tagify__input:focus:empty::after {
display: none
}
}
.tagify__input::before {
content: attr(data-placeholder);
height: 1em;
line-height: 1em;
margin: auto 0;
z-index: 1;
color: rgba(0, 0, 0, .4);
color: var(--placeholder-color);
white-space: nowrap;
pointer-events: none;
opacity: 0;
position: absolute
}
.tagify--mix .tagify__input::before {
display: none;
position: static;
line-height: inherit
}
.tagify__input::after {
content: attr(data-suggest);
display: inline-block;
white-space: pre;
color: #000;
opacity: .3;
pointer-events: none;
max-width: 100px
}
.tagify__input .tagify__tag {
margin: 0
}
.tagify__input .tagify__tag>div {
padding-top: 0;
padding-bottom: 0
}
.tagify--mix {
display: block
}
.tagify--mix .tagify__input {
padding: 5px;
margin: 0;
width: 100%;
height: 100%;
line-height: 1.5
}
.tagify--mix .tagify__input::before {
height: auto
}
.tagify--mix .tagify__input::after {
content: none
}
.tagify--select::after {
content: '>';
opacity: .5;
position: absolute;
top: 50%;
right: 0;
bottom: 0;
font: 16px monospace;
line-height: 8px;
height: 8px;
pointer-events: none;
transform: translate(-150%, -50%) scaleX(1.2) rotate(90deg);
transition: .2s ease-in-out
}
.tagify--select[aria-expanded=true]::after {
transform: translate(-150%, -50%) rotate(270deg) scaleY(1.2)
}
.tagify--select .tagify__tag {
position: absolute;
top: 0;
right: 1.8em;
bottom: 0
}
.tagify--select .tagify__tag div {
display: none
}
.tagify--select .tagify__input {
width: 100%
}
.tagify--invalid {
--tags-border-color: #D39494
}
.tagify__dropdown {
position: absolute;
z-index: 9999;
transform: translateY(1px);
overflow: hidden
}
.tagify__dropdown[placement=top] {
margin-top: 0;
transform: translateY(-100%)
}
.tagify__dropdown[placement=top] .tagify__dropdown__wrapper {
border-top-width: 1px;
border-bottom-width: 0
}
.tagify__dropdown[position=text] {
box-shadow: 0 0 0 3px rgba(var(--tagify-dd-color-primary), .1);
font-size: .9em
}
.tagify__dropdown[position=text] .tagify__dropdown__wrapper {
border-width: 1px
}
.tagify__dropdown__wrapper {
max-height: 300px;
overflow: hidden;
background: #fff;
background: var(--tagify-dd-bg-color);
border: 1px solid #3595f6;
border-color: var(--tagify-dd-color-primary);
border-top-width: 0;
box-shadow: 0 2px 4px -2px rgba(0, 0, 0, .2);
transition: .25s cubic-bezier(0, 1, .5, 1)
}
.tagify__dropdown__wrapper:hover {
overflow: auto
}
.tagify__dropdown--initial .tagify__dropdown__wrapper {
max-height: 20px;
transform: translateY(-1em)
}
.tagify__dropdown--initial[placement=top] .tagify__dropdown__wrapper {
transform: translateY(2em)
}
.tagify__dropdown__item {
box-sizing: inherit;
padding: .3em .5em;
margin: 1px;
cursor: pointer;
border-radius: 2px;
position: relative;
outline: 0
}
.tagify__dropdown__item--active {
background: #3595f6;
background: var(--tagify-dd-color-primary);
color: #fff
}
.tagify__dropdown__item:active {
filter: brightness(105%)
}

File diff suppressed because one or more lines are too long

View File

@ -20,7 +20,7 @@
<b>We do not change anything in the location.</b> <b>We do not change anything in the location.</b>
</li> </li>
<li> <li>
We don't smoke if possible. Not only, because it smells bad and causes litter, there is always the chance, to set anything on fire with flying sparks. Let it be dry leaves on a hot summer day or (poentially) flammable materials in industrial plants. <b>We don't smoke if possible.</b> Not only, because it smells bad and causes litter, there is always the chance, to set anything on fire with flying sparks. Let it be dry leaves on a hot summer day or (poentially) flammable materials in industrial plants.
</li> </li>
<li> <li>
<b>Spraying is an absolute "no-go"!</b> <b>Spraying is an absolute "no-go"!</b>

View File

@ -3,19 +3,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> {% block additional_head %}
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% endblock additional_head %}
<link rel="stylesheet" href="{% static 'main.css' %}">
<link rel="icon" type="image/png" href="{% static 'favicon.ico' %}">
<title>
{% block title %}Urban Exploration{% endblock %}
</title>
{% block additional_head %} <meta charset="UTF-8">
{% endblock additional_head %} <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{% static 'main.css' %}">
</head> <link rel="icon" type="image/png" href="{% static 'favicon.ico' %}">
<title>
{% block title %}Urban Exploration{% endblock %}
</title>
</head>
<body> <body>
<div class="LP-Wrapper__Site"> <div class="LP-Wrapper__Site">

View File

@ -1,16 +1,18 @@
{% load widget_tweaks %} {% load widget_tweaks %}
<div class="LP-Input {% if field.errors %} LP-Input--error {% endif %}"> <div class="LP-Input {% if classes%}{{classes}}{% endif %} {% if field.errors %} LP-Input--error {% endif %}">
<label for="{{field.id_for_label}}" class="LP-Input__Label">{{field.label}}</label> <label for="{{field.id_for_label}}" class="LP-Input__Label">{{field.label}}</label>
{% render_field field class="LP-Input__Field"%} {% with class="LP-Input__Field "%}
{% render_field field class=class%}
{% endwith %}
<span class="LP-Input__Message"> <span class="LP-Input__Message">
{% if field.errors %} {% if field.errors %}
{% for error in field.errors%} {% for error in field.errors%}
{{error}} {{error}}
{% endfor %} {% endfor %}
{% elif field.help_text%} {% elif field.help_text%}
{{ field.help_text }} {{ field.help_text }}
{% endif %} {% endif %}
</span> </span>
</div> </div>

View File

@ -0,0 +1,66 @@
<div class="LP-TagList">
<ul class="LP-TagList__List">
{% for tag in tag_list %}
<li class="LP-TagList__Item">
<div class="LP-Tag">
<a href="#" class="LP-Link">
<span class="LP-Link__Text">{{tag}}</span>
</a>
{% if request.user and request.user == config.tagged_item.submitted_by %}
<a href="{% url config.delete_url_name tagged_id=config.tagged_item.id tag_id=tag.id %}" class="LP-Link">
<span class="LP-Tag__Remove RV-Iconized__Container RV-Iconized__Container--extraSmall">
<svg class="RV-Iconized__Icon" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M19 6.4L17.6 5 12 10.6 6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12z">
</path>
</svg>
</span>
</a>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
</div>
<form id="id_tag_submit_form" class="LP-Form LP-Form--inline LP-Form--tagging" method="POST" action="{{config.submit_url}}">
<fieldset class="LP-Form__Fieldset">
<legend class="LP-Form__Legend">Tags hinzufügen</legend>
{% csrf_token %}
<div class="LP-Form__Composition LP-Form__Composition--breakable">
<div class="LP-Form__Field LP-Form__Button LP-Input LP-Input--tagging">
<button id="id_tag_submit_button" class="LP-Button"> Tags hinzufügen</button>
</div>
<div class="LP-Form__Field">
{% include 'partials/form/inputField.html' with field=config.submit_form.tag_list classes="LP-Input--tagging" %}
</div>
</div>
</fieldset>
</form>
<script>
const input = document.getElementById('{{config.submit_form.tag_list.auto_id}}')
const submit_form = document.getElementById('id_tag_submit_form')
const submit_button = document.getElementById('id_tag_submit_button')
submit_form.onsubmit = () => false
const tagify = new Tagify(input, {
'whitelist': [
{% for tag in all_tags %}
'{{tag}}',
{% endfor %}
]
})
const on_form_submit = function() {
concat_value = ''
console.log(tagify)
concat_value = tagify.value.map(value => value.value).join(',')
console.log(concat_value)
input.value = concat_value
submit_form.submit()
}
submit_button.onclick = on_form_submit
</script>

View File

@ -6,102 +6,103 @@
{% block additional_head %} {% block additional_head %}
<link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css"> <link rel="stylesheet" href="{% static 'maps/ol.css' %}" type="text/css">
<script src="{% static 'maps/ol.js' %}"></script> <script src="{% static 'maps/ol.js' %}"></script>
<script src="{% static 'tagify.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'minimal.css' %}" type="text/css">
{% endblock additional_head %} {% endblock additional_head %}
{% block title %}{{place.name}}{% endblock %} {% block title %}{{place.name}}{% endblock %}
{% block additional_menu_items %} {% block additional_menu_items %}
<li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_edit' pk=place.pk %}" class="LP-Link"><span <li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_edit' pk=place.pk %}" class="LP-Link"><span class="LP-Link__Text">Edit place</span></a></li>
class="LP-Link__Text">Edit place</span></a></li> <li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_delete' pk=place.pk %}" class="LP-Link"><span class="LP-Link__Text">Delete place</span></a></li>
<li class="LP-Menu__Item LP-Menu__Item--additional"><a href="{% url 'place_delete' pk=place.pk %}" class="LP-Link"><span
class="LP-Link__Text">Delete place</span></a></li>
{% endblock additional_menu_items %} {% endblock additional_menu_items %}
{% block maincontent %} {% block maincontent %}
<article class="LP-PlaceDetail"> <article class="LP-PlaceDetail">
<header class="LP-PlaceDetail__Header"> <header class="LP-PlaceDetail__Header">
<h1 class="LP-Headline">{{ place.name }}</h1> <h1 class="LP-Headline">{{ place.name }}</h1>
{% if place.images.first.filename.hero.url %} {% if place.images.first.filename.hero.url %}
<figure class="LP-PlaceDetail__Image"> <figure class="LP-PlaceDetail__Image">
<img src="{{ place.images.first.filename.hero.url }}" class="LP-Image" /> <img src="{{ place.images.first.filename.hero.url }}" class="LP-Image" />
</figure> </figure>
{% endif %} {% endif %}
</header> </header>
<div class="LP-PlaceDetail__Description"> <div class="LP-PlaceDetail__Description">
<p class="LP-Paragraph">{{ place.description }}</p> <p class="LP-Paragraph">{{ place.description }}</p>
</div> </div>
<section class="LP-Section"> <section class="LP-Section">
<h1 class="LP-Headline">Map-Links</h1>
{% include 'partials/osm_map.html' %}
<div class="LP-LinkList">
<ul class="LP-LinkList__Container">
<li class="LP-LinkList__Item"><a target="_blank"
href="https://www.google.com/maps?q={{place.latitude}},{{place.longitude}}"
class="LP-Link"><span class="LP-Text">Google Maps</span></a></li>
<li class="LP-LinkList__Item"><a target="_blank"
href="https://www.tim-online.nrw.de/tim-online2/?center={{place.latitude}},{{place.longitude}}&icon=true&bg=dop"
class="LP-Link"><span class="LP-Text">TIM Online</span></a></li>
<li class="LP-LinkList__Item"><a target="_blank"
href="http://www.openstreetmap.org/?mlat={{place.latitude}}&mlon={{place.longitude}}&zoom=16"
class="LP-Link"><span class="LP-Text">OSM</span></a></li>
</ul>
</div>
</section>
<section class="LP-Section"> {% url 'place_tag_submit' place_id=place.id as tag_submit_url%}
<h1 class="LP-Headline">Photoalben</h1> {% include 'partials/tagging.html' with tag_list=place.tags.all config=tagging_config all_tags=all_tags %}
<div class="LP-LinkList">
<ul class="LP-LinkList__Container"> </section>
{% for photo_album in place.photo_albums.all %}
<li class="LP-LinkList__Item"> <section class="LP-Section">
<a target="_blank" href="{{photo_album.url}}" class="LP-Link"> <h1 class="LP-Headline">Map-Links</h1>
<span class="LP-Text">{{photo_album.label}}</span> {% include 'partials/osm_map.html' %}
</a> <div class="LP-LinkList">
{% if user == photo_album.submitted_by or user == place.submitted_by %} <ul class="LP-LinkList__Container">
<a href="{% url 'photo_album_delete' pk=photo_album.pk%}" class="LP-Link LP-LinkList__ItemHover" title="Delete Photo Album"> <li class="LP-LinkList__Item"><a target="_blank" href="https://www.google.com/maps?q={{place.latitude}},{{place.longitude}}" class="LP-Link"><span class="LP-Text">Google Maps</span></a></li>
<div class="RV-Iconized__Container RV-Iconized__Container--small"> <li class="LP-LinkList__Item"><a target="_blank" href="https://www.tim-online.nrw.de/tim-online2/?center={{place.latitude}},{{place.longitude}}&icon=true&bg=dop" class="LP-Link"><span class="LP-Text">TIM Online</span></a></li>
{% icon 'trash' className="RV-Iconized__Icon" %} <li class="LP-LinkList__Item"><a target="_blank" href="http://www.openstreetmap.org/?mlat={{place.latitude}}&mlon={{place.longitude}}&zoom=16" class="LP-Link"><span class="LP-Text">OSM</span></a></li>
</div> </ul>
</a> </div>
{% endif %} </section>
</li>
{% endfor %} <section class=" LP-Section">
<li class="LP-LinkList__Item"> <h1 class="LP-Headline">Photoalben</h1>
<a href="{% url 'photo_album_create' place_id=place.id %}" class="LP-Link"> <div class="LP-LinkList">
<div class="RV-Iconized__Container RV-Iconized__Container--small"> <ul class="LP-LinkList__Container">
<svg class="RV-Iconized__Icon" version="1.1" id="Capa_1" {% for photo_album in place.photo_albums.all %}
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" <li class="LP-LinkList__Item">
x="0px" y="0px" viewBox="0 0 512 512" xml:space="preserve"> <a target="_blank" href="{{photo_album.url}}" class="LP-Link">
<g> <span class="LP-Text">{{photo_album.label}}</span>
<path d="M492,236H276V20c0-11.046-8.954-20-20-20c-11.046,0-20,8.954-20,20v216H20c-11.046,0-20,8.954-20,20s8.954,20,20,20h216 </a>
{% if user == photo_album.submitted_by or user == place.submitted_by %}
<a href="{% url 'photo_album_delete' pk=photo_album.pk%}" class="LP-Link LP-LinkList__ItemHover" title="Delete Photo Album">
<div class="RV-Iconized__Container RV-Iconized__Container--small">
{% icon 'trash' className="RV-Iconized__Icon" %}
</div>
</a>
{% endif %}
</li>
{% endfor %}
<li class="LP-LinkList__Item">
<a href="{% url 'photo_album_create' place_id=place.id %}" class="LP-Link">
<div class="RV-Iconized__Container RV-Iconized__Container--small">
<svg class="RV-Iconized__Icon" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" xml:space="preserve">
<g>
<path d="M492,236H276V20c0-11.046-8.954-20-20-20c-11.046,0-20,8.954-20,20v216H20c-11.046,0-20,8.954-20,20s8.954,20,20,20h216
v216c0,11.046,8.954,20,20,20s20-8.954,20-20V276h216c11.046,0,20-8.954,20-20C512,244.954,503.046,236,492,236z" /> v216c0,11.046,8.954,20,20,20s20-8.954,20-20V276h216c11.046,0,20-8.954,20-20C512,244.954,503.046,236,492,236z" />
</g> </g>
</svg> </svg>
<span class="RV-Iconized__Text">Fotoalbum hinzufügen</span> <span class="RV-Iconized__Text">Fotoalbum hinzufügen</span>
</div> </div>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</section> </section>
<section class="LP-Section"> <section class="LP-Section">
<h1 class="LP-Headline">Bilder</h1> <h1 class="LP-Headline">Bilder</h1>
<div class="LP-ImageGrid"> <div class="LP-ImageGrid">
<ul class="LP-ImageGrid__Container"> <ul class="LP-ImageGrid__Container">
{% for place_image in place.images.all %} {% for place_image in place.images.all %}
<li class="LP-ImageGrid__Item"> <li class="LP-ImageGrid__Item">
<a href="{{ place_image.filename.large.url }}" class="LP-Link"><img class="LP-Image" <a href="{{ place_image.filename.large.url }}" class="LP-Link"><img class="LP-Image" src="{{ place_image.filename.thumbnail.url }}"></a>
src="{{ place_image.filename.thumbnail.url }}"></a> </li>
</li> {% endfor %}
{% endfor %} </ul>
</ul> </div>
</div> </section>
</section>
</article> </article>
{% endblock maincontent %} {% endblock maincontent %}

View File

@ -1,8 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
''' Tests for the lostplaces_app. '''
rom django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,13 @@
from django.db import models as django_models
from lostplaces_app.models import Explorer
def mock_user():
explorer_list = Explorer.objects.all()
if len(explorer_list) <= 0:
return Explorer.objects.create_user(
username='testpeter',
password='Develop123'
)
else:
return explorer_list[0]

View File

@ -0,0 +1,121 @@
from django.db import models
class TestModel:
'''
Base class for Lostplaces models
'''
model_name = None
def _test_field(self, field_name, field_class):
'''
Tests if a field exists under the given name and
if the field is of the right type
'''
field = self.object._meta.get_field(field_name)
self.assertTrue(
field,
msg="%s has no field named '%s'" % (
self.model_name,
field_name
)
)
self.assertEqual(
type(field), field_class,
msg='%s.%s name field is no CharField' % (
self.model_name,
field_name
)
)
return field
def _test_char_field(self, field_name, min_length, max_length):
'''
Tests if the given field is a char field and if its max_length
is in min_length and max_legth
'''
field = self._test_field(field_name, models.CharField)
self.assertEqual(
type(field), models.CharField,
msg='%s.%s name field is no CharField' % (
self.model_name,
field_name
)
)
self.assertTrue(
field.max_length in range(min_length, max_length),
msg='%s.%s field max_length not in range of %d and %d' % (
self.model_name,
field_name,
min_length,
max_length
)
)
def _test_float_field(self, field_name, min_value=None, max_value=None):
'''
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._test_field(field_name, models.FloatField)
if min_value:
self.assertTrue(
len(field.validators) >= 1,
msg='%s.%s first validator should check minimum' % (
self.model_name,
field_name
)
)
self.assertEqual(
field.validators[0].limit_value,
min_value,
msg='%s.%s min value missmatch' % (
self.model_name,
field_name
)
)
if max_value:
index = 0
if min_value:
index += 1
self.assertTrue(
len(field.validators) >= index+1,
msg='%s.%s second validator should check maximum' % (
self.model_name,
field_name
)
)
self.assertEqual(
field.validators[1].limit_value,
max_value,
msg='%s.%s max value missmatch' % (
self.model_name,
field_name
)
)
class TestSubmittable(TestModel):
model_name='<Class>'
related_name = None
nullable = False
def test_submitted_when(self):
submitted_when = self._test_field('submitted_when', models.DateTimeField)
self.assertTrue(submitted_when.auto_now_add,
msg='%s.submitted_when should be auto_now_add' % (
self.model_name
)
)
def test_submitted_by(self):
submitted_by = self._test_field('submitted_by',models.ForeignKey)
if self.related_name:
self.assertEqual(submitted_by.remote_field.related_name, self.related_name)
if self.nullable:
self.assertTrue(submitted_by.null,)
self.assertTrue(submitted_by.blank)
self.assertEqual(submitted_by.remote_field.on_delete, models.SET_NULL)

View File

@ -0,0 +1,55 @@
import datetime
from unittest import mock
from django.test import TestCase
from django.db import models
from django.core.files import File
from lostplaces_app.models import PlaceImage
from lostplaces_app.tests.models import TestSubmittable
from lostplaces_app.tests import mock_user
from lostplaces_app.tests.models import TestModel
from lostplaces_app.tests.models.test_place_model import mock_place
from easy_thumbnails.fields import ThumbnailerImageField
def mock_place_image():
return PlaceImage(
description='Im a description',
filename=mock.MagicMock(spec=File, name='FileMock'),
place=mock_place(),
submitted_when=datetime.datetime.now(),
submitted_by=mock_user()
)
class TestPlaceImage(TestSubmittable, TestCase):
model_name = 'PlaceImage'
def setUp(self):
self.object = mock_place_image()
def test_description(self):
self._test_field('description', models.TextField)
def test_filename(self):
self._test_field('filename',ThumbnailerImageField)
def test_place(self):
field = self._test_field('place', models.ForeignKey)
self.assertEqual(field.remote_field.on_delete, models.CASCADE,
msg='%s.%s deleting of %s should be cascadinf' % (
self.model_name,
'place',
self.model_name
)
)
self.assertEqual(field.remote_field.related_name, 'images',
msg='%s.%s related name should be images' % (
self.model_name,
'place'
)
)
def test_str(self):
place_image = mock_place_image()
self.assertEqual(str(place_image), ' '.join([place_image.place.name, str(place_image.pk)]))

View File

@ -0,0 +1,136 @@
import datetime
from django.test import TestCase
from django.db import models
from lostplaces_app.models import Place
from lostplaces_app.tests.models import TestSubmittable
from lostplaces_app.tests import mock_user
from lostplaces_app.tests.models import TestModel
from taggit.managers import TaggableManager
def mock_place():
place = Place.objects.create(
name='Im a place',
submitted_when=datetime.datetime.now(),
submitted_by=mock_user(),
location='Testtown',
latitude=50.5,
longitude=7.0,
description='This is just a test, do not worry'
)
place.tags.add('I a tag', 'testlocation')
return place
class PlaceTestCase(TestSubmittable, TestCase):
model_name = 'Place'
related_name = 'places'
nullable = True
def setUp(self):
self.place = mock_place()
self.object = self.place
def test_name_field(self):
self._test_char_field(
field_name='name',
min_length=10,
max_length=100
)
def test_location(self):
self._test_char_field(
field_name='location',
min_length=10,
max_length=100
)
def test_latitude(self):
self._test_float_field(
field_name='latitude',
min_value=-90,
max_value=90
)
def test_longitude(self):
self._test_float_field(
field_name='longitude',
min_value=-180,
max_value=180
)
def test_decsription(self):
self._test_field('description', models.TextField)
def test_tags(self):
self._test_field('tags', TaggableManager)
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 = mock_place()
place.latitude = i+1
place.longitude = i+10
place_list.append(place)
avg_latlon = Place.average_latlon(place_list)
self.assertEqual(avg_latlon[0], 5.5,
msg='%s: average latitude missmatch' % (
self.model_name
)
)
self.assertEqual(avg_latlon[1], 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 = mock_place()
avg_latlon = Place.average_latlon([place])
self.assertEqual(avg_latlon[0], place.latitude,
msg='%s:(one place) average latitude missmatch' % (
self.model_name
)
)
self.assertEqual(avg_latlon[1], 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[0], 0,
msg='%s: (no places) average latitude missmatch' % (
self.model_name
)
)
self.assertEqual(avg_latlon[1], 0,
msg='%s: a(no places) verage longitude missmatch' % (
self.model_name
)
)
def test_str(self):
place = mock_place()
self.assertEqual(str(place), place.name,
msg='%s __str__ should return the name' % (
self.model_name
)
)

View File

@ -0,0 +1,58 @@
from django.test import TestCase, Client
from django.urls import reverse_lazy
from lostplaces_app.models import Place
from lostplaces_app.models import Explorer
from lostplaces_app.tests.models.test_place_model import mock_place
from lostplaces_app.tests import mock_user
class TestIsAuthenticated(TestCase):
def setUp(self):
self. client = Client()
mock_place()
mock_user()
def test_logged_in(self):
self.client.login(username='testpeter', password='Develop123')
response = self.client.get(reverse_lazy('place_detail', kwargs={'pk': 1}))
self.assertEqual(response.status_code, 200)
def test_not_logged_in(self):
url = reverse_lazy('place_detail', kwargs={'pk': 1})
response = self.client.get(url, follow=True)
self.assertRedirects(
response=response,
expected_url='?'.join([str(reverse_lazy('login')), 'redirect_to=/place/1/']),
status_code=302,
target_status_code=200,
msg_prefix='''Accesing an IsAuthenticated view while not logged should
redirect to login page with redirect params
''',
fetch_redirect_response=True
)
self.assertTrue(response.context['messages'])
self.assertTrue(len(response.context['messages']) > 0)
class TestIsPlaceSubmitter(TestCase):
def setUp(self):
self. client = Client()
mock_place()
mock_user()
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):
Explorer.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

@ -0,0 +1,35 @@
from django.test import TestCase, Client
from django.urls import reverse_lazy
from lostplaces_app.models import Place
from lostplaces_app.tests.models.test_place_model import mock_place
from lostplaces_app.tests import mock_user
class TestPlaceCreateView(TestCase):
def setUp(self):
self. client = Client()
mock_place()
mock_user()
def test_url_logged_in(self):
self.client.login(username='testpeter', password='Develop123')
response = self.client.get(reverse_lazy('place_detail', kwargs={'pk': 1}))
self.assertEqual(response.status_code, 200)
def test_url_not_logged_in(self):
url = reverse_lazy('place_detail', kwargs={'pk': 1})
response = self.client.get(url)
self.assertRedirects(
response=response,
expected_url='?'.join([str(reverse_lazy('login')), 'redirect_to=/place/1/']),
status_code=302,
target_status_code=200,
msg_prefix='''Accesing PlaceDetailView while not logged should
redirect to login page with redirect params
''',
fetch_redirect_response=True
)

View File

@ -7,6 +7,8 @@ from .views import (
PlaceCreateView, PlaceCreateView,
PlaceUpdateView, PlaceUpdateView,
PlaceDeleteView, PlaceDeleteView,
PlaceTagDeleteView,
PlaceTagSubmitView,
PhotoAlbumCreateView, PhotoAlbumCreateView,
PhotoAlbumDeleteView, PhotoAlbumDeleteView,
FlatView FlatView
@ -22,5 +24,9 @@ urlpatterns = [
path('place/update/<int:pk>/', PlaceUpdateView.as_view(), name='place_edit'), path('place/update/<int:pk>/', PlaceUpdateView.as_view(), name='place_edit'),
path('place/delete/<int:pk>/', PlaceDeleteView.as_view(), name='place_delete'), path('place/delete/<int:pk>/', PlaceDeleteView.as_view(), name='place_delete'),
path('place/', PlaceListView.as_view(), name='place_list'), path('place/', PlaceListView.as_view(), name='place_list'),
path('flat/<slug:slug>/', FlatView, name='flatpage') path('flat/<slug:slug>/', FlatView, name='flatpage'),
# POST-only URL for tag submission
path('place/tag/<int:place_id>', PlaceTagSubmitView.as_view(), name='place_tag_submit'),
path('place/tag/delete/<int:tagged_id>/<int:tag_id>', PlaceTagDeleteView.as_view(), name='place_tag_delete')
] ]

View File

@ -11,7 +11,9 @@ from django.urls import reverse_lazy
from lostplaces_app.models import Place, PlaceImage from lostplaces_app.models import Place, PlaceImage
from lostplaces_app.views import IsAuthenticated, IsPlaceSubmitter from lostplaces_app.views import IsAuthenticated, IsPlaceSubmitter
from lostplaces_app.forms import PlaceForm, PlaceImageCreateForm from lostplaces_app.forms import PlaceForm, PlaceImageCreateForm, TagSubmitForm
from taggit.models import Tag
class PlaceListView(IsAuthenticated, ListView): class PlaceListView(IsAuthenticated, ListView):
paginate_by = 5 paginate_by = 5
@ -30,7 +32,14 @@ class PlaceDetailView(IsAuthenticated, View):
context = { context = {
'place': place, 'place': place,
'place_list': [ place ], 'place_list': [ place ],
'place_map_center': [ place.latitude, place.longitude ] 'place_map_center': [ place.latitude, place.longitude ],
'all_tags': Tag.objects.all(),
'tagging_config': {
'submit_url': reverse_lazy('place_tag_submit', kwargs={'place_id': place.id}),
'submit_form': TagSubmitForm(),
'tagged_item': place,
'delete_url_name': 'place_tag_delete'
}
} }
return render(request, 'place/place_detail.html', context) return render(request, 'place/place_detail.html', context)

View File

@ -5,15 +5,19 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.contrib import messages from django.contrib import messages
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponseForbidden
from lostplaces_app.forms import ExplorerCreationForm from lostplaces_app.forms import ExplorerCreationForm, TagSubmitForm
from lostplaces_app.models import Place, PhotoAlbum from lostplaces_app.models import Place, PhotoAlbum
from lostplaces_app.views import IsAuthenticated from lostplaces_app.views.base_views import IsAuthenticated
from lostplaces_app.views.base_views import ( from lostplaces_app.views.base_views import (
PlaceAssetCreateView, PlaceAssetCreateView,
PlaceAssetDeleteView PlaceAssetDeleteView,
) )
from taggit.models import Tag
class SignUpView(SuccessMessageMixin, CreateView): class SignUpView(SuccessMessageMixin, CreateView):
form_class = ExplorerCreationForm form_class = ExplorerCreationForm
success_url = reverse_lazy('login') success_url = reverse_lazy('login')
@ -49,5 +53,27 @@ class PhotoAlbumDeleteView(PlaceAssetDeleteView):
success_message = 'Photo Album deleted' success_message = 'Photo Album deleted'
permission_denied_messsage = 'You do not have permissions to alter this photo album' permission_denied_messsage = 'You do not have permissions to alter this photo album'
class PlaceTagSubmitView(IsAuthenticated, View):
def post(self, request, place_id, *args, **kwargs):
place = Place.objects.get(pk=place_id)
form = TagSubmitForm(request.POST)
if form.is_valid():
tag_list_raw = form.cleaned_data['tag_list']
tag_list_raw = tag_list_raw.strip().split(',')
tag_list = []
for tag in tag_list_raw:
tag_list.append(tag.strip())
place.tags.add(*tag_list)
place.save()
return redirect(reverse_lazy('place_detail', kwargs={'pk': place.id}))
class PlaceTagDeleteView(IsAuthenticated, View):
def get(self, request, tagged_id, tag_id, *args, **kwargs):
place = Place.objects.get(pk=tagged_id)
tag = Tag.objects.get(pk=tag_id)
place.tags.remove(tag)
return redirect(reverse_lazy('place_detail', kwargs={'pk': tagged_id}))
def FlatView(request, slug): def FlatView(request, slug):
return render(request, 'flat/' + slug + '.html') return render(request, 'flat/' + slug + '.html')