diff --git a/lostplaces/lostplaces_app/forms.py b/lostplaces/lostplaces_app/forms.py
index d88d090..6ac307a 100644
--- a/lostplaces/lostplaces_app/forms.py
+++ b/lostplaces/lostplaces_app/forms.py
@@ -48,3 +48,7 @@ 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)
\ No newline at end of file
diff --git a/lostplaces/lostplaces_app/models.py b/lostplaces/lostplaces_app/models.py
index d513bb0..cf625a2 100644
--- a/lostplaces/lostplaces_app/models.py
+++ b/lostplaces/lostplaces_app/models.py
@@ -10,6 +10,7 @@ from django.db import models
from django.dispatch import receiver
from django.contrib.auth.models import AbstractUser
from easy_thumbnails.fields import ThumbnailerImageField
+from taggit.managers import TaggableManager
# Create your models here.
@@ -55,6 +56,7 @@ class Place (models.Model):
longitude = models.FloatField()
description = models.TextField()
+ tags = TaggableManager(blank=True)
# Get center position of LP-geocoordinates.
def average_latlon(place_list):
@@ -145,21 +147,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/main.css b/lostplaces/lostplaces_app/static/main.css
index de9b1c1..8489e0c 100644
--- a/lostplaces/lostplaces_app/static/main.css
+++ b/lostplaces/lostplaces_app/static/main.css
@@ -574,7 +574,8 @@ body {
padding: 8px 14px;
border-radius: 2px;
font-weight: bold;
- cursor: pointer; }
+ cursor: pointer;
+ white-space: nowrap; }
.LP-Button:active {
background-color: #76323F;
color: #f9f9f9; }
@@ -600,19 +601,28 @@ body {
flex-direction: column;
margin-bottom: -30px;
padding: 10px 0; }
- .LP-Input .LP-Input__Field {
+ .LP-Input--tagging .LP-Button {
+ height: 53px; }
+ .LP-Input--tagging .LP-Input__Field, .LP-Input--tagging .tagify {
+ min-height: 36px;
+ height: max-content;
+ font-family: Montserrat, Helvetica, sans-serif;
+ font-size: 1em;
+ padding: 0;
+ padding-left: 8px; }
+ .LP-Input .LP-Input__Field, .LP-Input .tagify {
border: none;
border-bottom: 1px solid #565656;
padding: 8px 0;
margin-bottom: 30px;
width: 100%; }
- .LP-Input .LP-Input__Field:focus, .LP-Input .LP-Input__Field:active, .LP-Input .LP-Input__Field:invalid {
+ .LP-Input .LP-Input__Field:focus, .LP-Input .tagify:focus, .LP-Input .LP-Input__Field:active, .LP-Input .tagify:active, .LP-Input .LP-Input__Field:invalid, .LP-Input .tagify:invalid, .LP-Input .LP-Input__Field--active, .LP-Input .tagify--focus {
margin-bottom: 29px;
border-bottom: 2px solid #76323F;
background-color: #f9f9f9;
border-radius: 3px 3px 0 0;
box-shadow: none; }
- .LP-Input .LP-Input__Field[type=submit] {
+ .LP-Input .LP-Input__Field[type=submit], .LP-Input .tagify[type=submit] {
background-color: #C09F80;
color: #565656;
border: none;
@@ -620,7 +630,7 @@ body {
border-radius: 2px;
font-weight: bold;
cursor: pointer; }
- .LP-Input .LP-Input__Field[type=submit]:active {
+ .LP-Input .LP-Input__Field[type=submit]:active, .LP-Input .tagify[type=submit]:active {
background-color: #76323F;
color: #f9f9f9; }
.LP-Input .LP-Input__Label {
@@ -636,22 +646,30 @@ body {
position: relative;
top: -30px;
overflow: hidden; }
- .LP-Input--error .LP-Input__Field {
+ .LP-Input--error .LP-Input__Field, .LP-Input--error .tagify {
margin-bottom: 25px;
border-bottom: 2px solid #76323F;
margin-bottom: 29px; }
.LP-Input--error .LP-Input__Message {
color: #76323F; }
- .LP-Input--disabled .LP-Input__Field, .LP-Input--disabled .LP-Input__Field:disabled {
+ .LP-Input--disabled .LP-Input__Field, .LP-Input--disabled .tagify,
+ .LP-Input--disabled .LP-Input__Field:disabled,
+ .LP-Input--disabled .tagify:disabled {
background-color: transparent;
border-bottom: 1px dashed #565656;
cursor: not-allowed; }
- label + .LP-Input--disabled .LP-Input__Field, label + .LP-Input--disabled .LP-Input__Field:disabled {
+ label + .LP-Input--disabled .LP-Input__Field, label + .LP-Input--disabled .tagify, label + .LP-Input--disabled .LP-Input__Field:disabled, label + .LP-Input--disabled .tagify:disabled {
color: red; }
- .LP-Input--disabled .LP-Input__Field:focus, .LP-Input--disabled .LP-Input__Field:active, .LP-Input--disabled .LP-Input__Field:disabled:focus, .LP-Input--disabled .LP-Input__Field:disabled:active {
+ .LP-Input--disabled .LP-Input__Field:focus, .LP-Input--disabled .tagify:focus, .LP-Input--disabled .LP-Input__Field:active, .LP-Input--disabled .tagify:active,
+ .LP-Input--disabled .LP-Input__Field:disabled:focus,
+ .LP-Input--disabled .tagify:disabled:focus,
+ .LP-Input--disabled .LP-Input__Field:disabled:active,
+ .LP-Input--disabled .tagify:disabled:active {
margin-bottom: 30px;
border-radius: 0; }
- .LP-Input--disabled .LP-Input__Field ~ .LP-Input__Message, .LP-Input--disabled .LP-Input__Field:disabled ~ .LP-Input__Message {
+ .LP-Input--disabled .LP-Input__Field ~ .LP-Input__Message, .LP-Input--disabled .tagify ~ .LP-Input__Message,
+ .LP-Input--disabled .LP-Input__Field:disabled ~ .LP-Input__Message,
+ .LP-Input--disabled .tagify:disabled ~ .LP-Input__Message {
visibility: hidden; }
.LP-Input--disabled .LP-Input__Label {
color: #565656; }
@@ -670,12 +688,14 @@ body {
width: auto;
object-fit: contain; }
-.LP-Tag {
+.LP-Tag, .tagify__tag {
padding: 8px 14px;
background-color: #D7CEC7;
border-radius: 2px;
width: max-content; }
- .LP-Tag .LP-Paragraph {
+ .LP-Tag:hover, .tagify__tag:hover {
+ background-color: #bdbdbd; }
+ .LP-Tag .LP-Paragraph, .tagify__tag .LP-Paragraph {
padding: 0;
margin: 0;
font-family: Montserrat, Helvetica, sans-serif;
@@ -861,8 +881,8 @@ body {
flex-wrap: wrap;
padding: 0;
margin: 0; }
- .LP-TagList .LP-TagList__List .LP-TagList__Item {
- margin: 6px; }
+ .LP-TagList .LP-TagList__List .LP-TagList__Item, .LP-TagList .LP-TagList__List .tagify__tag {
+ margin: 3px; }
.LP-Menu {
border-left: 1px solid #C09F80; }
@@ -1189,7 +1209,13 @@ body {
.LP-Footer .LP-LinkList__List .LP-LinkList__Item .LP-Link:hover {
background-color: inherit; }
-.LP-Form--inline .LP-Form__Legend, .LP-Form--inline .LP-Input__Label {
+.LP-Form--tagging {
+ margin-top: 25px; }
+ .LP-Form--tagging div.LP-Form__Composition {
+ gap: 25px; }
+
+.LP-Form--inline .LP-Form__Legend,
+.LP-Form--inline .LP-Input__Label {
display: none; }
.LP-Form--inline .LP-Form__Button {
@@ -1198,6 +1224,12 @@ body {
width: min-content;
flex-basis: max-content; }
+.LP-Form--inline fieldset.LP-Form__Fieldset {
+ max-width: unset; }
+
+.LP-Form--inline div.LP-Form__Composition {
+ padding: 0; }
+
@media (max-width: 450px) {
.LP-Form:not(.LP-Form--inline) .LP-Form__Composition {
flex-wrap: wrap; } }
@@ -1359,25 +1391,23 @@ body {
border: none; }
.LP-ImageGrid__Container {
gap: 10px; }
- .LP-ImageGrid .LP-ImageGrid__Item {
- box-shadow: 0 0 10px #565656; }
- .LP-ImageGrid .LP-ImageGrid__Item, .LP-ImageGrid .LP-ImageGrid__Item * {
- overflow: hidden;
- word-break: break-all; }
- .LP-ImageGrid .LP-ImageGrid__Item img {
- width: 100%;
- height: 100%;
- object-fit: cover; }
- .LP-ImageGrid .LP-ImageGrid__Item--left img {
- object-position: left; }
- .LP-ImageGrid .LP-ImageGrid__Item--center img {
- object-position: center; }
- .LP-ImageGrid .LP-ImageGrid__Item--top img {
- object-position: top; }
- .LP-ImageGrid .LP-ImageGrid__Item--bottom img {
- object-position: botom; }
- .LP-ImageGrid .LP-ImageGrid__Item--center img {
- object-position: center; }
+ .LP-ImageGrid .LP-ImageGrid__Item, .LP-ImageGrid .LP-ImageGrid__Item * {
+ overflow: hidden;
+ word-break: break-all; }
+ .LP-ImageGrid .LP-ImageGrid__Item img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover; }
+ .LP-ImageGrid .LP-ImageGrid__Item--left img {
+ object-position: left; }
+ .LP-ImageGrid .LP-ImageGrid__Item--center img {
+ object-position: center; }
+ .LP-ImageGrid .LP-ImageGrid__Item--top img {
+ object-position: top; }
+ .LP-ImageGrid .LP-ImageGrid__Item--bottom img {
+ object-position: botom; }
+ .LP-ImageGrid .LP-ImageGrid__Item--center img {
+ object-position: center; }
.LP-MainContainer {
margin: 0 auto;
@@ -1413,3 +1443,115 @@ body {
margin: 0;
padding: 0;
margin-bottom: 25px; } }
+
+.tagify {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap; }
+ .tagify + input,
+ .tagify + textarea {
+ display: none; }
+
+.tagify__tag {
+ background-color: #bdbdbd;
+ display: inline-flex;
+ cursor: default;
+ transition: .13s ease-out;
+ height: max-content;
+ align-items: center;
+ gap: 3px; }
+ .tagify__tag:hover {
+ background-color: #e9e9e9; }
+
+.tagify__input {
+ flex-grow: 1;
+ display: inline-block;
+ min-width: 110px;
+ margin: 5px;
+ line-height: inherit;
+ position: relative;
+ white-space: pre-wrap;
+ margin-left: 15px; }
+
+.tagify__tag__removeBtn {
+ order: 5;
+ cursor: pointer;
+ font: 1em/1 Arial;
+ transition: .2s ease-out;
+ color: #76323F; }
+
+.tagify__tag__removeBtn::after {
+ content: "\00D7"; }
+
+.tagify__tag__removeBtn:hover {
+ color: #565656; }
+
+.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, 0.3) inset !important;
+ box-shadow: 0 0 0 var(--tag-inset-shadow-size) var(--tag-remove-bg) inset !important;
+ transition: .2s; }
+
+.tagify__tag--loading .tagify__tag__removeBtn {
+ display: none; }
+
+.tagify[readonly]:not(.tagify--mix) .tagify__tag__removeBtn {
+ display: none; }
+
+.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), 0.1);
+ font-size: .9em; }
+
+.tagify__dropdown[position=text] .tagify__dropdown__wrapper {
+ border-width: 1px; }
+
+.tagify__dropdown__wrapper {
+ max-height: 300px;
+ overflow: hidden;
+ background-color: #f9f9f9;
+ box-shadow: 0 2px 4px -2px rgba(0, 0, 0, 0.2);
+ transition: 0.25s cubic-bezier(0, 1, 0.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;
+ font-family: Montserrat, Helvetica, sans-serif; }
+
+.tagify__dropdown__item--active {
+ color: #f9f9f9;
+ background-color: gray; }
+
+.tagify__dropdown__item:active {
+ filter: brightness(105%); }
diff --git a/lostplaces/templates/403.html b/lostplaces/lostplaces_app/templates/403.html
similarity index 100%
rename from lostplaces/templates/403.html
rename to lostplaces/lostplaces_app/templates/403.html
diff --git a/lostplaces/lostplaces_app/templates/global.html b/lostplaces/lostplaces_app/templates/global.html
index ee681f6..e657bf5 100644
--- a/lostplaces/lostplaces_app/templates/global.html
+++ b/lostplaces/lostplaces_app/templates/global.html
@@ -2,82 +2,85 @@
-
-
-
-
-
- {% block title %}Urban Exploration{% endblock %}
-
-
- {% block additional_head %}
- {% endblock additional_head %}
-
-
-
-
-
-
-
-
-
- {% if messages %}
-
-
- {% for message in messages %}
-
-
+
+
\ No newline at end of file
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 %}
-
+
{{field.label}}
- {% 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..d9db359
--- /dev/null
+++ b/lostplaces/lostplaces_app/templates/partials/tagging.html
@@ -0,0 +1,53 @@
+
+
+ {% for tag in tag_list %}
+
+
+
+ {% endfor %}
+
+
+
+
+
+
\ 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 ce6d90c..b687a1e 100644
--- a/lostplaces/lostplaces_app/templates/place/place_detail.html
+++ b/lostplaces/lostplaces_app/templates/place/place_detail.html
@@ -6,7 +6,7 @@
{% block additional_head %}
-
+
@@ -17,109 +17,92 @@
{% block title %}{{place.name}}{% endblock %}
{% block additional_menu_items %}
-
-
+
+
{% endblock additional_menu_items %}
{% block maincontent %}
-
+
-
-
{{ place.description }}
-
+
+
{{ place.description }}
+
-
- Map-Links
- {% include 'partials/osm_map.html' %}
-
-
+
-
-
- 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/templates/registration/login.html b/lostplaces/lostplaces_app/templates/registration/login.html
similarity index 100%
rename from lostplaces/templates/registration/login.html
rename to lostplaces/lostplaces_app/templates/registration/login.html
diff --git a/lostplaces/templates/signup.html b/lostplaces/lostplaces_app/templates/signup.html
similarity index 100%
rename from lostplaces/templates/signup.html
rename to lostplaces/lostplaces_app/templates/signup.html
diff --git a/lostplaces/lostplaces_app/templatetags/svg_icon.py b/lostplaces/lostplaces_app/templatetags/svg_icon.py
index 7090b6c..beb0038 100644
--- a/lostplaces/lostplaces_app/templatetags/svg_icon.py
+++ b/lostplaces/lostplaces_app/templatetags/svg_icon.py
@@ -1,11 +1,12 @@
-import json
+import json, os
from importlib import import_module
from django.core.cache import cache
from django.conf import settings
from django.template import Library, TemplateSyntaxError
-icons_json_path = getattr(settings, 'SVG_ICONS_SOURCE_FILE')
+#icons_json_path = getattr(settings, 'SVG_ICONS_SOURCE_FILE')
+icons_json_path = os.path.join(settings.BASE_DIR, 'lostplaces_app', 'static', 'icons', 'icons.icomoon.json')
icons_json = json.load(open(icons_json_path))
register = Library()
diff --git a/lostplaces/lostplaces_app/urls.py b/lostplaces/lostplaces_app/urls.py
index b319793..d5d7b15 100644
--- a/lostplaces/lostplaces_app/urls.py
+++ b/lostplaces/lostplaces_app/urls.py
@@ -8,7 +8,8 @@ from .views import (
PlaceUpdateView,
PlaceDeleteView,
PhotoAlbumCreateView,
- PhotoAlbumDeleteView
+ PhotoAlbumDeleteView,
+ PlaceTagSubmitView
)
urlpatterns = [
@@ -20,5 +21,6 @@ urlpatterns = [
path('photo_album/delete/
', PhotoAlbumDeleteView.as_view(), name='photo_album_delete'),
path('place/update//', PlaceUpdateView.as_view(), name='place_edit'),
path('place/delete//', PlaceDeleteView.as_view(), name='place_delete'),
- path('place/', PlaceListView.as_view(), name='place_list')
+ path('place/', PlaceListView.as_view(), name='place_list'),
+ path('place/tag/', PlaceTagSubmitView.as_view(), name='place_tag_submit'),
]
diff --git a/lostplaces/lostplaces_app/views.py b/lostplaces/lostplaces_app/views.py
deleted file mode 100644
index bd07381..0000000
--- a/lostplaces/lostplaces_app/views.py
+++ /dev/null
@@ -1,245 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-''' Django views. '''
-from django.shortcuts import render, redirect, get_object_or_404
-from django.urls import reverse_lazy
-from django.views.generic.edit import CreateView, UpdateView, DeleteView
-from django.views.generic.detail import SingleObjectMixin
-from django.views.generic import ListView
-from django.views import View
-from django.http import Http404
-from django.contrib import messages
-from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
-
-from django.contrib.messages.views import SuccessMessageMixin
-
-from .forms import (
- ExplorerCreationForm,
- PlaceForm,
- PlaceImageCreateForm
-)
-from .models import Place, PlaceImage, Voucher, PhotoAlbum
-
-# Create your views here.
-
-# BaseView that checks if user is logged in.
-class IsAuthenticated(LoginRequiredMixin, View):
- redirect_field_name = 'redirect_to'
- login_required_message = 'Please login to proceed'
-
- def handle_no_permission(self):
- messages.error(self.request, self.login_required_message)
- return super().handle_no_permission()
-
-# BaseView that checks if logged in user is submitter of place.
-class IsPlaceSubmitter(UserPassesTestMixin, View):
- place_submitter_error_message = None
-
- def get_place(self):
- pass
-
- def test_func(self):
- """ Check if user is eligible to modify place. """
-
- if not hasattr(self.request, 'user'):
- return False
-
- if self.request.user.is_superuser:
- return True
-
- # Check if currently logged in user was the submitter
- place_obj = self.get_place()
-
- if place_obj and hasattr(place_obj, 'submitted_by') and self.request.user == place_obj.submitted_by:
- return True
-
- if self.place_submitter_error_message:
- messages.error(self.request, self.place_submitter_error_message)
- return False
-
-class SignUpView(SuccessMessageMixin, CreateView):
- form_class = ExplorerCreationForm
- success_url = reverse_lazy('login')
- template_name = 'signup.html'
- success_message = 'User created.'
-
-class PlaceListView(IsAuthenticated, ListView):
- paginate_by = 5
- model = Place
- template_name = 'place/place_list.html'
- ordering = ['name']
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context['place_map_center'] = Place.average_latlon(context['place_list'])
- return context
-
-class PlaceDetailView(IsAuthenticated, View):
- def get(self, request, pk):
- place = Place.objects.get(pk=pk)
- context = {
- 'place': place,
- 'place_list': [ place ],
- 'place_map_center': [ place.latitude, place.longitude ]
- }
- return render(request, 'place/place_detail.html', context)
-
-class HomeView(View):
- def get(self, request, *args, **kwargs):
- place_list = Place.objects.all().order_by('-submitted_when')[:10]
- place_map_center = Place.average_latlon(place_list)
- context = {
- 'place_list': place_list,
- 'place_map_center': place_map_center
- }
- return render(request, 'home.html', context)
-
-class PlaceUpdateView(IsAuthenticated, IsPlaceSubmitter, SuccessMessageMixin, UpdateView):
- template_name = 'place/place_update.html'
- model = Place
- form_class = PlaceForm
- success_message = 'Successfully updated place.'
- place_submitter_error_message = 'You do no have permissions to alter this place'
-
- def get_success_url(self):
- return reverse_lazy('place_detail', kwargs={'pk':self.get_object().pk})
-
- def get_place(self):
- return self.get_object()
-
-class PlaceCreateView(IsAuthenticated, View):
-
- def get(self, request, *args, **kwargs):
- place_image_form = PlaceImageCreateForm()
- place_form = PlaceForm()
-
- context = {
- 'place_form': place_form,
- 'place_image_form': place_image_form
- }
- return render(request, 'place/place_create.html', context)
-
- def post(self, request, *args, **kwargs):
- place_form = PlaceForm(request.POST)
-
- if place_form.is_valid():
- submitter = request.user
- place = place_form.save(commit=False)
- # Save logged in user as "submitted_by"
- place.submitted_by = submitter
- place.save()
-
- if request.FILES:
- self._apply_multipart_image_upload(
- files=request.FILES.getlist('filename'),
- place=place,
- submitter=submitter
- )
-
- kwargs_to_pass = {
- 'pk': place.pk
- }
-
- messages.success(
- self.request, 'Successfully created place.')
- return redirect(reverse_lazy('place_detail', kwargs=kwargs_to_pass))
-
- else:
- context = {
- 'form': form_place
- }
-
- # Usually the browser should have checked the form before sending.
- messages.error(
- self.request, 'Please fill in all required fields.')
- return render(request, 'place/place_create.html', context)
-
- def _apply_multipart_image_upload(self, files, place, submitter):
- for image in files:
- place_image = PlaceImage.objects.create(
- filename=image,
- place=place,
- submitted_by=submitter
- )
- place_image.save()
-
-class PlaceDeleteView(IsAuthenticated, IsPlaceSubmitter, DeleteView):
- template_name = 'place/place_delete.html'
- model = Place
- success_message = 'Successfully deleted place.'
- success_url = reverse_lazy('place_list')
- success_message = 'Place deleted'
- place_submitter_error_message = 'You do no have permission to delete this place'
-
- def delete(self, request, *args, **kwargs):
- messages.success(self.request, self.success_message)
- return super().delete(request, *args, **kwargs)
-
- def get_place(self):
- return self.get_object()
-
-class AlbumCreateView(IsAuthenticated, View):
- def get(self, request, *args, **kwargs):
- url = request.GET['url']
- place_id = request.GET['place_id']
- place = Place.objects.get(pk=place_id)
- photo_album = PhotoAlbum()
- photo_album.url = url
- photo_album.place = place
- photo_album.submitted_by = request.user
- photo_album.save()
- print(photo_album)
- return redirect(reverse_lazy('place_detail', kwargs={'pk': place_id}))
-
-class PhotoAlbumCreateView(IsAuthenticated, SuccessMessageMixin, CreateView):
- model = PhotoAlbum
- fields = ['url', 'label']
- template_name = 'photo_album/photo_album_create.html'
- success_message = 'Photo Album submitted'
-
- def get(self, request, place_id, *args, **kwargs):
- self.place = Place.objects.get(pk=place_id)
- return super().get(request, *args, **kwargs)
-
- def post(self, request, place_id, *args, **kwargs):
- self.place = Place.objects.get(pk=place_id)
- response = super().post(request, *args, **kwargs)
- self.object.place = self.place
- self.object.submitted_by = request.user
- self.object.save()
- return response
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context['place'] = self.place
- return context
-
- def get_success_url(self):
- return reverse_lazy('place_detail', kwargs={'pk': self.place.id})
-
-class PhotoAlbumDeleteView(IsAuthenticated, IsPlaceSubmitter, SingleObjectMixin, View):
- model = PhotoAlbum
- pk_url_kwarg = 'pk'
- success_message = 'Photo Album deleted'
-
- def get_place(self):
- place_id = self.get_object().place.id
- return Place.objects.get(pk=place_id)
-
- def test_func(self):
- can_edit_place = super().test_func()
- if can_edit_place:
- return True
-
- if self.get_object().submitted_by == self.request.user:
- return True
-
- messages.error(self.request, 'You do not have permissions to alter this photo album')
- return False
-
- def get(self, request, *args, **kwargs):
- place_id = self.get_object().place.id
- self.get_object().delete()
- messages.success(self.request, self.success_message)
- return redirect(reverse_lazy('place_detail', kwargs={'pk': place_id}))
diff --git a/lostplaces/lostplaces_app/views/__init__.py b/lostplaces/lostplaces_app/views/__init__.py
new file mode 100644
index 0000000..72591e2
--- /dev/null
+++ b/lostplaces/lostplaces_app/views/__init__.py
@@ -0,0 +1,3 @@
+from lostplaces_app.views.base_views import *
+from lostplaces_app.views.views import *
+from lostplaces_app.views.place_views import *
\ No newline at end of file
diff --git a/lostplaces/lostplaces_app/views/base_views.py b/lostplaces/lostplaces_app/views/base_views.py
new file mode 100644
index 0000000..4eeded9
--- /dev/null
+++ b/lostplaces/lostplaces_app/views/base_views.py
@@ -0,0 +1,98 @@
+from django.views import View
+from django.views.generic.edit import CreateView
+from django.views.generic.detail import SingleObjectMixin
+
+from django.contrib import messages
+from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
+from django.contrib.messages.views import SuccessMessageMixin
+
+from django.shortcuts import redirect
+from django.urls import reverse_lazy
+
+from lostplaces_app.models import Place
+
+class IsAuthenticated(LoginRequiredMixin, View):
+ redirect_field_name = 'redirect_to'
+ permission_denied_message = 'Please login to proceed'
+
+ def handle_no_permission(self):
+ messages.error(self.request, self.permission_denied_message)
+ return super().handle_no_permission()
+
+class IsPlaceSubmitter(UserPassesTestMixin, View):
+ place_submitter_error_message = None
+
+ def get_place(self):
+ pass
+
+ def test_func(self):
+ """ Check if user is eligible to modify place. """
+
+ if not hasattr(self.request, 'user'):
+ return False
+
+ if self.request.user.is_superuser:
+ return True
+
+ # Check if currently logged in user was the submitter
+ place_obj = self.get_place()
+
+ if place_obj and hasattr(place_obj, 'submitted_by') and self.request.user == place_obj.submitted_by:
+ return True
+
+ if self.place_submitter_error_message:
+ messages.error(self.request, self.place_submitter_error_message)
+ return False
+
+class PlaceAssetCreateView(IsAuthenticated, SuccessMessageMixin, CreateView):
+ model = None
+ fields = []
+ template_name = ''
+ success_message = ''
+
+ def get(self, request, place_id, *args, **kwargs):
+ self.place = Place.objects.get(pk=place_id)
+ return super().get(request, *args, **kwargs)
+
+ def post(self, request, place_id, *args, **kwargs):
+ self.place = Place.objects.get(pk=place_id)
+ response = super().post(request, *args, **kwargs)
+ self.object.place = self.place
+ self.object.submitted_by = request.user
+ self.object.save()
+ return response
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['place'] = self.place
+ return context
+
+ def get_success_url(self):
+ return reverse_lazy('place_detail', kwargs={'pk': self.place.id})
+
+class PlaceAssetDeleteView(IsAuthenticated, IsPlaceSubmitter, SingleObjectMixin, View):
+ model = None
+ pk_url_kwarg = 'pk'
+ success_message = ''
+ permission_denied_message = ''
+
+ def get_place(self):
+ place_id = self.get_object().place.id
+ return Place.objects.get(pk=place_id)
+
+ def test_func(self):
+ can_edit_place = super().test_func()
+ if can_edit_place:
+ return True
+
+ if self.get_object().submitted_by == self.request.user:
+ return True
+
+ messages.error(self.request, self.permission_denied_message)
+ return False
+
+ def get(self, request, *args, **kwargs):
+ place_id = self.get_object().place.id
+ self.get_object().delete()
+ messages.success(self.request, self.success_message)
+ return redirect(reverse_lazy('place_detail', kwargs={'pk': place_id}))
\ No newline at end of file
diff --git a/lostplaces/lostplaces_app/views/place_views.py b/lostplaces/lostplaces_app/views/place_views.py
new file mode 100644
index 0000000..30a7123
--- /dev/null
+++ b/lostplaces/lostplaces_app/views/place_views.py
@@ -0,0 +1,123 @@
+from django.views import View
+from django.views.generic.edit import CreateView, UpdateView, DeleteView
+from django.views.generic.detail import SingleObjectMixin
+from django.views.generic import ListView
+
+from django.contrib import messages
+from django.contrib.messages.views import SuccessMessageMixin
+
+from django.shortcuts import render, redirect
+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, TagSubmitForm
+
+from taggit.models import Tag
+
+class PlaceListView(IsAuthenticated, ListView):
+ paginate_by = 5
+ model = Place
+ template_name = 'place/place_list.html'
+ ordering = ['name']
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['place_map_center'] = Place.average_latlon(context['place_list'])
+ return context
+
+class PlaceDetailView(IsAuthenticated, View):
+ def get(self, request, pk):
+ place = Place.objects.get(pk=pk)
+ context = {
+ 'place': place,
+ 'place_list': [ place ],
+ 'place_map_center': [ place.latitude, place.longitude ],
+ 'tagging_form': TagSubmitForm(),
+ 'all_tags': Tag.objects.all()
+ }
+ return render(request, 'place/place_detail.html', context)
+
+class PlaceUpdateView(IsAuthenticated, IsPlaceSubmitter, SuccessMessageMixin, UpdateView):
+ template_name = 'place/place_update.html'
+ model = Place
+ form_class = PlaceForm
+ success_message = 'Successfully updated place.'
+ place_submitter_error_message = 'You do no have permissions to alter this place'
+
+ def get_success_url(self):
+ return reverse_lazy('place_detail', kwargs={'pk':self.get_object().pk})
+
+ def get_place(self):
+ return self.get_object()
+
+class PlaceCreateView(IsAuthenticated, View):
+
+ def get(self, request, *args, **kwargs):
+ place_image_form = PlaceImageCreateForm()
+ place_form = PlaceForm()
+
+ context = {
+ 'place_form': place_form,
+ 'place_image_form': place_image_form
+ }
+ return render(request, 'place/place_create.html', context)
+
+ def post(self, request, *args, **kwargs):
+ place_form = PlaceForm(request.POST)
+
+ if place_form.is_valid():
+ submitter = request.user
+ place = place_form.save(commit=False)
+ # Save logged in user as "submitted_by"
+ place.submitted_by = submitter
+ place.save()
+
+ if request.FILES:
+ self._apply_multipart_image_upload(
+ files=request.FILES.getlist('filename'),
+ place=place,
+ submitter=submitter
+ )
+
+ kwargs_to_pass = {
+ 'pk': place.pk
+ }
+
+ messages.success(
+ self.request, 'Successfully created place.')
+ return redirect(reverse_lazy('place_detail', kwargs=kwargs_to_pass))
+
+ else:
+ context = {
+ 'form': form_place
+ }
+
+ # Usually the browser should have checked the form before sending.
+ messages.error(
+ self.request, 'Please fill in all required fields.')
+ return render(request, 'place/place_create.html', context)
+
+ def _apply_multipart_image_upload(self, files, place, submitter):
+ for image in files:
+ place_image = PlaceImage.objects.create(
+ filename=image,
+ place=place,
+ submitted_by=submitter
+ )
+ place_image.save()
+
+class PlaceDeleteView(IsAuthenticated, IsPlaceSubmitter, DeleteView):
+ template_name = 'place/place_delete.html'
+ model = Place
+ success_message = 'Successfully deleted place.'
+ success_url = reverse_lazy('place_list')
+ success_message = 'Place deleted'
+ place_submitter_error_message = 'You do no have permission to delete this place'
+
+ def delete(self, request, *args, **kwargs):
+ messages.success(self.request, self.success_message)
+ return super().delete(request, *args, **kwargs)
+
+ def get_place(self):
+ return self.get_object()
\ No newline at end of file
diff --git a/lostplaces/lostplaces_app/views/views.py b/lostplaces/lostplaces_app/views/views.py
new file mode 100644
index 0000000..5725b58
--- /dev/null
+++ b/lostplaces/lostplaces_app/views/views.py
@@ -0,0 +1,58 @@
+from django.views import View
+from django.views.generic.edit import CreateView
+
+from django.contrib.messages.views import SuccessMessageMixin
+from django.contrib import messages
+from django.urls import reverse_lazy
+from django.shortcuts import render, redirect
+
+from lostplaces_app.forms import ExplorerCreationForm, TagSubmitForm
+from lostplaces_app.models import Place, PhotoAlbum
+from lostplaces_app.views.base_views import IsAuthenticated
+
+from lostplaces_app.views.base_views import (
+ PlaceAssetCreateView,
+ PlaceAssetDeleteView
+)
+class SignUpView(SuccessMessageMixin, CreateView):
+ form_class = ExplorerCreationForm
+ success_url = reverse_lazy('login')
+ template_name = 'signup.html'
+ success_message = 'User created.'
+
+class HomeView(View):
+ def get(self, request, *args, **kwargs):
+ place_list = Place.objects.all().order_by('-submitted_when')[:10]
+ place_map_center = Place.average_latlon(place_list)
+ context = {
+ 'place_list': place_list,
+ 'place_map_center': place_map_center
+ }
+ return render(request, 'home.html', context)
+
+class PhotoAlbumCreateView(PlaceAssetCreateView):
+ model = PhotoAlbum
+ fields = ['url', 'label']
+ template_name = 'photo_album/photo_album_create.html'
+ success_message = 'Photo Album submitted'
+
+class PhotoAlbumDeleteView(PlaceAssetDeleteView):
+ model = PhotoAlbum
+ pk_url_kwarg = 'pk'
+ 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}))