diff --git a/drakul/authentication/urls.py b/drakul/authentication/urls.py index 587434a..7d2d5b9 100644 --- a/drakul/authentication/urls.py +++ b/drakul/authentication/urls.py @@ -1,5 +1,5 @@ from django.contrib.auth import views as auth_views -from django.urls import path +from django.urls import path, reverse_lazy from . import views @@ -8,7 +8,7 @@ urlpatterns = [ # https://docs.djangoproject.com/en/2.2/topics/auth/default/#module-django.contrib.auth.views path("login/", auth_views.LoginView.as_view(), name="login"), path("logout/", auth_views.LogoutView.as_view(), name="logout"), - path("password_change/", auth_views.PasswordChangeView.as_view(success_url="/"), name="password_change"), + path("password_change/", auth_views.PasswordChangeView.as_view(success_url=reverse_lazy("user_profile")), name="password_change"), path("signup/", views.SignupView.as_view(), name="signup"), ] diff --git a/drakul/base/templates/base.html b/drakul/base/templates/base.html index a02ce83..e010936 100644 --- a/drakul/base/templates/base.html +++ b/drakul/base/templates/base.html @@ -32,7 +32,7 @@ {% endif %} {% if user.is_authenticated %} - Logged in as {{ user.username }} + Logged in as {{ user.username }} Log Out {% else %} Sign Up diff --git a/drakul/base/templates/bootstrap4/table_inline_formset.html b/drakul/base/templates/bootstrap4/table_inline_formset.html new file mode 100644 index 0000000..0b10ed6 --- /dev/null +++ b/drakul/base/templates/bootstrap4/table_inline_formset.html @@ -0,0 +1,63 @@ +{% comment %} + Based on Crispy's default bootstrap4/table_inline_formset.html, but removed 'table-striped' + https://github.com/django-crispy-forms/django-crispy-forms/blob/master/crispy_forms/templates/bootstrap4/table_inline_formset.html +{% endcomment %} + + +{% load crispy_forms_tags %} +{% load crispy_forms_utils %} +{% load crispy_forms_field %} + +{% specialspaceless %} +{% if formset_tag %} +
+{% endif %} + {% if formset_method|lower == 'post' and not disable_csrf %} + {% csrf_token %} + {% endif %} + +
+ {{ formset.management_form|crispy }} +
+ + + + {% if formset.readonly and not formset.queryset.exists %} + {% else %} + + {% for field in formset.forms.0 %} + {% if field.label and not field.is_hidden %} + + {{ field.label|safe }}{% if field.field.required and not field|is_checkbox %}*{% endif %} + + {% endif %} + {% endfor %} + + {% endif %} + + + + + {% for field in formset.empty_form %} + {% include 'bootstrap4/field.html' with tag="td" form_show_labels=False %} + {% endfor %} + + + {% for form in formset %} + {% if form_show_errors and not form.is_extra %} + {% include "bootstrap4/errors.html" %} + {% endif %} + + + {% for field in form %} + {% include 'bootstrap4/field.html' with tag="td" form_show_labels=False %} + {% endfor %} + + {% endfor %} + + + + {% include "bootstrap4/inputs.html" %} + +{% if formset_tag %}{% endif %} +{% endspecialspaceless %} diff --git a/drakul/users/admin.py b/drakul/users/admin.py index 6666ed7..17917df 100755 --- a/drakul/users/admin.py +++ b/drakul/users/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin +from .forms import UserCharactersInlineFormSet from .models import User, Character, Rank @@ -12,6 +13,7 @@ class RankAdmin(admin.ModelAdmin): class CharacterInline(admin.TabularInline): model = Character + formset = UserCharactersInlineFormSet # disallows deleting user's main character fields = ["name", "klass", "role"] extra = 0 diff --git a/drakul/users/forms.py b/drakul/users/forms.py index 9bb1884..44a346b 100644 --- a/drakul/users/forms.py +++ b/drakul/users/forms.py @@ -1,6 +1,10 @@ -from django.forms import ModelForm +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Field +from django.core.exceptions import ValidationError, NON_FIELD_ERRORS +from django.forms import ModelForm, inlineformset_factory, BaseInlineFormSet from .models import Character +from ..users.models import User class CharacterForm(ModelForm): @@ -8,3 +12,51 @@ class CharacterForm(ModelForm): model = Character fields = ["name", "klass", "role"] + +class UserForm(ModelForm): + class Meta: + model = User + fields = ["username", "rank", "main"] + + def __init__(self, user, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["rank"].disabled = True + self.fields["main"].queryset = user.characters + + helper = FormHelper() + helper.layout = Layout( + Field("username"), + Field("rank"), + Field("main"), + ) + helper.render_hidden_fields = True # Include MultiFormMixin's hidden form_id_field_prefix + helper.form_tag = False + + +class UserCharactersInlineFormSet(BaseInlineFormSet): + def clean(self): + super().clean() + for form in self.deleted_forms: + if form.instance == self.instance.main: + form._errors[NON_FIELD_ERRORS] = self.error_class(["Can't delete user's main character."]) + raise ValidationError(["Can't delete user's main character."]) + + +UserCharactersFormSet = inlineformset_factory( + User, # parent model + Character, + formset=UserCharactersInlineFormSet, + fields=["name", "klass", "role"], + can_delete=True, + extra=1 +) + + +class UserCharactersFormSetHelper(FormHelper): + template = "bootstrap4/table_inline_formset.html" + layout = Layout( + Field("name"), + Field("klass"), + Field("role") + ) + form_tag = False diff --git a/drakul/users/migrations/0006_auto_20200924_2019.py b/drakul/users/migrations/0006_auto_20200924_2019.py new file mode 100644 index 0000000..f53435b --- /dev/null +++ b/drakul/users/migrations/0006_auto_20200924_2019.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.1 on 2020-09-24 20:19 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_auto_20191121_1646'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='main', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='users.character'), + ), + ] diff --git a/drakul/users/models.py b/drakul/users/models.py index c1046cd..b11e11d 100644 --- a/drakul/users/models.py +++ b/drakul/users/models.py @@ -33,8 +33,7 @@ class User(AbstractUser): main = models.OneToOneField( "Character", related_name="+", - on_delete=models.CASCADE, - blank=True + on_delete=models.CASCADE # has to cascade to allow User deletion, since 'main' Character is circularly related ) rank = models.ForeignKey( diff --git a/drakul/users/templates/users/profile.html b/drakul/users/templates/users/profile.html new file mode 100644 index 0000000..51b98f7 --- /dev/null +++ b/drakul/users/templates/users/profile.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% load crispy_forms_tags %} + +{% block title %}Profile{% endblock %} + + +{% block content %} +
+
+
Profile
+
+
+ {% crispy user_form %} + +
+
+
Characters
+
+
+ {% crispy user_characters_formset user_characters_formset_helper %} + +
+
+
+
+{% endblock %} diff --git a/drakul/users/urls.py b/drakul/users/urls.py index 1c63951..8694698 100644 --- a/drakul/users/urls.py +++ b/drakul/users/urls.py @@ -4,4 +4,5 @@ from . import views urlpatterns = [ path("attendance/", views.AttendanceView.as_view(), name="attendance"), + path("profile/", views.UserProfileView.as_view(), name="user_profile"), ] diff --git a/drakul/users/views.py b/drakul/users/views.py index 39e1a70..1f198e2 100644 --- a/drakul/users/views.py +++ b/drakul/users/views.py @@ -1,11 +1,16 @@ from contextlib import suppress +from django.contrib.auth.mixins import LoginRequiredMixin from django.db.models import Avg, Q, Value from django.db.models.functions import Coalesce +from django.urls import reverse from django.utils import timezone from django.views.generic import TemplateView +from django.views.generic.detail import SingleObjectMixin +from .forms import UserForm, UserCharactersFormSet, UserCharactersFormSetHelper from .models import User +from ..base.views import MultiModelFormView from ..raids.models import Raid, RaidResponse @@ -47,6 +52,37 @@ class AttendanceView(TemplateView): return context -# CharacterDetailView: - #slug_field = "title" - #slug_url_kwarg = "title" +class UserProfileView(LoginRequiredMixin, SingleObjectMixin, MultiModelFormView): + template_name = "users/profile.html" + form_classes = { + "user_form": UserForm, + "user_characters_formset": UserCharactersFormSet + } + + def get_object(self, queryset=None): + return self.request.user + + def get_user_form_instance(self): + return self.object + + def get_user_characters_formset_instance(self): + return self.object + + def get_user_form_kwargs(self, kwargs): + kwargs["user"] = self.request.user + return kwargs + + def get_context_data(self, **kwargs): + kwargs["user_characters_formset_helper"] = UserCharactersFormSetHelper() + return super().get_context_data(**kwargs) + + def get_success_url(self): + return reverse("user_profile") + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + return super().get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + return super().post(request, *args, **kwargs)