diff --git a/Pipfile b/Pipfile index d275152..cca9ef8 100644 --- a/Pipfile +++ b/Pipfile @@ -5,13 +5,14 @@ verify_ssl = true [dev-packages] pylint = "*" +coverage = "*" [packages] django = "*" easy-thumbnails = "*" image = "*" django-widget-tweaks = "*" -django-svg-icons = "*" +django-taggit = "*" # Commented out to not explicitly specify Python 3 subversion. # [requires] diff --git a/Readme.md b/Readme.md index fe24285..02c06cc 100644 --- a/Readme.md +++ b/Readme.md @@ -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+ * [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. ## 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 @@ -25,14 +26,17 @@ $ pipenv shell (lostplaces-backend) $ lostplaces/manage.py makemigrations (lostplaces-backend) $ lostplaces/manage.py migrate (lostplaces-backend) $ lostplaces/manage.py createsuperuser -(lostplaces-backend) $ lostplaces/manage.py runserver +(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6 ``` ### Returning to the venv ``` $ cd lostplaces-backend $ 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 @@ -41,16 +45,15 @@ Visit: [admin](http://localhost:8000/admin) for administrative backend or ## Installing lostplaces ### 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 ``` -pipenv install +pipenv install / update ``` - ### Add 'lostplaces_app' to your INSTALLED_APPS setting like this ``` @@ -59,6 +62,7 @@ INSTALLED_APPS = [ 'lostplaces_app', 'easy_thumbnails', 'widget_tweaks', + 'django_taggit' ] ``` @@ -92,11 +96,11 @@ urlpatterns = [ ] + 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 ;-) diff --git a/lostplaces/lostplaces/settings.py b/lostplaces/lostplaces/settings.py index 715a4fd..46ffa59 100644 --- a/lostplaces/lostplaces/settings.py +++ b/lostplaces/lostplaces/settings.py @@ -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! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['localhost', '192.168.178.49'] # Application definition @@ -43,7 +43,8 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'easy_thumbnails', - 'widget_tweaks' + 'widget_tweaks', + 'taggit' ] MIDDLEWARE = [ diff --git a/lostplaces/lostplaces_app/forms.py b/lostplaces/lostplaces_app/forms.py index d88d090..9b97388 100644 --- a/lostplaces/lostplaces_app/forms.py +++ b/lostplaces/lostplaces_app/forms.py @@ -48,3 +48,11 @@ class PlaceImageCreateForm(forms.ModelForm): super().__init__(*args, **kwargs) self.fields['filename'].required = False + + +class TagSubmitForm(forms.Form): + tag_list = forms.CharField( + max_length=500, + required=False, + widget=forms.TextInput(attrs={'autocomplete':'off'}) + ) \ No newline at end of file diff --git a/lostplaces/lostplaces_app/models.py b/lostplaces/lostplaces_app/models.py index d513bb0..81244ad 100644 --- a/lostplaces/lostplaces_app/models.py +++ b/lostplaces/lostplaces_app/models.py @@ -9,7 +9,9 @@ import uuid from django.db import models from django.dispatch import receiver from django.contrib.auth.models import AbstractUser +from django.core.validators import MaxValueValidator, MinValueValidator from easy_thumbnails.fields import ThumbnailerImageField +from taggit.managers import TaggableManager # Create your models here. @@ -51,10 +53,21 @@ class Place (models.Model): related_name='places' ) location = models.CharField(max_length=50) - latitude = models.FloatField() - longitude = models.FloatField() + 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): @@ -95,6 +108,7 @@ class PlaceImage (models.Model): 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, @@ -145,21 +159,21 @@ def auto_delete_file_on_change(sender, instance, **kwargs): class ExternalLink(models.Model): - url = models.URLField(max_length=200) - label = models.CharField(max_length=100) - submitted_by = models.ForeignKey( + 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) + submitted_when = models.DateTimeField(auto_now_add=True, null=True) class PhotoAlbum(ExternalLink): - place = models.ForeignKey( + place = models.ForeignKey( Place, on_delete=models.CASCADE, related_name='photo_albums', - null=True + null=True ) \ No newline at end of file diff --git a/lostplaces/lostplaces_app/static/tagify.css b/lostplaces/lostplaces_app/static/tagify.css new file mode 100644 index 0000000..750119a --- /dev/null +++ b/lostplaces/lostplaces_app/static/tagify.css @@ -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%) +} \ No newline at end of file diff --git a/lostplaces/lostplaces_app/static/tagify.min.js b/lostplaces/lostplaces_app/static/tagify.min.js new file mode 100644 index 0000000..6ec4da4 --- /dev/null +++ b/lostplaces/lostplaces_app/static/tagify.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Tagify=e()}(this,(function(){"use strict";function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}function e(t,e,i){return e in t?Object.defineProperty(t,e,{value:i,enumerable:!0,configurable:!0,writable:!0}):t[e]=i,t}function i(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,s)}return i}function s(t){for(var s=1;s/g,">").replace(/"/g,""").replace(/`|'/g,"'")}function d(t,e,i){function s(t,e){for(var i in e)e.hasOwnProperty(i)&&(o(e[i])?o(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]):t[i]=e[i])}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function c(t){return String.prototype.normalize?"string"==typeof t?t.normalize("NFD").replace(/[\u0300-\u036f]/g,""):void 0:t}var h={init:function(){this.DOM.dropdown=this.parseTemplate("dropdown",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector("."+this.settings.classNames.dropdownWrapper)},show:function(t){var e,i,s,a=this,r=this.settings,l=window.getSelection(),d="mix"==r.mode&&!r.enforceWhitelist,c=!r.whitelist||!r.whitelist.length,h="manual"==r.dropdown.position;if((!c||d||r.templates.dropdownItemNoMatch)&&!1!==r.dropdown.enable&&!this.state.isLoading){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems.call(this,t),t&&!this.suggestedListItems.length&&(this.trigger("dropdown:noMatch",t),r.templates.dropdownItemNoMatch&&(s=r.templates.dropdownItemNoMatch.call(this,{value:t}))),!s){if(this.suggestedListItems.length)t&&d&&!this.state.editing.scope&&!n(this.suggestedListItems[0].value,t)&&this.suggestedListItems.unshift({value:t});else{if(!t||!d||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide.call(this);this.suggestedListItems=[{value:t}]}i=""+(o(e=this.suggestedListItems[0])?e.value:e),r.autoComplete&&i&&0==i.indexOf(t)&&this.input.autocomplete.suggest.call(this,e)}this.dropdown.fill.call(this,s),r.dropdown.highlightFirst&&this.dropdown.highlightOption.call(this,this.DOM.dropdown.content.children[0]),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=t||!0,this.state.dropdown.query=t,this.state.selection={anchorOffset:l.anchorOffset,anchorNode:l.anchorNode},h||setTimeout((function(){a.dropdown.position.call(a),a.dropdown.render.call(a)})),setTimeout((function(){a.trigger("dropdown:show",a.DOM.dropdown)}))}},hide:function(t){var e=this,i=this.DOM,s=i.scope,a=i.dropdown,n="manual"==this.settings.dropdown.position&&!t;if(a&&document.body.contains(a)&&!n)return window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),s.setAttribute("aria-expanded",!1),a.parentNode.removeChild(a),setTimeout((function(){e.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger("dropdown:hide",a),this},render:function(){var t,e,i,s=this,a=(t=this.DOM.dropdown,(i=t.cloneNode(!0)).style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(i),e=i.clientHeight,i.parentNode.removeChild(i),e),n=this.settings;return this.DOM.scope.setAttribute("aria-expanded",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(n.classNames.dropdownInital),this.dropdown.position.call(this,a),n.dropdown.appendTarget.appendChild(this.DOM.dropdown),setTimeout((function(){return s.DOM.dropdown.classList.remove(n.classNames.dropdownInital)}))),this},fill:function(t){var e;t="string"==typeof t?t:this.dropdown.createListHTML.call(this,t||this.suggestedListItems),this.DOM.dropdown.content.innerHTML=(e=t)?e.replace(/\>[\r\n ]+\<").replace(/(<.*?>)|\s+/g,(function(t,e){return e||" "})):""},refilter:function(t){t=t||this.state.dropdown.query||"",this.suggestedListItems=this.dropdown.filterListItems.call(this,t),this.suggestedListItems.length?this.dropdown.fill.call(this):this.dropdown.hide.call(this),this.trigger("dropdown:updated",this.DOM.dropdown)},position:function(t){var e,i,s,a,n,o,r,l=this.DOM.dropdown,d=document.documentElement.clientHeight,c=Math.max(document.documentElement.clientWidth||0,window.innerWidth||0)>480?this.settings.dropdown.position:"all",h=this.DOM["input"==c?"input":"scope"];t=t||l.clientHeight,this.state.dropdown.visible&&("text"==c?(a=(i=this.getCaretGlobalPosition()).bottom,s=i.top,n=i.left,o="auto"):(r=function(t){for(var e=0,i=0;t;)e+=t.offsetLeft||0,i+=t.offsetTop||0,t=t.parentNode;return{left:e,top:i}}(this.settings.dropdown.appendTarget),s=(i=h.getBoundingClientRect()).top+2-r.top,a=i.bottom-1-r.top,n=i.left-r.left,o=i.width+"px"),s=Math.floor(s),a=Math.ceil(a),e=d-i.bottom0&&void 0!==arguments[0])||arguments[0],e=this.dropdown.events.callbacks,i=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this),onKeyDown:e.onKeyDown.bind(this),onMouseOver:e.onMouseOver.bind(this),onMouseLeave:e.onMouseLeave.bind(this),onClick:e.onClick.bind(this),onScroll:e.onScroll.bind(this)},s=t?"addEventListener":"removeEventListener";"manual"!=this.settings.dropdown.position&&(window[s]("resize",i.position),window[s]("keydown",i.onKeyDown)),this.DOM.dropdown[s]("mouseover",i.onMouseOver),this.DOM.dropdown[s]("mouseleave",i.onMouseLeave),this.DOM.dropdown[s]("mousedown",i.onClick),this.DOM.dropdown.content[s]("scroll",i.onScroll)},callbacks:{onKeyDown:function(t){var e=this.DOM.dropdown.querySelector("[class$='--active']"),i=e;switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":var s;t.preventDefault(),i&&(i=i[("ArrowUp"==t.key||"Up"==t.key?"previous":"next")+"ElementSibling"]),i||(i=(s=this.DOM.dropdown.content.children)["ArrowUp"==t.key||"Up"==t.key?s.length-1:0]),this.dropdown.highlightOption.call(this,i,!0);break;case"Escape":case"Esc":this.dropdown.hide.call(this);break;case"ArrowRight":if(this.state.actions.ArrowLeft)return;case"Tab":if("mix"!=this.settings.mode&&!this.settings.autoComplete.rightKey&&!this.state.editing){try{var a=i?i.textContent:this.suggestedListItems[0].value;this.input.autocomplete.set.call(this,a)}catch(t){}return!1}case"Enter":t.preventDefault(),this.dropdown.selectOption.call(this,e);break;case"Backspace":if("mix"==this.settings.mode||this.state.editing.scope)return;var n=this.input.value.trim();""!=n&&8203!=n.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0))}},onMouseOver:function(t){var e=t.target.closest("."+this.settings.classNames.dropdownItem);e&&this.dropdown.highlightOption.call(this,e)},onMouseLeave:function(t){this.dropdown.highlightOption.call(this)},onClick:function(t){var e=this;if(0==t.button&&t.target!=this.DOM.dropdown){var i=t.target.closest("."+this.settings.classNames.dropdownItem);this.state.actions.selectOption=!0,setTimeout((function(){return e.state.actions.selectOption=!1}),50),this.settings.hooks.suggestionClick(t,{tagify:this,suggestionElm:i}).then((function(){i&&e.dropdown.selectOption.call(e,i)})).catch((function(t){return t}))}},onScroll:function(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger("dropdown:scroll",{percentage:Math.round(i)})}}},highlightOption:function(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.suggestedListItems[this.getNodeIndex(t)],this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),"manual"!=this.settings.dropdown.position&&this.dropdown.position.call(this))},selectOption:function(t){var e=this,i=this.settings.dropdown,a=i.clearOnSelect,n=i.closeOnSelect;if(!t)return this.addTags(this.input.value,!0),void(n&&this.dropdown.hide.call(this));var o=t.getAttribute("tagifySuggestionIdx"),r=(o?this.suggestedListItems[+o]:"")||this.input.value;if(this.trigger("dropdown:select",{data:r,elm:t}),this.state.editing?this.onEditTagDone(this.state.editing.scope,s(s(s({},this.state.editing.scope.__tagifyTagData),{},{value:r.value},r instanceof Object?r:{}),{},{__isValid:!0})):this.addTags([r],a),setTimeout((function(){e.DOM.input.focus(),e.toggleFocusClass(!0)})),n)return this.dropdown.hide.call(this);this.dropdown.refilter.call(this)},selectAll:function(){var t=this.settings.skipInvalid;return this.settings.skipInvalid=!0,this.addTags(this.settings.whitelist,!0),this.settings.skipInvalid=t,this.dropdown.hide.call(this),this},filterListItems:function(t){var e,i,s,a,n,r=this,l=this.settings,d=l.dropdown,h=[],g=l.whitelist,u=d.maxItems||1/0,p=d.searchKeys,f=0;if(!t||!p.length)return(l.duplicates?g:g.filter((function(t){return!r.isTagDuplicate(o(t)?t.value:t)}))).slice(0,u);for(n=d.caseSensitive?""+t:(""+t).toLowerCase();f=0):i=p.some((function(t){var i=""+e[t]||"";return d.accentedSearch&&(i=c(i),n=c(n)),d.caseSensitive||(i=i.toLowerCase()),0==i.indexOf(n)})),a=!l.duplicates&&this.isTagDuplicate(o(e)?e.value:e),i&&!a&&u--&&h.push(e),0!=u);f++);return h},createListHTML:function(t){var e=this;return t.map((function(t,i){"string"!=typeof t&&"number"!=typeof t||(t={value:t});var s=e.settings.dropdown.mapValueTo,a=s?"function"==typeof s?s(t):t[s]:t.value,n=d({},t,{value:a&&"string"==typeof a?l(a):a,tagifySuggestionIdx:i});return e.settings.templates.dropdownItem.call(e,n)})).join("")}},g={delimiters:",",pattern:null,maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,keepInvalidTags:!1,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,editTags:2,transformTag:function(){},trim:!0,mixMode:{insertAfterTag:" "},autoComplete:{enabled:!0,rightKey:!1},classNames:{namespace:"tagify",input:"tagify__input",focus:"tagify--focus",tag:"tagify__tag",tagNoAnimation:"tagify--noAnim",tagInvalid:"tagify--invalid",tagNotAllowed:"tagify--notAllowed",inputInvalid:"tagify__input--invalid",tagX:"tagify__tag__removeBtn",tagText:"tagify__tag-text",dropdown:"tagify__dropdown",dropdownWrapper:"tagify__dropdown__wrapper",dropdownItem:"tagify__dropdown__item",dropdownItemActive:"tagify__dropdown__item--active",dropdownInital:"tagify__dropdown--initial",scopeLoading:"tagify--loading",tagLoading:"tagify__tag--loading",tagEditing:"tagify__tag--editable",tagFlash:"tagify__tag--flash",tagHide:"tagify__tag--hide",hasMaxTags:"tagify--hasMaxTags",hasNoTags:"tagify--noTags",empty:"tagify--empty"},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:["value","searchBy"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,highlightFirst:!1,closeOnSelect:!0,clearOnSelect:!0,position:"all",appendTarget:null},hooks:{beforeRemoveTag:function(){return Promise.resolve()},suggestionClick:function(){return Promise.resolve()}}};var u={customBinding:function(){var t=this;this.customEventsList.forEach((function(e){t.on(e,t.settings.callbacks[e])}))},binding:function(){var t,e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],i=this.events.callbacks,s=e?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!e)for(var a in this.state.mainEvents=e,e&&!this.listeners.main&&(this.DOM.input.addEventListener(this.isIE?"keydown":"input",i[this.isIE?"onInputIE":"onInput"].bind(this)),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),t=this.listeners.main=this.listeners.main||{focus:["input",i.onFocusBlur.bind(this)],blur:["input",i.onFocusBlur.bind(this)],keydown:["input",i.onKeydown.bind(this)],click:["scope",i.onClickScope.bind(this)],dblclick:["scope",i.onDoubleClickScope.bind(this)],paste:["input",i.onPaste.bind(this)]})("blur"!=a||e)&&this.DOM[t[a][0]][s](a,t[a][1])},callbacks:{onFocusBlur:function(t){var e=t.target?this.trim(t.target.textContent):"",i=this.settings,s=t.type,a=i.dropdown.enabled>=0,n={relatedTarget:t.relatedTarget},o=this.state.actions.selectOption&&(a||!i.dropdown.closeOnSelect),r=this.state.actions.addNew&&a,l=window.getSelection();if("blur"==s){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide.call(this),void this.DOM.input.focus();this.postUpdate(),this.triggerChangeEvent()}if(!o&&!r)if(this.state.hasFocus="focus"==s&&+new Date,this.toggleFocusClass(this.state.hasFocus),"mix"!=i.mode){if("focus"==s)return this.trigger("focus",n),void(0===i.dropdown.enabled&&this.dropdown.show.call(this));"blur"==s&&(this.trigger("blur",n),this.loading(!1),("select"==this.settings.mode?!this.value.length||this.value[0].value!=e:e&&!this.state.actions.selectOption&&i.addTagOnBlur)&&this.addTags(e,!0)),this.DOM.input.removeAttribute("style"),this.dropdown.hide.call(this)}else"focus"==s?this.trigger("focus",n):"blur"==t.type&&(this.trigger("blur",n),this.loading(!1),this.dropdown.hide.call(this),this.state.dropdown.visible=void 0,this.state.selection={anchorOffset:l.anchorOffset,anchorNode:l.anchorNode},l.getRangeAt&&l.rangeCount&&(this.state.selection.range=l.getRangeAt(0)))},onKeydown:function(t){var e=this,i=this.trim(t.target.textContent);if(this.trigger("keydown",{originalEvent:this.cloneEvent(t)}),"mix"==this.settings.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":if(this.state.editing)return;var s=document.getSelection(),a="Delete"==t.key&&s.anchorOffset==s.anchorNode.length,n=1==s.anchorNode.nodeType||!s.anchorOffset&&s.anchorNode.previousElementSibling,o=r(this.DOM.input.innerHTML),l=this.getTagElms();if(3==s.anchorNode.nodeType&&!s.anchorNode.nodeValue&&s.anchorNode.previousElementSibling&&t.preventDefault(),(n||a)&&!this.settings.backspace)return void t.preventDefault();setTimeout((function(){if(r(e.DOM.input.innerHTML).length>=o.length&&(e.removeTags(s.anchorNode.previousElementSibling),e.fixFirefoxLastTagNoCaret(),2==e.DOM.input.children.length&&"BR"==e.DOM.input.children[1].tagName))return e.DOM.input.innerHTML="",e.value.length=0,!0;e.value=[].map.call(l,(function(t,i){var s=t.__tagifyTagData;if(t.parentNode)return s;e.trigger("remove",{tag:t,index:i,data:s})})).filter((function(t){return t}))}),50)}return!0}switch(t.key){case"Backspace":this.state.dropdown.visible&&"manual"!=this.settings.dropdown.position||""!=i&&8203!=i.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show.call(this);break;case"ArrowRight":var d=this.state.inputSuggestion||this.state.ddItemData;if(d&&this.settings.autoComplete.rightKey)return void this.addTags([d],!0);break;case"Tab":if(i&&t.preventDefault(),!i||"select"==this.settings.mode)return!0;case"Enter":if(this.state.dropdown.visible||229==t.keyCode)return;t.preventDefault(),setTimeout((function(){e.state.actions.selectOption||e.addTags(i,!0)}))}},onInput:function(t){if("mix"==this.settings.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled,s={value:e,inputElm:this.DOM.input};s.isValid=this.validateTag({value:e}),this.trigger("input",s),this.input.value!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e)&&this.input.set.call(this):this.settings.dropdown.enabled>=0&&this.dropdown[i?"show":"hide"].call(this,e))},onMixTagsInput:function(t){var e,i,s,a,n,o,r,l,c,h=this,g=this.settings,u=this.value.length;if(this.getTagElms().length>u)return this.value=[].map.call(this.getTagElms(),(function(t){return t.__tagifyTagData})),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(r=window.getSelection()).rangeCount>0&&3==r.anchorNode.nodeType){if((e=r.getRangeAt(0).cloneRange()).collapse(!0),e.setStart(r.focusNode,0),a=(i=e.toString().slice(0,e.endOffset)).split(g.pattern).length-1,(s=i.match(g.pattern))&&(n=i.slice(i.lastIndexOf(s[s.length-1]))),n){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:n.match(g.pattern)[0],value:n.replace(g.pattern,"")},this.state.tag.baseOffset=r.baseOffset-this.state.tag.value.length,c=this.state.tag.value.match(g.delimiters))return this.state.tag.value=this.state.tag.value.replace(g.delimiters,""),this.state.tag.delimiters=c[0],this.addTags(this.state.tag.value,g.dropdown.clearOnSelect),void this.dropdown.hide.call(this);o=this.state.tag.value.length>=g.dropdown.enabled;try{l=(l=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&l.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(l||a500)?this.state.dropdown.visible?this.dropdown.hide.call(this):0===i.dropdown.enabled&&"mix"!=i.mode&&this.dropdown.show.call(this):"select"==i.mode&&!this.state.dropdown.visible&&this.dropdown.show.call(this));this.removeTags(t.target.parentNode)}else this.state.hasFocus||this.DOM.input.focus()},onPaste:function(t){var e;t.preventDefault(),e=(t.clipboardData||window.clipboardData).getData("Text"),"mix"==this.settings.mode?this.injectAtCaret(e,window.getSelection().getRangeAt(0)):this.addTags(e)},onEditTagInput:function(t,e){var i=t.closest("."+this.settings.classNames.tag),s=this.getNodeIndex(i),a=this.tagData(i),n=this.input.normalize.call(this,t),o=n!=a.__originalData.value,r=this.validateTag({value:n});o||!0!==t.originalIsValid||(r=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==r),a.__isValid=r,i.title=!0===r?a.title||a.value:r,n.length>=this.settings.dropdown.enabled&&(this.state.editing.value=n,this.dropdown.show.call(this,n)),this.trigger("edit:input",{tag:i,index:s,data:d({},this.value[s],{newValue:n}),originalEvent:this.cloneEvent(e)})},onEditTagFocus:function(t){this.state.editing={scope:t,input:t.querySelector("[contenteditable]")}},onEditTagBlur:function(t){if(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t)){var e=t.closest("."+this.settings.classNames.tag),i=this.input.normalize.call(this,t),s=i,a=d({},this.tagData(e),{value:s}),n=s!=a.__originalData.value,o=this.validateTag(a);if(!i)return this.removeTags(e),void this.onEditTagDone(null,a);n?(this.settings.transformTag.call(this,a),!0===(o=this.validateTag(a))?(a=this.getWhitelistItemByValue(s)||a.__preInvalidData||{},a=Object.assign({},a,{value:s}),this.settings.transformTag.call(this,a),this.onEditTagDone(e,a)):this.trigger("invalid",{data:a,tag:e,message:o})):this.onEditTagDone(e,a.__originalData)}},onEditTagkeydown:function(t,e){switch(this.trigger("edit:keydown",{originalEvent:this.cloneEvent(t)}),t.key){case"Esc":case"Escape":t.target.textContent=e.__tagifyTagData.__originalData.value;case"Enter":case"Tab":t.preventDefault(),t.target.blur()}},onDoubleClickScope:function(t){var e,i,s=t.target.closest("."+this.settings.classNames.tag),a=this.settings;s&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute("readonly"),"select"==a.mode||a.readonly||e||i||!this.settings.editTags||this.editTag(s),this.toggleFocusClass(!0),this.trigger("dblclick",{tag:s,index:this.getNodeIndex(s),data:this.tagData(s)}))}}};function p(t,e){return t.previousElementSibling&&t.previousElementSibling.classList.contains("tagify")?(console.warn("Tagify: ","input element is already Tagified",t),this):t?(this.isFirefox="undefined"!=typeof InstallTrigger,this.isIE=window.document.documentMode,this.applySettings(t,e||{}),this.state={editing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},d(this,new this.EventDispatcher(this)),this.build(t),this.getCSSVars(),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this),void(t.autofocus&&this.DOM.input.focus())):(console.warn("Tagify: ","invalid input element ",t),this)}return p.prototype={dropdown:h,TEXTS:{empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},DEFAULTS:g,customEventsList:["change","add","remove","invalid","input","click","keydown","focus","blur","edit:input","edit:updated","edit:start","edit:keydown","dropdown:show","dropdown:hide","dropdown:select","dropdown:updated","dropdown:noMatch"],trim:function(t){return this.settings.trim?t.trim():t},parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},templates:{wrapper:function(t,e){return'\n \n ')},tag:function(t){return'\n \n
\n ').concat(t.value,"\n
\n
")},dropdown:function(t){var e=t.dropdown,i="manual"==e.position,s="".concat(t.classNames.dropdown);return'
\n
\n
')},dropdownItem:function(t){return"
').concat(t.value,"
")},dropdownItemNoMatch:null},parseTemplate:function(t,e){return t=this.settings.templates[t]||t,this.parseHTML(t.apply(this,e))},applySettings:function(t,e){this.DEFAULTS.templates=this.templates;var i=this.settings=d({},this.DEFAULTS,e);if(i.readonly=t.hasAttribute("readonly"),i.placeholder=t.getAttribute("placeholder")||i.placeholder||"",i.required=t.hasAttribute("required"),this.isIE&&(i.autoComplete=!1),["whitelist","blacklist"].forEach((function(e){var s=t.getAttribute("data-"+e);s&&(s=s.split(i.delimiters))instanceof Array&&(i[e]=s)})),"autoComplete"in e&&!o(e.autoComplete)&&(i.autoComplete=this.DEFAULTS.autoComplete,i.autoComplete.enabled=e.autoComplete),"mix"==i.mode&&(i.autoComplete.rightKey=!0,i.delimiters=e.delimiters||null),t.pattern)try{i.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings.delimiters)try{i.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}"select"==i.mode&&(i.dropdown.enabled=0),i.dropdown.appendTarget=e.dropdown&&e.dropdown.appendTarget?e.dropdown.appendTarget:document.body},getAttributes:function(t){if("[object Object]"!=Object.prototype.toString.call(t))return"";var e,i,s=Object.keys(t),a="";for(i=s.length;i--;)"class"!=(e=s[i])&&t.hasOwnProperty(e)&&void 0!==t[e]&&(a+=" "+e+(void 0!==t[e]?'="'.concat(t[e],'"'):""));return a},getCaretGlobalPosition:function(){var t=document.getSelection();if(t.rangeCount){var e,i,s=t.getRangeAt(0),a=s.startContainer,n=s.startOffset;if(n>0)return(i=document.createRange()).setStart(a,n-1),i.setEnd(a,n),{left:(e=i.getBoundingClientRect()).right,top:e.top,bottom:e.bottom};if(a.getBoundingClientRect)return a.getBoundingClientRect()}return{left:-9999,top:-9999}},getCSSVars:function(){var t,e=getComputedStyle(this.DOM.scope,null);this.CSSVars={tagHideTransition:function(t){var e=t.value;return"s"==t.unit?1e3*e:e}(function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter((function(t){return t})).pop().trim();return{value:+t.split(e).filter((function(t){return t}))[0].trim(),unit:e}}((t="tag-hide-transition",e.getPropertyValue("--"+t))))}},build:function(t){var e=this.DOM;e.originalInput=t,e.scope=this.parseTemplate("wrapper",[t,this.settings]),e.input=e.scope.querySelector("."+this.settings.classNames.input),t.parentNode.insertBefore(e.scope,t),this.settings.dropdown.enabled>=0&&this.dropdown.init.call(this)},destroy:function(){this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.dropdown.hide.call(this,!0),clearTimeout(this.dropdownHide__bindEventsTimeout)},loadOriginalValues:function(t){var e=this;if(t=t||this.DOM.originalInput.value)if(this.removeAllTags(),"mix"==this.settings.mode)this.parseMixTags(t.trim());else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t).forEach((function(t){return t&&t.classList.add(e.settings.classNames.tagNoAnimation)}))}else this.postUpdate();this.state.lastOriginalValueReported=this.DOM.originalInput.value,this.state.loadedOriginalValues=!0},cloneEvent:function(t){var e={};for(var i in t)e[i]=t[i];return e},EventDispatcher:function(e){var i=document.createTextNode("");function s(t,e,s){s&&e.split(/\s+/g).forEach((function(e){return i[t+"EventListener"].call(i,e,s)}))}this.off=function(t,e){return s("remove",t,e),this},this.on=function(t,e){return e&&"function"==typeof e&&s("add",t,e),this},this.trigger=function(s,a){var n;if(s)if(e.settings.isJQueryPlugin)"remove"==s&&(s="removeTag"),jQuery(e.DOM.originalInput).triggerHandler(s,[a]);else{try{var o=d({},"object"===t(a)?a:{value:a});if(o.tagify=this,a instanceof Object)for(var r in a)a[r]instanceof HTMLElement&&(o[r]=a[r]);n=new CustomEvent(s,{detail:o})}catch(t){console.warn(t)}i.dispatchEvent(n)}}},loading:function(t){return this.state.isLoading=t,this.DOM.scope.classList[t?"add":"remove"](this.settings.classNames.scopeLoading),this},tagLoading:function(t,e){return t&&t.classList[e?"add":"remove"](this.settings.classNames.tagLoading),this},toggleFocusClass:function(t){this.DOM.scope.classList.toggle(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent("change",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger("change",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)},events:u,fixFirefoxLastTagNoCaret:function(){var t=this.DOM.input;if(this.isFirefox&&t.childNodes.length&&1==t.lastChild.nodeType)return t.appendChild(document.createTextNode("​")),this.setRangeAtStartEnd(!0),!0},placeCaretAfterTag:function(t){var e=t.nextSibling,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartAfter(e||t),s.setEndAfter(e||t),i.removeAllRanges(),i.addRange(s))},insertAfterTag:function(t,e){e=e||this.settings.mixMode.insertAfterTag,t&&e&&(e="string"==typeof e?document.createTextNode(e):e,t.appendChild(e),t.parentNode.insertBefore(e,t.nextSibling))},editTag:function(t,e){var i=this;t=t||this.getLastTag(),e=e||{},this.dropdown.hide.call(this);var s=t.querySelector("."+this.settings.classNames.tagText),a=this.getNodeIndex(t),n=t.__tagifyTagData,o=this.events.callbacks,r=this,l=!0;if(s){if(!(n instanceof Object&&"editable"in n)||n.editable)return t.__tagifyTagData.__originalData=d({},n),t.classList.add(this.settings.classNames.tagEditing),s.setAttribute("contenteditable",!0),s.addEventListener("focus",o.onEditTagFocus.bind(this,t)),s.addEventListener("blur",(function(){setTimeout(o.onEditTagBlur.bind(r),0,s)})),s.addEventListener("input",o.onEditTagInput.bind(this,s)),s.addEventListener("keydown",(function(e){return o.onEditTagkeydown.call(i,e,t)})),s.focus(),this.setRangeAtStartEnd(!1,s),e.skipValidation||(l=this.editTagToggleValidity(t,n.value)),s.originalIsValid=l,this.trigger("edit:start",{tag:t,index:a,data:n,isValid:l}),this}else console.warn("Cannot find element in Tag template: .",this.settings.classNames.tagText)},editTagToggleValidity:function(t,e){var i,s=t.__tagifyTagData;if(s)return i=!(!s.__isValid||1==s.__isValid),t.classList.toggle(this.settings.classNames.tagInvalid,i),s.__isValid;console.warn("tag has no data: ",t,s)},onEditTagDone:function(t,e){this.state.editing=!1,e=e||{};var i={tag:t,index:this.getNodeIndex(t),data:e};this.trigger("edit:beforeUpdate",i),delete e.__originalData,t&&(this.editTagToggleValidity(t),this.replaceTag(t,e)),this.trigger("edit:updated",i),this.dropdown.hide.call(this),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag:function(t,e){e&&e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&d(e,this.getInvalidTagParams(e,e.__isValid));var i=this.createTagElem(e);t.parentNode.replaceChild(i,t),this.updateValueByDOMTags()},updateValueByDOMTags:function(){var t=this;this.value.length=0,[].forEach.call(this.getTagElms(),(function(e){e.classList.contains(t.settings.classNames.tagNotAllowed)||t.value.push(t.tagData(e))})),this.update()},setRangeAtStartEnd:function(t,e){t="number"==typeof t?t:!!t,e=(e=e||this.DOM.input).lastChild||e;var i=document.getSelection();try{i.rangeCount>=1&&["Start","End"].forEach((function(s){return i.getRangeAt(0)["set"+s](e,t||e.length)}))}catch(t){console.warn("Tagify: ",t)}},injectAtCaret:function(t,e){if(e=e||this.state.selection.range)return"string"==typeof t&&(t=document.createTextNode(t)),e.deleteContents(),e.insertNode(t),this.insertAfterTag(t),this.DOM.input.focus(),this.setRangeAtStartEnd(!0,t.nextSibling),this.updateValueByDOMTags(),this.update(),this},input:{value:"",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],i=this.settings.dropdown.closeOnSelect;this.input.value=t,e&&(this.DOM.input.innerHTML=t),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},validate:function(){var t=!this.input.value||!0===this.validateTag({value:this.input.value});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize:function(t){var e=t||this.DOM.input,i=[];e.childNodes.forEach((function(t){return 3==t.nodeType&&i.push(t.nodeValue)})),i=i.join("\n");try{i=i.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return i=i.replace(/\s/g," "),this.settings.trim&&(i=i.replace(/^\s+/,"")),i},autocomplete:{suggest:function(t){if(this.settings.autoComplete.enabled){"string"==typeof(t=t||{})&&(t={value:t});var e=t.value?""+t.value:"",i=e.substr(0,this.input.value.length).toLowerCase(),s=e.substring(this.input.value.length);e&&this.input.value&&i==this.input.value.toLowerCase()?(this.DOM.input.setAttribute("data-suggest",s),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}},set:function(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.input.value+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd()),this.input.autocomplete.suggest.call(this),this.dropdown.hide.call(this),!0)}}},getTagIdx:function(t){return this.value.findIndex((function(e){return JSON.stringify(e)==JSON.stringify(t)}))},getNodeIndex:function(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms:function(){for(var t=arguments.length,e=new Array(t),i=0;i=this.settings.maxTags&&this.TEXTS.exceed},normalizeTags:function(t){var e=this,i=this.settings,n=i.whitelist,o=i.delimiters,r=i.mode,l=!!n&&n[0]instanceof Object,d=t instanceof Array,c=[],h=function(t){return(t+"").split(o).filter((function(t){return t})).map((function(t){return{value:e.trim(t)}}))};if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=h(t)}else if(d){var g;t=(g=[]).concat.apply(g,a(t.map((function(t){return t.value?h(t.value).map((function(e){return s(s({},t),e)})):h(t)}))))}return l&&(t.forEach((function(t){var i=e.getWhitelistItemByValue(t.value);i&&i instanceof Object?c.push(i):"mix"!=r&&c.push(t)})),c.length&&(t=c)),t},getWhitelistItemByValue:function(t){var e=this,i=this.settings.whitelist.find((function(i){return n("string"==typeof i?i:i.value,t,e.settings.dropdown.caseSensitive)}));return"string"==typeof i?{value:i}:i},parseMixTags:function(t){var e=this,i=this.settings,s=i.mixTagsInterpolator,a=i.duplicates,n=i.transformTag,o=i.enforceWhitelist,r=[];return t=t.split(s[0]).map((function(t,i){var l,d,c=t.split(s[1]),h=c[0];try{if(h==+h)throw Error;l=JSON.parse(h)}catch(t){l=e.normalizeTags(h)[0]}if(!(c.length>1)||o&&!e.isTagWhitelisted(l.value)||!a&&e.isTagDuplicate(l.value)){if(t)return i?s[0]+t:t}else n.call(e,l),d=e.createTagElem(l),r.push(l),d.classList.add(e.settings.classNames.tagNoAnimation),c[0]=d.outerHTML,e.value.push(l);return c.join("")})).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.DOM.input.normalize(),this.getTagElms().forEach((function(t,i){return e.tagData(t,r[i])})),this.update({withoutChangeEvent:!0}),t},replaceTextWithNode:function(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),i=n.nodeValue.lastIndexOf(e),s=n.splitText(i),t&&n.parentNode.replaceChild(t,s),!0}},selectTag:function(t,e){if(!this.settings.enforceWhitelist||this.isTagWhitelisted(e.value))return this.input.set.call(this,e.value,!0),this.state.actions.selectOption&&setTimeout(this.setRangeAtStartEnd.bind(this)),this.getLastTag()?this.replaceTag(this.getLastTag(),e):this.appendTag(t),this.value[0]=e,this.trigger("add",{tag:t,data:e}),this.update(),[t]},addEmptyTag:function(){var t={value:""},e=this.createTagElem(t);this.tagData(e,t),this.appendTag(e),this.editTag(e,{skipValidation:!0})},addTags:function(t,e){var i=this,s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.settings.skipInvalid,a=[],n=this.settings;return t&&0!=t.length?(t=this.normalizeTags(t),"mix"==n.mode?this.addMixTags(t):("select"==n.mode&&(e=!1),this.DOM.input.removeAttribute("style"),t.forEach((function(t){var e,o={},r=Object.assign({},t,{value:t.value+""});if((t=Object.assign({},r)).__isValid=i.hasMaxTags()||i.validateTag(t),n.transformTag.call(i,t),!0!==t.__isValid){if(s)return;d(o,i.getInvalidTagParams(t,t.__isValid),{__preInvalidData:r}),t.__isValid==i.TEXTS.duplicate&&i.flashTag(i.getTagElmByValue(t.value))}if(t.readonly&&(o["aria-readonly"]=!0),e=i.createTagElem(d({},t,o)),a.push(e),"select"==n.mode)return i.selectTag(e,t);i.appendTag(e),t.__isValid&&!0===t.__isValid?(i.value.push(t),i.update(),i.trigger("add",{tag:e,index:i.value.length-1,data:t})):(i.trigger("invalid",{data:t,index:i.value.length,tag:e,message:t.__isValid}),n.keepInvalidTags||setTimeout((function(){return i.removeTags(e,!0)}),1e3)),i.dropdown.position.call(i)})),t.length&&e&&this.input.set.call(this),this.dropdown.refilter.call(this),a)):("select"==n.mode&&this.removeAllTags(),a)},addMixTags:function(t){var e,i=this,s=this.settings,a=this.state.tag.delimiters;return s.transformTag.call(this,t[0]),t[0].prefix=t[0].prefix||this.state.tag?this.state.tag.prefix:(s.pattern.source||s.pattern)[0],e=this.createTagElem(t[0]),this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((function(){return e.classList.add(i.settings.classNames.tagNoAnimation)}),300),this.value.push(t[0]),this.update(),!a&&setTimeout((function(){i.insertAfterTag(e),i.placeCaretAfterTag(e)}),this.isFirefox?100:0),this.state.tag=null,this.trigger("add",d({},{tag:e},{data:t[0]})),e},appendTag:function(t){var e=this.DOM.scope.lastElementChild;e===this.DOM.input?this.DOM.scope.insertBefore(t,e):this.DOM.scope.appendChild(t)},createTagElem:function(t){var e,i=d({},t,{value:l(t.value)});return this.settings.readonly&&(t.readonly=!0),e=this.parseTemplate("tag",[i]),this.tagData(e,t),e},reCheckInvalidTags:function(){var t=this,e=this.settings,i=".".concat(e.classNames.tag,".").concat(e.classNames.tagNotAllowed),s=this.DOM.scope.querySelectorAll(i);[].forEach.call(s,(function(e){var i=t.tagData(e),s=e.getAttribute("title")==t.TEXTS.duplicate,a=!0===t.validateTag(i);s&&a&&(i=i.__preInvalidData?i.__preInvalidData:{value:i.value},t.replaceTag(e,i))}))},removeTags:function(t,e,i){var s,a=this;t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce((function(t,e){return e&&"string"==typeof e&&(e=a.getTagElmByValue(e)),e&&t.push({node:e,idx:a.getTagIdx(a.tagData(e)),data:a.tagData(e,{__removed:!0})}),t}),[]),i="number"==typeof i?i:this.CSSVars.tagHideTransition,"select"==this.settings.mode&&(i=0,this.input.set.call(this)),1==s.length&&s[0].node.classList.contains(this.settings.classNames.tagNotAllowed)&&(e=!0),s.length&&this.settings.hooks.beforeRemoveTag(s,{tagify:this}).then((function(){function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?this.settings.keepInvalidTags&&this.trigger("remove",{tag:t.node,index:t.idx}):(this.trigger("remove",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter.call(this),this.dropdown.position.call(this),this.DOM.input.normalize(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+"px",document.body.clientTop,e.node.classList.add(this.settings.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(a,s[0]):s.forEach(t.bind(a)),e||(s.forEach((function(t){var e=Object.assign({},t.data);delete e.__removed;var i=a.getTagIdx(e);i>-1&&a.value.splice(i,1)})),a.update())})).catch((function(t){}))},removeAllTags:function(){this.value=[],"mix"==this.settings.mode?this.DOM.input.innerHTML="":Array.prototype.slice.call(this.getTagElms()).forEach((function(t){return t.parentNode.removeChild(t)})),this.dropdown.position.call(this),"select"==this.settings.mode&&this.input.set.call(this),this.update()},postUpdate:function(){var t=this.settings.classNames,e="mix"==this.settings.mode?this.DOM.originalInput.value:this.value.length;this.DOM.scope.classList.toggle(t.hasMaxTags,this.value.length>=this.settings.maxTags),this.DOM.scope.classList.toggle(t.hasNoTags,!this.value.length),this.DOM.scope.classList.toggle(t.empty,!e)},update:function(t){var e,i,s=this.DOM.originalInput,a=(t||{}).withoutChangeEvent,n=(e=this.value,i=["__isValid","__removed"],e.map((function(t){var e={};for(var s in t)i.indexOf(s)<0&&(e[s]=t[s]);return e})));s.value="mix"==this.settings.mode?this.getMixedTagsAsString(n):n.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(n):JSON.stringify(n):"",this.postUpdate(),!a&&this.state.loadedOriginalValues&&this.triggerChangeEvent()},getMixedTagsAsString:function(){var t="",e=this,i=this.settings.mixTagsInterpolator;return function s(a){a.childNodes.forEach((function(a){if(1==a.nodeType){if(a.classList.contains(e.settings.classNames.tag)&&e.tagData(a)){if(e.tagData(a).__removed)return;return void(t+=i[0]+JSON.stringify(a.__tagifyTagData)+i[1])}"BR"!=a.tagName||a.parentNode!=e.DOM.input&&1!=a.parentNode.childNodes.length?"DIV"!=a.tagName&&"P"!=a.tagName||(t+="\r\n",s(a)):t+="\r\n"}else t+=a.textContent}))}(this.DOM.input),t}},p.prototype.removeTag=p.prototype.removeTags,p})); diff --git a/lostplaces/lostplaces_app/templates/flat/codex.html b/lostplaces/lostplaces_app/templates/flat/codex.html index 4a15df9..6f8c83e 100644 --- a/lostplaces/lostplaces_app/templates/flat/codex.html +++ b/lostplaces/lostplaces_app/templates/flat/codex.html @@ -20,7 +20,7 @@ We do not change anything in the location.
  • - 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. + 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.
  • Spraying is an absolute "no-go"! diff --git a/lostplaces/lostplaces_app/templates/global.html b/lostplaces/lostplaces_app/templates/global.html index 481704d..6863ad3 100644 --- a/lostplaces/lostplaces_app/templates/global.html +++ b/lostplaces/lostplaces_app/templates/global.html @@ -3,19 +3,18 @@ - - - - - - - {% block title %}Urban Exploration{% endblock %} - + + {% block additional_head %} + {% endblock additional_head %} - {% block additional_head %} - {% endblock additional_head %} - - + + + + + + {% block title %}Urban Exploration{% endblock %} + +
    diff --git a/lostplaces/lostplaces_app/templates/partials/form/inputField.html b/lostplaces/lostplaces_app/templates/partials/form/inputField.html index 1d141c2..8447c67 100644 --- a/lostplaces/lostplaces_app/templates/partials/form/inputField.html +++ b/lostplaces/lostplaces_app/templates/partials/form/inputField.html @@ -1,16 +1,18 @@ {% load widget_tweaks %} -
    +
    - {% render_field field class="LP-Input__Field"%} + {% with class="LP-Input__Field "%} + {% render_field field class=class%} + {% endwith %} - {% if field.errors %} - {% for error in field.errors%} - {{error}} - {% endfor %} + {% if field.errors %} + {% for error in field.errors%} + {{error}} + {% endfor %} {% elif field.help_text%} - {{ field.help_text }} + {{ field.help_text }} {% endif %}
    \ No newline at end of file diff --git a/lostplaces/lostplaces_app/templates/partials/tagging.html b/lostplaces/lostplaces_app/templates/partials/tagging.html new file mode 100644 index 0000000..581055b --- /dev/null +++ b/lostplaces/lostplaces_app/templates/partials/tagging.html @@ -0,0 +1,66 @@ +
    +
      + {% for tag in tag_list %} +
    • +
      + + {{tag}} + + {% if request.user and request.user == config.tagged_item.submitted_by %} + + + + + + + + + + {% endif %} +
      +
    • + {% endfor %} +
    +
    + +
    +
    + Tags hinzufügen + {% csrf_token %} +
    +
    + +
    +
    + {% include 'partials/form/inputField.html' with field=config.submit_form.tag_list classes="LP-Input--tagging" %} +
    +
    +
    +
    + + \ No newline at end of file diff --git a/lostplaces/lostplaces_app/templates/place/place_detail.html b/lostplaces/lostplaces_app/templates/place/place_detail.html index 160b2fd..452b38e 100644 --- a/lostplaces/lostplaces_app/templates/place/place_detail.html +++ b/lostplaces/lostplaces_app/templates/place/place_detail.html @@ -6,102 +6,103 @@ {% block additional_head %} + + + + + {% endblock additional_head %} {% block title %}{{place.name}}{% endblock %} {% block additional_menu_items %} -
  • Edit place
  • -
  • Delete place
  • +
  • Edit place
  • +
  • Delete place
  • {% endblock additional_menu_items %} {% block maincontent %}
    -
    -

    {{ place.name }}

    - {% if place.images.first.filename.hero.url %} -
    - -
    - {% endif %} -
    +
    +

    {{ place.name }}

    + {% if place.images.first.filename.hero.url %} +
    + +
    + {% endif %} +
    -
    -

    {{ place.description }}

    -
    +
    +

    {{ place.description }}

    +
    -
    -

    Map-Links

    - {% include 'partials/osm_map.html' %} - -
    +
    -
    -

    Photoalben

    -
    + +
    +

    Photoalben

    + -
    + + + Fotoalbum hinzufügen + + + + + +
    -
    -

    Bilder

    -
    -
      - {% for place_image in place.images.all %} -
    • - -
    • - {% endfor %} -
    -
    -
    +
    +

    Bilder

    +
    +
      + {% for place_image in place.images.all %} +
    • + +
    • + {% endfor %} +
    +
    +
    {% endblock maincontent %} \ No newline at end of file diff --git a/lostplaces/lostplaces_app/tests.py b/lostplaces/lostplaces_app/tests.py deleted file mode 100644 index 1a11e63..0000000 --- a/lostplaces/lostplaces_app/tests.py +++ /dev/null @@ -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. diff --git a/lostplaces/lostplaces_app/tests/__init__.py b/lostplaces/lostplaces_app/tests/__init__.py new file mode 100644 index 0000000..d23299f --- /dev/null +++ b/lostplaces/lostplaces_app/tests/__init__.py @@ -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] \ No newline at end of file diff --git a/lostplaces/lostplaces_app/tests/models/__init__.py b/lostplaces/lostplaces_app/tests/models/__init__.py new file mode 100644 index 0000000..7538d94 --- /dev/null +++ b/lostplaces/lostplaces_app/tests/models/__init__.py @@ -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='' + 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) diff --git a/lostplaces/lostplaces_app/tests/models/test_place_image_model.py b/lostplaces/lostplaces_app/tests/models/test_place_image_model.py new file mode 100644 index 0000000..bd1bf41 --- /dev/null +++ b/lostplaces/lostplaces_app/tests/models/test_place_image_model.py @@ -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)])) diff --git a/lostplaces/lostplaces_app/tests/models/test_place_model.py b/lostplaces/lostplaces_app/tests/models/test_place_model.py new file mode 100644 index 0000000..7213266 --- /dev/null +++ b/lostplaces/lostplaces_app/tests/models/test_place_model.py @@ -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 + ) + + ) diff --git a/lostplaces/lostplaces_app/tests/test_models.py b/lostplaces/lostplaces_app/tests/test_models.py new file mode 100644 index 0000000..e69de29 diff --git a/lostplaces/lostplaces_app/tests/views/__init__.py b/lostplaces/lostplaces_app/tests/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lostplaces/lostplaces_app/tests/views/test_base_views.py b/lostplaces/lostplaces_app/tests/views/test_base_views.py new file mode 100644 index 0000000..6525595 --- /dev/null +++ b/lostplaces/lostplaces_app/tests/views/test_base_views.py @@ -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) \ No newline at end of file diff --git a/lostplaces/lostplaces_app/tests/views/test_place_views.py b/lostplaces/lostplaces_app/tests/views/test_place_views.py new file mode 100644 index 0000000..ff4b46f --- /dev/null +++ b/lostplaces/lostplaces_app/tests/views/test_place_views.py @@ -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 + ) + + \ No newline at end of file diff --git a/lostplaces/lostplaces_app/urls.py b/lostplaces/lostplaces_app/urls.py index 6733958..b6d8246 100644 --- a/lostplaces/lostplaces_app/urls.py +++ b/lostplaces/lostplaces_app/urls.py @@ -7,6 +7,8 @@ from .views import ( PlaceCreateView, PlaceUpdateView, PlaceDeleteView, + PlaceTagDeleteView, + PlaceTagSubmitView, PhotoAlbumCreateView, PhotoAlbumDeleteView, FlatView @@ -23,4 +25,8 @@ urlpatterns = [ path('place/delete//', PlaceDeleteView.as_view(), name='place_delete'), path('place/', PlaceListView.as_view(), name='place_list'), path('flat//', FlatView, name='flatpage') + + # POST-only URL for tag submission + path('place/tag/', PlaceTagSubmitView.as_view(), name='place_tag_submit'), + path('place/tag/delete//', PlaceTagDeleteView.as_view(), name='place_tag_delete') ] diff --git a/lostplaces/lostplaces_app/views/place_views.py b/lostplaces/lostplaces_app/views/place_views.py index f407d71..f538187 100644 --- a/lostplaces/lostplaces_app/views/place_views.py +++ b/lostplaces/lostplaces_app/views/place_views.py @@ -11,7 +11,9 @@ from django.urls import reverse_lazy from lostplaces_app.models import Place, PlaceImage 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): paginate_by = 5 @@ -30,7 +32,14 @@ class PlaceDetailView(IsAuthenticated, View): context = { 'place': 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) diff --git a/lostplaces/lostplaces_app/views/views.py b/lostplaces/lostplaces_app/views/views.py index c638205..8b2c1ef 100644 --- a/lostplaces/lostplaces_app/views/views.py +++ b/lostplaces/lostplaces_app/views/views.py @@ -5,15 +5,19 @@ from django.contrib.messages.views import SuccessMessageMixin from django.contrib import messages from django.urls import reverse_lazy 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.views import IsAuthenticated +from lostplaces_app.views.base_views import IsAuthenticated from lostplaces_app.views.base_views import ( PlaceAssetCreateView, - PlaceAssetDeleteView + PlaceAssetDeleteView, ) + +from taggit.models import Tag + class SignUpView(SuccessMessageMixin, CreateView): form_class = ExplorerCreationForm success_url = reverse_lazy('login') @@ -49,5 +53,27 @@ class PhotoAlbumDeleteView(PlaceAssetDeleteView): success_message = 'Photo Album deleted' 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): return render(request, 'flat/' + slug + '.html')