diff --git a/django_lostplaces/lostplaces/admin.py b/django_lostplaces/lostplaces/admin.py index c6f6280..ec58dac 100644 --- a/django_lostplaces/lostplaces/admin.py +++ b/django_lostplaces/lostplaces/admin.py @@ -9,8 +9,6 @@ from django.contrib.auth.admin import UserAdmin from django.utils import timezone from lostplaces.models import * -from lostplaces.forms import ExplorerCreationForm, ExplorerChangeForm - # Register your models here. class VoucherAdmin(admin.ModelAdmin): diff --git a/django_lostplaces/lostplaces/forms.py b/django_lostplaces/lostplaces/forms.py index 9cbb37f..57a3939 100644 --- a/django_lostplaces/lostplaces/forms.py +++ b/django_lostplaces/lostplaces/forms.py @@ -8,12 +8,12 @@ from django.db import models from django.contrib.auth.forms import UserCreationForm, UserChangeForm from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ -from lostplaces.models import Place, PlaceImage, Voucher +from lostplaces.models import Place, PlaceImage, Voucher, Explorer -class ExplorerCreationForm(UserCreationForm): +class SignupVoucherForm(UserCreationForm): class Meta: model = User - fields = ('username', 'email') + fields = ('username', 'email', 'first_name', 'last_name') voucher = forms.CharField( max_length=30, help_text=_('The Voucher you got from an administrator') @@ -35,10 +35,33 @@ class ExplorerCreationForm(UserCreationForm): fetched_voucher.delete() return True -class ExplorerChangeForm(UserChangeForm): +class ExplorerUserChangeForm(UserChangeForm): class Meta: model = User - fields = ('username', 'email') + fields = [ 'username', 'first_name', 'last_name', 'email' ] + ''' Hide password hint.''' + password = None + + ''' Display username, but display it non-editable and not required. ''' + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['username'].required = False + self.fields['username'].help_text = None + self.fields['username'].widget.attrs['disabled'] = 'disabled' + + def clean_username(self): + # As shown in the above answer. + instance = getattr(self, 'instance', None) + if instance: + return instance.username + else: + return self.cleaned_data.get('username', None) + +class ExplorerChangeForm(forms.ModelForm): + class Meta: + model = Explorer + fields = '__all__' + exclude = ['user', 'favorite_places'] class PlaceForm(forms.ModelForm): class Meta: diff --git a/django_lostplaces/lostplaces/locale/de/LC_MESSAGES/django.po b/django_lostplaces/lostplaces/locale/de/LC_MESSAGES/django.po index e0bc7c9..1eaa7a9 100644 --- a/django_lostplaces/lostplaces/locale/de/LC_MESSAGES/django.po +++ b/django_lostplaces/lostplaces/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-11 21:53+0200\n" +"POT-Creation-Date: 2020-12-25 16:04+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Commander1024 \n" "Language-Team: LANGUAGE \n" @@ -30,7 +30,7 @@ msgstr "Ungültiger Voucher" msgid "Expired voucher" msgstr "Abgelaufener Voucher" -#: models/abstract_models.py:29 +#: models/abstract_models.py:29 templates/explorer/profile.html:31 msgid "Name" msgstr "Name" @@ -74,6 +74,22 @@ msgstr "Adresse (URL)" msgid "link text" msgstr "Linktext" +#: models/models.py:47 +msgid "Biography" +msgstr "Beschreibung" + +#: models/models.py:48 +msgid "Describe yourself, your preferences, etc. in a few sentences." +msgstr "Beschreibe Dich selbst, Deine Vorlieben, usw. in ein paar Sätzen." + +#: models/models.py:56 +msgid "Profile image" +msgstr "Profilbild" + +#: models/models.py:57 +msgid "Optional profile image for display in Explorer profile" +msgstr "Optionales Profilbild zur Anzeige im Explorerprofil" + #: models/place.py:21 msgid "Location" msgstr "Ort" @@ -110,38 +126,92 @@ msgstr "Du wirst in 5 Sekunden weitergeleitet" msgid "Go Back" msgstr "Zurück" +#: templates/explorer/profile.html:41 +msgid "E-mail" +msgstr "E-Mail" + +#: templates/explorer/profile.html:52 +msgid "Joined" +msgstr "Beigetreten" + +#: templates/explorer/profile.html:60 +#, fuzzy +#| msgid "All Places" +msgid "Places" +msgstr "Alle Places" + +#: templates/explorer/profile.html:68 +msgid "Place assets" +msgstr "" + +#: templates/explorer/profile.html:76 +#, fuzzy +#| msgid "Edit Explorer profile" +msgid "Edit Profile" +msgstr "Explorerprofil bearbeiten" + +#: templates/explorer/profile.html:87 +msgid "Favorite places" +msgstr "Favoriten" + +#: templates/explorer/profile.html:103 +msgid "Places submitted by" +msgstr "Places hinzugefügt von" + +#: templates/explorer/profile.html:118 +msgid "Images submitted by" +msgstr "Bilder hinzugefügt von" + +#: templates/explorer/profile.html:140 +msgid "Photo albums submitted by" +msgstr "Fotoalben hinzugefügt von" + +#: templates/explorer/profile_update.html:6 +#: templates/explorer/profile_update.html:12 +msgid "Edit Explorer profile" +msgstr "Explorerprofil bearbeiten" + +#: templates/explorer/profile_update.html:48 +#: templates/place/place_update.html:42 +msgid "Update" +msgstr "Aktualisieren" + #: templates/global.html:32 msgid "Logout" msgstr "Ausloggen" -#: templates/global.html:34 +#: templates/global.html:33 +msgid "Profile" +msgstr "Profil" + +#: templates/global.html:35 msgid "Admin" msgstr "Admin" -#: templates/global.html:39 templates/registration/login.html:4 +#: templates/global.html:40 templates/registration/login.html:4 #: templates/registration/login.html:10 templates/registration/login.html:23 msgid "Login" msgstr "Anmelden" -#: templates/global.html:40 templates/registration/login.html:29 -#: templates/signup.html:6 templates/signup.html:12 templates/signup.html:41 +#: templates/global.html:41 templates/registration/login.html:29 +#: templates/signup.html:6 templates/signup.html:12 templates/signup.html:49 msgid "Sign up" msgstr "Registrieren" -#: templates/global.html:50 templates/home.html:10 +#: templates/global.html:51 templates/home.html:10 msgid "Home" msgstr "Startseite" -#: templates/global.html:51 +#: templates/global.html:52 msgid "UrBex Codex" msgstr "UrBex Codex" -#: templates/global.html:56 templates/place/place_create.html:5 +#: templates/global.html:57 templates/place/place_create.html:5 #: templates/place/place_create.html:10 msgid "Create place" msgstr "Place erstellen" -#: templates/global.html:57 +#: templates/global.html:58 msgid "All places" msgstr "Alle Places" @@ -194,6 +264,14 @@ msgstr "Abschicken" msgid "Cancel" msgstr "Abbrechen" +#: templates/partials/icons/place_favorite.html:6 +msgid "Remove from favorites" +msgstr "Aus den Favoriten entfernen" + +#: templates/partials/icons/place_favorite.html:10 +msgid "Save as favorite" +msgstr "In den Favoriten speichern" + #: templates/partials/nav/footer.html:64 msgid "Made by" msgstr "Erstellt von" @@ -318,10 +396,6 @@ msgstr "Alle Places" msgid "Our lost places" msgstr "Unsere Lost Places" -#: templates/place/place_update.html:42 -msgid "Update" -msgstr "Aktualisieren" - #: templates/place_image/place_image_create.html:7 msgid "Submit images to a place" msgstr "Bilder zu einem Place hinzufügen" @@ -334,6 +408,16 @@ msgstr "Noch kein Konto?" msgid "Please login to proceed" msgstr "Bitte log Dich ein um fortzufahren" +#: views/explorer_views.py:78 +#, fuzzy +#| msgid "Successfully updated place" +msgid "Successfully updated Explorer profile" +msgstr "Place erfolgreich aktualisiert" + +#: views/explorer_views.py:84 views/place_views.py:105 +msgid "Please fill in all required fields." +msgstr "Bitte füll alle benötigten Felder aus." + #: views/place_image_views.py:26 msgid "Image(s) submitted successfully" msgstr "Bild(er) erfolgreich hinzugefügt" @@ -358,10 +442,6 @@ msgstr "Du darfst diesen Place nicht bearbeiten" msgid "Successfully created place" msgstr "Place erfolgreich erstellt" -#: views/place_views.py:105 -msgid "Please fill in all required fields." -msgstr "Bitte füll alle benötigten Felder aus." - #: views/place_views.py:112 msgid "Successfully deleted place" msgstr "Place erfolgreich gelöscht" @@ -385,3 +465,8 @@ msgstr "Fotoalbum-Link gelöscht" #: views/views.py:60 msgid "You are not allowed to edit this photo album link" msgstr "Du darfst diesen Fotoalbum-Link nicht bearbeiten" + +#, fuzzy +#~| msgid "Filename(s)" +#~ msgid "Filename" +#~ msgstr "Dateiname(n)" diff --git a/django_lostplaces/lostplaces/models/models.py b/django_lostplaces/lostplaces/models/models.py index 0146c50..2d39ca6 100644 --- a/django_lostplaces/lostplaces/models/models.py +++ b/django_lostplaces/lostplaces/models/models.py @@ -6,20 +6,34 @@ database. ''' +import os import uuid from django.db import models from django.contrib.auth.models import User -from django.db.models.signals import post_save +from django.db.models.signals import post_save, pre_save from django.dispatch import receiver +from django.utils.translation import ugettext_lazy as _ from lostplaces.models.abstract_models import Expireable from lostplaces.models.place import Place +from easy_thumbnails.fields import ThumbnailerImageField +from easy_thumbnails.files import get_thumbnailer + +def generate_profile_image_filename(instance, filename): + """ + Callback for generating filename for uploaded explorer profile images. + Returns filename as: explorer_pk-username.jpg + """ + + return 'explorers/' + str(instance.user.pk) + '-' + str(instance.user.username) + '.' + filename.split('.')[-1] + class Explorer(models.Model): """ - Profile that is linked to the a User. + Profile that is linked to the Django user. Every user has a profile. + Provides additional attributes for user profile. """ user = models.OneToOneField( @@ -27,7 +41,21 @@ class Explorer(models.Model): on_delete=models.CASCADE, related_name='explorer' ) - + bio = models.TextField( + blank=True, + null=True, + verbose_name=_('Biography / Description'), + help_text=_('Describe yourself, your preferences, etc. in a few sentences.') + ) + profile_image = ThumbnailerImageField( + blank=True, + null=True, + upload_to=generate_profile_image_filename, + resize_source=dict(size=(400, 400), + sharpen=True), + verbose_name=_('Profile image'), + help_text=_('Optional profile image for display in Explorer profile') + ) favorite_places = models.ManyToManyField( Place, related_name='explorer_favorites', @@ -37,7 +65,7 @@ class Explorer(models.Model): def __str__(self): return self.user.username - + @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: @@ -47,13 +75,34 @@ def create_user_profile(sender, instance, created, **kwargs): def save_user_profile(sender, instance, **kwargs): instance.explorer.save() +@receiver(pre_save, sender=Explorer) +def auto_delete_file_on_change(sender, instance, **kwargs): + """ + Deletes old file from filesystem + when corresponding `Explorer` object is updated + with new file. + """ + if not instance.pk: + return False + + try: + old_file = Explorer.objects.get(pk=instance.pk).profile_image + except Explorer.DoesNotExist: + return False + print("Deleting:", old_file) + new_file = instance.profile_image + if not old_file == new_file: + old_file.delete(save=False) + class Voucher(Expireable): """ - Vouchers are authorization to created_when = models.DateTimeField(auto_now_add=True) - expires_when = models.DateTimeField()kens to allow the registration of new users. + Vouchers are authorization tokens to allow the registration of new users. A voucher has a code, a creation and a deletion date, which are all positional. Creation date is being set automatically during voucher creation. + + created_when = models.DateTimeField(auto_now_add=True) + expires_when = models.DateTimeField() """ code = models.CharField(unique=True, max_length=30) @@ -64,4 +113,3 @@ class Voucher(Expireable): def __str__(self): return "Voucher " + str(self.code) - diff --git a/django_lostplaces/lostplaces/models/place.py b/django_lostplaces/lostplaces/models/place.py index 588696f..a0d1b99 100644 --- a/django_lostplaces/lostplaces/models/place.py +++ b/django_lostplaces/lostplaces/models/place.py @@ -49,10 +49,10 @@ class Place(Submittable, Taggable, Mapable): return self.name -def generate_image_upload_path(instance, filename): +def generate_place_image_filename(instance, filename): """ - Callback for generating path for uploaded images. - Returns filename as: place_pk-placename{-rnd_string}.jpg + Callback for generating filename for uploaded place images. + Returns filename as: place_pk-placename{-number}.jpg """ return 'places/' + str(instance.place.pk) + '-' + str(instance.place.name) + '.' + filename.split('.')[-1] @@ -72,7 +72,7 @@ class PlaceAsset(Submittable): null=True ) -class PlaceImage (PlaceAsset): +class PlaceImage(PlaceAsset): """ PlaceImage defines an image file object that points to a file in uploads/. Intermediate image sizes are generated as defined in THUMBNAIL_ALIASES. @@ -84,7 +84,7 @@ class PlaceImage (PlaceAsset): verbose_name=_('Description'), ) filename = ThumbnailerImageField( - upload_to=generate_image_upload_path, + upload_to=generate_place_image_filename, resize_source=dict(size=(2560, 2560), sharpen=True), verbose_name=_('Filename(s)'), @@ -104,7 +104,6 @@ class PlaceImage (PlaceAsset): return 'Image ' + str(self.pk) - # These two auto-delete files from filesystem when they are unneeded: @receiver(post_delete, sender=PlaceImage) @@ -118,7 +117,6 @@ def auto_delete_file_on_delete(sender, instance, **kwargs): thumbmanager = get_thumbnailer(instance.filename) thumbmanager.delete(save=False) - @receiver(pre_save, sender=PlaceImage) def auto_delete_file_on_change(sender, instance, **kwargs): """ @@ -137,5 +135,4 @@ def auto_delete_file_on_change(sender, instance, **kwargs): # No need to delete thumbnails, as they will be overwritten on regeneration. new_file = instance.filename if not old_file == new_file: - if os.path.isfile(old_file.path): - os.remove(old_file.path) + old_file.delete(save=False) diff --git a/django_lostplaces/lostplaces/templates/explorer/profile.html b/django_lostplaces/lostplaces/templates/explorer/profile.html index cf4cd63..8bdd07d 100644 --- a/django_lostplaces/lostplaces/templates/explorer/profile.html +++ b/django_lostplaces/lostplaces/templates/explorer/profile.html @@ -7,13 +7,46 @@ {% block maincontent %}
+
+

Bio

+

{{explorer.bio}}

+

{{explorer.user.username}}

+
+ + {% if explorer.profile_image %} +
+ +
+ {% endif %} + + {% if explorer.user.first_name %} + + + {% endif %} + + + {% if explorer.user.email %} + + + {% endif %} + + + +
+ {% trans 'Name' %} + + {{explorer.user.first_name}} {{explorer.user.last_name}} +
+ {% trans 'E-mail' %} + + + {{explorer.user.email}} + +
{% trans 'Joined' %} @@ -32,18 +65,39 @@
- {% trans 'Place Assets'%} + {% trans 'Place assets'%} {{asset_count}}
+ +
+
+
+

{% trans 'Favorite places' %}

+
    + {% for place in explorer.favorite_places.all %} +
  • + {% include 'partials/place_teaser.html' with place=place extended=True %} +
  • + {% endfor %} +
+ + {% include 'partials/nav/pagination.html' %} + +
+
+

{% trans 'Places submitted by' %} {{explorer.user.username}}

diff --git a/django_lostplaces/lostplaces/templates/explorer/profile_update.html b/django_lostplaces/lostplaces/templates/explorer/profile_update.html new file mode 100644 index 0000000..9db8eca --- /dev/null +++ b/django_lostplaces/lostplaces/templates/explorer/profile_update.html @@ -0,0 +1,55 @@ +{% extends 'global.html'%} +{% load i18n %} +{% load thumbnail %} +{% load widget_tweaks %} + +# {% block title %}{% trans 'Edit Explorer profile' %}{% endblock %} + +{% block maincontent %} + +
+
+ {% trans 'Edit Explorer profile' %} + {% csrf_token %} +
+
+ {% include 'partials/form/inputField.html' with field=explorer_user_change_form.username %} +
+
+ {% include 'partials/form/inputField.html' with field=explorer_user_change_form.email %} +
+
+
+
+ {% include 'partials/form/inputField.html' with field=explorer_user_change_form.first_name %} +
+
+ {% include 'partials/form/inputField.html' with field=explorer_user_change_form.last_name %} +
+
+ +
+
+ {% include 'partials/form/inputField.html' with field=explorer_change_form.bio %} +
+
+ +
+ {% if explorer_image %} +
+ +
+ {% endif %} +
+ {% include 'partials/form/inputField.html' with field=explorer_change_form.profile_image %} +
+
+ + {% trans 'Update' as action %} +
+ {% include 'partials/form/submit.html' with referrer=request.META.HTTP_REFERER action=action %} +
+
+
+ +{% endblock maincontent %} diff --git a/django_lostplaces/lostplaces/templates/signup.html b/django_lostplaces/lostplaces/templates/signup.html index c3a3949..895784b 100644 --- a/django_lostplaces/lostplaces/templates/signup.html +++ b/django_lostplaces/lostplaces/templates/signup.html @@ -19,6 +19,14 @@ {% include 'partials/form/inputField.html' with field=form.email %}
+
+
+ {% include 'partials/form/inputField.html' with field=form.first_name %} +
+
+ {% include 'partials/form/inputField.html' with field=form.last_name %} +
+
diff --git a/django_lostplaces/lostplaces/tests/forms/test_explorer_forms.py b/django_lostplaces/lostplaces/tests/forms/test_explorer_forms.py index 89b6986..a2a0d25 100644 --- a/django_lostplaces/lostplaces/tests/forms/test_explorer_forms.py +++ b/django_lostplaces/lostplaces/tests/forms/test_explorer_forms.py @@ -4,15 +4,15 @@ from django import forms from django.utils import timezone from lostplaces.tests.forms import FormTestCase -from lostplaces.forms import ExplorerCreationForm +from lostplaces.forms import SignupVoucherForm from lostplaces.models.models import Voucher -class ExplorerCreationFormTestCase(FormTestCase): +class SignupVoucherFormTestCase(FormTestCase): """ This test case only tests for the voucher since all other aspects don't realy matter to this project and are already tested by django """ - form = ExplorerCreationForm + form = SignupVoucherForm @classmethod def setUpTestData(cls): @@ -37,7 +37,7 @@ class ExplorerCreationFormTestCase(FormTestCase): ) def test_validation_valid(self): - form = ExplorerCreationForm(self.post_data) + form = SignupVoucherForm(self.post_data) self.assertTrue( form.is_valid(), msg='Expecting the %s to validate' % ( @@ -49,7 +49,7 @@ class ExplorerCreationFormTestCase(FormTestCase): self.post_data = { 'voucher': 'Imanotacode123' } - form = ExplorerCreationForm(self.post_data) + form = SignupVoucherForm(self.post_data) self.assertFalse( form.is_valid(), msg='Expecting the %s to not validate' % ( diff --git a/django_lostplaces/lostplaces/urls.py b/django_lostplaces/lostplaces/urls.py index 8071f86..29f5007 100644 --- a/django_lostplaces/lostplaces/urls.py +++ b/django_lostplaces/lostplaces/urls.py @@ -19,6 +19,7 @@ from lostplaces.views import ( PlaceImageDeleteView, FlatView, ExplorerProfileView, + ExplorerProfileUpdateView, OSMMapView ) @@ -40,10 +41,9 @@ urlpatterns = [ path('place/tag/delete//', PlaceTagDeleteView.as_view(), name='place_tag_delete'), path('explorer//', ExplorerProfileView.as_view(), name='explorer_profile'), + path('explorer/update/', ExplorerProfileUpdateView.as_view(), name='explorer_profile_update'), path('explorer/favorite//', PlaceFavoriteView.as_view(), name='place_favorite'), path('explorer/unfavorite//', PlaceUnfavoriteView.as_view(), name='place_unfavorite'), path('osm/', OSMMapView.as_view(), name='osm') - - ] diff --git a/django_lostplaces/lostplaces/views/explorer_views.py b/django_lostplaces/lostplaces/views/explorer_views.py index d96ad45..62033e1 100644 --- a/django_lostplaces/lostplaces/views/explorer_views.py +++ b/django_lostplaces/lostplaces/views/explorer_views.py @@ -7,11 +7,13 @@ from django.utils.translation import ugettext_lazy as _ from django.shortcuts import render, redirect, get_object_or_404 from django.urls import reverse_lazy +from django.contrib import messages from lostplaces.common import get_all_subclasses from lostplaces.views.base_views import IsAuthenticatedMixin from lostplaces.models.models import Explorer from lostplaces.models.place import Place, PlaceAsset +from lostplaces.forms import ExplorerChangeForm, ExplorerUserChangeForm class ExplorerProfileView(IsAuthenticatedMixin, View): def get(self, request, explorer_id): @@ -33,13 +35,53 @@ class ExplorerProfileView(IsAuthenticatedMixin, View): asset_count += objects.count() context['asset_count'] = asset_count - - print(context['assets']) - + return render( request=request, template_name='explorer/profile.html', context=context ) - - \ No newline at end of file + +class ExplorerProfileUpdateView(IsAuthenticatedMixin, View): + success_message = '' + permission_denied_message = '' + + def get(self, request, *args, **kwargs): + context = { + 'explorer_user_change_form': ExplorerUserChangeForm(instance=request.user), + 'explorer_change_form': ExplorerChangeForm(instance=request.user.explorer) + } + if request.user.explorer.profile_image: + context['explorer_image'] = request.user.explorer.profile_image + return render(request, 'explorer/profile_update.html', context) + + def post(self, request, *args, **kwargs): + print(request.POST) + explorer_user_change_form = ExplorerUserChangeForm( + request.POST, + instance=request.user + ) + explorer_change_form = ExplorerChangeForm( + request.POST, + request.FILES, + instance=request.user.explorer + ) + + if explorer_change_form.is_valid() and explorer_user_change_form.is_valid(): + explorer_user_change_form.save() + explorer_change_form.save() + + print(explorer_change_form.cleaned_data) + + messages.success( + self.request, + _('Successfully updated Explorer profile') + ) + else: + # Usually the browser should have checked the form before sending. + messages.error( + self.request, + _('Please fill in all required fields.') + ) + return redirect(reverse_lazy('explorer_profile_update')) + \ No newline at end of file diff --git a/django_lostplaces/lostplaces/views/place_views.py b/django_lostplaces/lostplaces/views/place_views.py index c96f442..379f02e 100644 --- a/django_lostplaces/lostplaces/views/place_views.py +++ b/django_lostplaces/lostplaces/views/place_views.py @@ -145,5 +145,5 @@ class PlaceUnfavoriteView(IsAuthenticatedMixin, View): if request.user is not None: request.user.explorer.favorite_places.remove(place) request.user.explorer.save() - - return redirect_referer_or(request, reverse('place_detail', kwargs={'pk': place.pk})) \ No newline at end of file + + return redirect_referer_or(request, reverse('place_detail', kwargs={'pk': place.pk})) diff --git a/django_lostplaces/lostplaces/views/views.py b/django_lostplaces/lostplaces/views/views.py index bfd995a..74d16a2 100644 --- a/django_lostplaces/lostplaces/views/views.py +++ b/django_lostplaces/lostplaces/views/views.py @@ -11,7 +11,7 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.http import HttpResponseForbidden from django.utils.translation import ugettext_lazy as _ -from lostplaces.forms import ExplorerCreationForm, TagSubmitForm +from lostplaces.forms import SignupVoucherForm, TagSubmitForm from lostplaces.models import Place, PhotoAlbum from lostplaces.views.base_views import IsAuthenticatedMixin from lostplaces.common import redirect_referer_or @@ -24,7 +24,7 @@ from lostplaces.views.base_views import ( from taggit.models import Tag class SignUpView(SuccessMessageMixin, CreateView): - form_class = ExplorerCreationForm + form_class = SignupVoucherForm success_url = reverse_lazy('login') template_name = 'signup.html' success_message = _('User created')