Add Profile.
This commit is contained in:
parent
fad08758ce
commit
0bf64182f5
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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>
|
||||||
|
|
63
drakul/base/templates/bootstrap4/table_inline_formset.html
Normal file
63
drakul/base/templates/bootstrap4/table_inline_formset.html
Normal 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 %}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
19
drakul/users/migrations/0006_auto_20200924_2019.py
Normal file
19
drakul/users/migrations/0006_auto_20200924_2019.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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(
|
||||||
|
|
30
drakul/users/templates/users/profile.html
Normal file
30
drakul/users/templates/users/profile.html
Normal 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 %}
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue