Compare commits
4 commits
05871a7ddf
...
41eff3bec2
Author | SHA1 | Date | |
---|---|---|---|
Casper V. Kristensen | 41eff3bec2 | ||
Casper V. Kristensen | 0bf64182f5 | ||
Casper V. Kristensen | fad08758ce | ||
Casper V. Kristensen | b118881a13 |
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -6,10 +6,8 @@ from django import forms
|
|||
class BankImportForm(forms.Form):
|
||||
import_string = forms.CharField(widget=forms.Textarea)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.layout = Layout(
|
||||
Field("import_string"),
|
||||
Submit("submit", "Import", css_class="btn btn-block btn-primary")
|
||||
)
|
||||
helper = FormHelper()
|
||||
helper.layout = Layout(
|
||||
Field("import_string"),
|
||||
Submit("submit", "Import", css_class="btn btn-block btn-primary"),
|
||||
)
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
{% 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>
|
||||
{% else %}
|
||||
<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 %}
|
|
@ -31,7 +31,7 @@ class RaidResponseForm(ModelForm):
|
|||
type="submit",
|
||||
name="status",
|
||||
value=RaidResponse.Status.SIGNED_UP,
|
||||
css_class=f"btn-block {'btn-success' if not is_signed_up else 'btn-primary'}"
|
||||
css_class=f"btn btn-block {'btn-success' if not is_signed_up else 'btn-primary'}"
|
||||
),
|
||||
css_class="form-group col-md-3"
|
||||
),
|
||||
|
@ -41,7 +41,7 @@ class RaidResponseForm(ModelForm):
|
|||
type="submit",
|
||||
name="status",
|
||||
value=RaidResponse.Status.BACKUP,
|
||||
css_class="btn-warning btn-block",
|
||||
css_class="btn btn-block btn-warning",
|
||||
disabled=self.instance.pk and self.instance.status == RaidResponse.Status.BACKUP
|
||||
),
|
||||
css_class="form-group col-md-2"
|
||||
|
@ -52,7 +52,7 @@ class RaidResponseForm(ModelForm):
|
|||
type="submit",
|
||||
name="status",
|
||||
value=RaidResponse.Status.SIGNED_OFF,
|
||||
css_class="btn-danger btn-block",
|
||||
css_class="btn btn-block btn-danger",
|
||||
disabled=self.instance.pk and self.instance.status == RaidResponse.Status.SIGNED_OFF
|
||||
),
|
||||
css_class="form-group col-md-2"
|
||||
|
@ -72,35 +72,37 @@ class GuestRaidResponseForm(ModelForm):
|
|||
class Meta:
|
||||
model = RaidResponse
|
||||
fields = ["guest_name", "guest_klass", "role", "status"]
|
||||
labels = {
|
||||
"guest_name": "Name",
|
||||
"guest_klass": "Class"
|
||||
}
|
||||
|
||||
helper = FormHelper()
|
||||
helper.layout = Layout(
|
||||
Row(
|
||||
Column("guest_name", css_class="col-12 col-md-3"),
|
||||
Column("guest_klass", css_class="col-12 col-md-2"),
|
||||
Column("role", css_class="col-12 col-md-2"),
|
||||
Column("status", css_class="col-12 col-md-3"),
|
||||
Column(
|
||||
HTML("<label> </label>"), # offsets the button consistent with the other input elements
|
||||
Submit(
|
||||
"submit",
|
||||
"Add",
|
||||
css_class="btn btn-block btn-primary"
|
||||
),
|
||||
css_class="form-group col-md-2"
|
||||
),
|
||||
css_class="form-row"
|
||||
)
|
||||
)
|
||||
helper.render_hidden_fields = True # Include MultiFormMixin's hidden form_id_field_prefix
|
||||
helper.form_tag = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields["status"].initial = RaidResponse.Status.SIGNED_UP
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.layout = Layout(
|
||||
Row(
|
||||
Column("guest_name", css_class="col-12 col-md-3"),
|
||||
Column("guest_klass", css_class="col-12 col-md-2"),
|
||||
Column("role", css_class="col-12 col-md-2"),
|
||||
Column("status", css_class="col-12 col-md-3"),
|
||||
Column(
|
||||
HTML("<label> </label>"), # offsets the button consistent with the other input elements
|
||||
StrictButton(
|
||||
"Add",
|
||||
type="submit",
|
||||
css_class=f"btn-block btn-primary"
|
||||
),
|
||||
css_class="form-group col-md-2"
|
||||
),
|
||||
css_class="form-row"
|
||||
)
|
||||
)
|
||||
self.helper.render_hidden_fields = True # Include MultiFormMixin's hidden form_id_field_prefix
|
||||
self.helper.form_show_labels = True
|
||||
self.helper.form_tag = False
|
||||
|
||||
|
||||
class RaidResponseChangeForm(ModelForm):
|
||||
class Meta:
|
||||
|
@ -118,8 +120,8 @@ class RaidForm(ModelForm):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
|
||||
self.helper = FormHelper()
|
||||
if self.instance.pk is None:
|
||||
submit_button = Submit("submit", "Create", css_class="btn btn-success float-right")
|
||||
else:
|
||||
|
@ -134,16 +136,16 @@ class RaidForm(ModelForm):
|
|||
),
|
||||
submit_button
|
||||
)
|
||||
self.helper.render_hidden_fields = True
|
||||
self.helper.render_hidden_fields = True # Include MultiFormMixin's hidden form_id_field_prefix
|
||||
|
||||
|
||||
class RaidCommentForm(ModelForm):
|
||||
helper = FormHelper()
|
||||
helper.layout = Layout(
|
||||
Field("body", label="lol"),
|
||||
Submit("submit", "Submit"),
|
||||
Field("body"),
|
||||
Submit("submit", "Submit", css_class="btn btn-primary"),
|
||||
)
|
||||
helper.render_hidden_fields = True
|
||||
helper.render_hidden_fields = True # Include MultiFormMixin's hidden form_id_field_prefix
|
||||
helper.form_show_labels = False
|
||||
|
||||
class Meta:
|
||||
|
@ -163,16 +165,14 @@ RaidResponseFormSet = inlineformset_factory(
|
|||
class RaidResponseFormSetHelper(FormHelper):
|
||||
template = "raids/raid_response_table_inline_formset.html"
|
||||
|
||||
def __init__(self, form=None):
|
||||
super().__init__(form)
|
||||
self.layout = Layout(
|
||||
Field("character", css_class="character-select"),
|
||||
Field("guest_name", css_class="guest-name"),
|
||||
Field("guest_klass", css_class="guest-klass"),
|
||||
Field("role", css_class="role-select"),
|
||||
Field("status", css_class="status-select"),
|
||||
Field("group", css_class="group-select"),
|
||||
Field("note"),
|
||||
Field("attendance", css_class="attendance-input", style="width: 10ch"),
|
||||
)
|
||||
self.form_tag = False
|
||||
layout = Layout(
|
||||
Field("character", css_class="character-select"),
|
||||
Field("guest_name", css_class="guest-name"),
|
||||
Field("guest_klass", css_class="guest-klass"),
|
||||
Field("role", css_class="role-select"),
|
||||
Field("status", css_class="status-select"),
|
||||
Field("group", css_class="group-select"),
|
||||
Field("note"),
|
||||
Field("attendance", css_class="attendance-input", style="width: 10ch"),
|
||||
)
|
||||
form_tag = False
|
||||
|
|
|
@ -209,13 +209,13 @@ class RaidChangeView(PermissionRequiredMixin, SingleObjectMixin, MultiModelFormV
|
|||
def get_raid_response_formset_instance(self):
|
||||
return self.object
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("raid_detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["raid_response_formset_helper"] = RaidResponseFormSetHelper()
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("raid_detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
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(
|
||||
"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(
|
||||
|
@ -124,5 +123,12 @@ class Character(models.Model):
|
|||
def clean(self):
|
||||
self.name = self.name.capitalize()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
if self.user.main == self:
|
||||
# Sanity check: this is already checked by the profile and admin forms which allow character deletion, but
|
||||
# we check it again to avoid unintentionally cascading the delete to the entire user.
|
||||
raise ValidationError("Can't delete user's main character.")
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
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 = [
|
||||
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 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)
|
||||
|
|
Loading…
Reference in a new issue