Add Profile.

This commit is contained in:
Casper V. Kristensen 2020-09-24 21:13:07 +02:00
parent fad08758ce
commit 0bf64182f5
Signed by: caspervk
GPG key ID: 289CA03790535054
10 changed files with 211 additions and 9 deletions

View file

@ -1,5 +1,5 @@
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import path from django.urls import path, reverse_lazy
from . import views from . import views
@ -8,7 +8,7 @@ urlpatterns = [
# https://docs.djangoproject.com/en/2.2/topics/auth/default/#module-django.contrib.auth.views # 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("login/", auth_views.LoginView.as_view(), name="login"),
path("logout/", auth_views.LogoutView.as_view(), name="logout"), 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"), path("signup/", views.SignupView.as_view(), name="signup"),
] ]

View file

@ -32,7 +32,7 @@
{% endif %} {% endif %}
</div> </div>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<span class="navbar-text">Logged in as <strong>{{ user.username }}</strong></span> <span class="navbar-text">Logged in as <a class="font-weight-bold" href="{% url 'user_profile' %}">{{ user.username }}</a></span>
<a class="btn btn-outline-danger ml-3" role="button" href="{% url 'logout' %}">Log Out</a> <a class="btn btn-outline-danger ml-3" role="button" href="{% url 'logout' %}">Log Out</a>
{% else %} {% else %}
<a class="btn btn-outline-info mr-1" role="button" href="{% url 'signup' %}">Sign Up</a> <a class="btn btn-outline-info mr-1" role="button" href="{% url 'signup' %}">Sign Up</a>

View file

@ -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 %}
<form {{ flat_attrs|safe }} method="{{ form_method }}" {% if formset.is_multipart %} enctype="multipart/form-data"{% endif %}>
{% endif %}
{% if formset_method|lower == 'post' and not disable_csrf %}
{% csrf_token %}
{% endif %}
<div>
{{ formset.management_form|crispy }}
</div>
<table{% if form_id %} id="{{ form_id }}_table"{% endif%} class="table table-sm">
<thead>
{% if formset.readonly and not formset.queryset.exists %}
{% else %}
<tr>
{% for field in formset.forms.0 %}
{% if field.label and not field.is_hidden %}
<th for="{{ field.auto_id }}" class="col-form-label {% if field.field.required %}requiredField{% endif %}">
{{ field.label|safe }}{% if field.field.required and not field|is_checkbox %}<span class="asteriskField">*</span>{% endif %}
</th>
{% endif %}
{% endfor %}
</tr>
{% endif %}
</thead>
<tbody>
<tr class="d-none empty-form">
{% for field in formset.empty_form %}
{% include 'bootstrap4/field.html' with tag="td" form_show_labels=False %}
{% endfor %}
</tr>
{% for form in formset %}
{% if form_show_errors and not form.is_extra %}
{% include "bootstrap4/errors.html" %}
{% endif %}
<tr>
{% for field in form %}
{% include 'bootstrap4/field.html' with tag="td" form_show_labels=False %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% include "bootstrap4/inputs.html" %}
{% if formset_tag %}</form>{% endif %}
{% endspecialspaceless %}

View file

@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from .forms import UserCharactersInlineFormSet
from .models import User, Character, Rank from .models import User, Character, Rank
@ -12,6 +13,7 @@ class RankAdmin(admin.ModelAdmin):
class CharacterInline(admin.TabularInline): class CharacterInline(admin.TabularInline):
model = Character model = Character
formset = UserCharactersInlineFormSet # disallows deleting user's main character
fields = ["name", "klass", "role"] fields = ["name", "klass", "role"]
extra = 0 extra = 0

View file

@ -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 .models import Character
from ..users.models import User
class CharacterForm(ModelForm): class CharacterForm(ModelForm):
@ -8,3 +12,51 @@ class CharacterForm(ModelForm):
model = Character model = Character
fields = ["name", "klass", "role"] 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

View file

@ -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'),
),
]

View file

@ -33,8 +33,7 @@ class User(AbstractUser):
main = models.OneToOneField( main = models.OneToOneField(
"Character", "Character",
related_name="+", related_name="+",
on_delete=models.CASCADE, on_delete=models.CASCADE # has to cascade to allow User deletion, since 'main' Character is circularly related
blank=True
) )
rank = models.ForeignKey( rank = models.ForeignKey(

View file

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block title %}Profile{% endblock %}
{% block content %}
<div class="container">
<div class="card mb-3">
<h5 class="card-header">Profile</h5>
<div class="card-body">
<form method="post" action="{% url 'user_profile' %}">
{% crispy user_form %}
<div class="d-flex justify-content-end">
<a class="btn btn-outline-info float-right mr-2" role="button" href="{% url 'password_change' %}">Change Password</a>
<input class="btn btn-primary float-right" type="submit" name="submit" value="Save">
</div>
</form>
<div class="my-4"></div>
<h5>Characters</h5>
<hr>
<form method="post" class="table-borderless" action="{% url 'user_profile' %}">
{% crispy user_characters_formset user_characters_formset_helper %}
<input class="btn btn-primary float-right" type="submit" name="submit" value="Save">
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -4,4 +4,5 @@ from . import views
urlpatterns = [ urlpatterns = [
path("attendance/", views.AttendanceView.as_view(), name="attendance"), path("attendance/", views.AttendanceView.as_view(), name="attendance"),
path("profile/", views.UserProfileView.as_view(), name="user_profile"),
] ]

View file

@ -1,11 +1,16 @@
from contextlib import suppress from contextlib import suppress
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Avg, Q, Value from django.db.models import Avg, Q, Value
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.generic.detail import SingleObjectMixin
from .forms import UserForm, UserCharactersFormSet, UserCharactersFormSetHelper
from .models import User from .models import User
from ..base.views import MultiModelFormView
from ..raids.models import Raid, RaidResponse from ..raids.models import Raid, RaidResponse
@ -47,6 +52,37 @@ class AttendanceView(TemplateView):
return context return context
# CharacterDetailView: class UserProfileView(LoginRequiredMixin, SingleObjectMixin, MultiModelFormView):
#slug_field = "title" template_name = "users/profile.html"
#slug_url_kwarg = "title" 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)