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 user.is_authenticated %} - Hi {{ user.username }}! - logout - {% if user.is_superuser %} - | admin - {% endif %} - - {% else %} - You are not logged in. - login | - signup - {% endif %} - -
-
- - - -
- {% if messages %} -
-
    - {% for message in messages %} -
  • -
    -
    -
    -
    -
    -
    - {{ message }} + + {% block additional_head %} + {% endblock additional_head %} + + + + + + + {% block title %}Urban Exploration{% endblock %} + + + + + +
    +
    + +
    + + {% if user.is_authenticated %} + Hi {{ user.username }}! + logout + {% if user.is_superuser %} + | admin + {% endif %} + + {% else %} + You are not logged in. + login | + signup + {% endif %} + +
    +
    + + + +
    + {% if messages %} +
    +
      + {% for message in messages %} +
    • +
      +
      +
      -
    • - {% endfor %} -
    -
    - {% endif %} - {% block maincontent %} - {% endblock maincontent %} -
    -
    - +
    + {{ message }} +
    +
    +
  • + {% endfor %} +
+
+ {% endif %} + {% block maincontent %} + {% endblock maincontent %} +
+
+ + \ 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 %} -
+
- {% 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 @@ +
+ +
+ +
+
+ Tags hinzufügen + {% csrf_token %} +
+
+ +
+
+ {% include 'partials/form/inputField.html' with field=input_field 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 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 %} -
  • 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

    -
    - -
    -
    +
    +

    Bilder

    +
    + +
    +
    {% 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}))