Add guest raid responses.
This commit is contained in:
parent
5381795edc
commit
12acc50d5a
|
@ -1,6 +1,6 @@
|
|||
from crispy_forms.bootstrap import StrictButton
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Submit, Layout, Column, Row, Field
|
||||
from crispy_forms.layout import Submit, Layout, Column, Row, Field, HTML
|
||||
from django.forms import ModelForm, inlineformset_factory
|
||||
|
||||
from .models import RaidResponse, RaidComment, Raid
|
||||
|
@ -68,6 +68,40 @@ class RaidResponseForm(ModelForm):
|
|||
return self.cleaned_data["role"] or self.cleaned_data["character"].role
|
||||
|
||||
|
||||
class GuestRaidResponseForm(ModelForm):
|
||||
class Meta:
|
||||
model = RaidResponse
|
||||
fields = ["guest_name", "guest_klass", "role", "status"]
|
||||
|
||||
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:
|
||||
model = RaidResponse
|
||||
|
@ -120,7 +154,7 @@ class RaidCommentForm(ModelForm):
|
|||
RaidResponseFormSet = inlineformset_factory(
|
||||
Raid, # parent model
|
||||
RaidResponse,
|
||||
fields=["character", "role", "status", "group", "note", "attendance"],
|
||||
fields=["character", "guest_name", "guest_klass", "role", "status", "group", "note", "attendance"],
|
||||
can_delete=False,
|
||||
extra=1
|
||||
)
|
||||
|
@ -133,10 +167,12 @@ class RaidResponseFormSetHelper(FormHelper):
|
|||
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"),
|
||||
Field("attendance", css_class="attendance-input", style="width: 10ch"),
|
||||
)
|
||||
self.form_tag = False
|
||||
|
|
31
drakul/raids/migrations/0010_auto_20200923_1450.py
Normal file
31
drakul/raids/migrations/0010_auto_20200923_1450.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Generated by Django 3.1.1 on 2020-09-23 14:50
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0005_auto_20191121_1646'),
|
||||
('raids', '0009_auto_20200528_0410'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='raidresponse',
|
||||
name='guest_klass',
|
||||
field=models.PositiveSmallIntegerField(blank=True, choices=[(1, 'Druid'), (2, 'Hunter'), (3, 'Mage'), (4, 'Paladin'), (5, 'Priest'), (6, 'Rogue'), (7, 'Shaman'), (8, 'Warlock'), (9, 'Warrior')], null=True, verbose_name='Guest class'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='raidresponse',
|
||||
name='guest_name',
|
||||
field=models.CharField(blank=True, max_length=12, null=True, validators=[django.core.validators.MinLengthValidator(2)], verbose_name='Guest name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='raidresponse',
|
||||
name='character',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='raid_responses', to='users.character'),
|
||||
),
|
||||
]
|
|
@ -2,6 +2,7 @@ from datetime import timedelta
|
|||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
|
||||
from drakul.users.models import Character
|
||||
|
@ -53,9 +54,26 @@ class RaidResponse(models.Model):
|
|||
Character,
|
||||
related_name="raid_responses",
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True
|
||||
)
|
||||
|
||||
guest_name = models.CharField(
|
||||
"Guest name",
|
||||
max_length=12, # Blizzard limits character names to 2-12 characters
|
||||
validators=[MinLengthValidator(2)],
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
guest_klass = models.PositiveSmallIntegerField(
|
||||
"Guest class",
|
||||
choices=Character.Klass.choices,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
role = models.PositiveSmallIntegerField(
|
||||
choices=Character.Role.choices,
|
||||
blank=True,
|
||||
|
@ -116,8 +134,22 @@ class RaidResponse(models.Model):
|
|||
self._original_status = self.status
|
||||
|
||||
def clean(self):
|
||||
# Either Character or Guest Name/Guest Class must be set
|
||||
if self.character is None:
|
||||
errors = {}
|
||||
if self.guest_name is None:
|
||||
errors["guest_name"] = "This field is required for guests."
|
||||
if self.guest_klass is None:
|
||||
errors["guest_klass"] = "This field is required for guests."
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
else:
|
||||
self.guest_name = None
|
||||
self.guest_klass = None
|
||||
|
||||
# Make sure no-responses and sign-offs are character- and role-agnostic, but all other responses are not
|
||||
if self.status <= RaidResponse.Status.SIGNED_OFF:
|
||||
if not self.is_guest:
|
||||
self.character = self.character.user.main
|
||||
self.role = None
|
||||
elif self.role is None:
|
||||
|
@ -128,9 +160,30 @@ class RaidResponse(models.Model):
|
|||
self.attendance = RaidResponse.Status(self.status).default_attendance
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Delete guest "No Response"s
|
||||
if self.is_guest and self.status == RaidResponse.Status.NO_RESPONSE:
|
||||
self.delete()
|
||||
return
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
self._original_status = self.status
|
||||
|
||||
@property
|
||||
def is_guest(self):
|
||||
return self.character is None
|
||||
|
||||
@property
|
||||
def character_name(self):
|
||||
if self.is_guest:
|
||||
return self.guest_name
|
||||
return self.character.name
|
||||
|
||||
@property
|
||||
def character_klass(self):
|
||||
if self.is_guest:
|
||||
return self.guest_klass
|
||||
return self.character.klass
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.character} {self.get_status_display()} for '{self.raid}'"
|
||||
|
||||
|
|
|
@ -124,7 +124,8 @@
|
|||
|
||||
function trIsExtra(tr) {
|
||||
let characterSelect = tr.querySelector(".character-select");
|
||||
return characterSelect == null || characterSelect.value === ""; // the "--------" character has value=""
|
||||
let guestName = tr.querySelector(".guest-name");
|
||||
return (characterSelect == null || characterSelect.value === "") && (guestName == null || guestName.value === ""); // the "--------" character has value=""
|
||||
}
|
||||
|
||||
function updateSelectTotals() {
|
||||
|
@ -163,8 +164,8 @@
|
|||
}
|
||||
|
||||
updateSelectTotals();
|
||||
responseForm.querySelectorAll("select").forEach((select) => {
|
||||
select.addEventListener("change", updateSelectTotals);
|
||||
responseForm.querySelectorAll("select, input").forEach((element) => {
|
||||
element.addEventListener("change", updateSelectTotals);
|
||||
});
|
||||
|
||||
function changeSignupStatus(from, to) {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="card mb-1">
|
||||
|
||||
<h4 class="card-header d-flex">
|
||||
<span class="mr-auto">{{ raid.title }} <small class="text-muted">{{ raid.date }}</small></span>
|
||||
{% if perms.raids.change_raid %}<a class="btn btn-outline-primary btn-sm ml-2" role="button" href="{% url 'raid_change' raid.id %}">Edit</a>{% endif %}
|
||||
|
@ -30,6 +30,17 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if guest_response_form %}
|
||||
<div class="mb-2"></div>
|
||||
<div class="card">
|
||||
<h6 class="card-header">Add Guest</h6>
|
||||
<div class="card-body pt-2 pb-0">
|
||||
<form class="guest-raid-response-form" method="post" action="{% url 'raid_detail' raid.id %}">
|
||||
{% crispy guest_response_form %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-4"></div>
|
||||
|
||||
<div class="modal fade" id="exportModal" tabindex="-1" role="dialog" aria-labelledby="exportModalLabel" aria-hidden="true">
|
||||
|
@ -82,12 +93,14 @@
|
|||
</div>
|
||||
-->
|
||||
{% for response in rank_responses %}
|
||||
<div class="alert response-character-alert mb-1 alert-class-{{ response.character.klass }}" role="alert" data-response-id="{{ response.id }}"
|
||||
<div class="alert response-character-alert mb-1 alert-class-{{ response.character_klass }} text-truncate" role="alert" data-response-id="{{ response.id }}"
|
||||
{% if response.role is not None %} data-response-role="{{ response.role }}"{% endif %}>
|
||||
{% if response.character != response.character.user.main %}
|
||||
<abbr title="{{ response.character.user.main }}">{{ response.character.name }}</abbr>
|
||||
{% if response.is_guest %}
|
||||
<span class="float-left">⇌</span>{{ response.character_name }}
|
||||
{% elif response.character != response.character.user.main %}
|
||||
<abbr title="{{ response.character.user.main }}">{{ response.character_name }}</abbr>
|
||||
{% else %}
|
||||
{{ response.character.name }}
|
||||
{{ response.character_name }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -14,7 +14,7 @@ from django.views.generic.edit import BaseUpdateView
|
|||
|
||||
from drakul.base.views import MultiModelFormView
|
||||
from .forms import RaidResponseForm, RaidCommentForm, RaidForm, RaidResponseFormSetHelper, RaidResponseFormSet, \
|
||||
RaidResponseChangeForm
|
||||
RaidResponseChangeForm, GuestRaidResponseForm
|
||||
from .models import Raid, RaidResponse, InstanceReset
|
||||
|
||||
User = get_user_model()
|
||||
|
@ -117,6 +117,8 @@ class RaidDetailView(SingleObjectMixin, MultiModelFormView):
|
|||
}
|
||||
if self.object.response_deadline > timezone.now():
|
||||
classes["response_form"] = RaidResponseForm
|
||||
if self.request.user.has_perm("raids.change_raid"):
|
||||
classes["guest_response_form"] = GuestRaidResponseForm
|
||||
return classes
|
||||
|
||||
def get_response_form_instance(self):
|
||||
|
@ -129,6 +131,10 @@ class RaidDetailView(SingleObjectMixin, MultiModelFormView):
|
|||
form.instance.raid = self.object
|
||||
form.save()
|
||||
|
||||
def guest_response_form_valid(self, form):
|
||||
form.instance.raid = self.object
|
||||
form.save()
|
||||
|
||||
def comment_form_valid(self, form):
|
||||
form.instance.raid = self.object
|
||||
form.instance.user = self.request.user
|
||||
|
@ -138,12 +144,19 @@ class RaidDetailView(SingleObjectMixin, MultiModelFormView):
|
|||
kwargs["user"] = self.request.user
|
||||
return kwargs
|
||||
|
||||
def get_guest_response_form_kwargs(self, kwargs):
|
||||
kwargs["prefix"] = "guest"
|
||||
return kwargs
|
||||
|
||||
def get_comment_form_success_url(self):
|
||||
return reverse("raid_detail", kwargs={"pk": self.object.pk}) + "#comments"
|
||||
|
||||
def get_response_form_success_url(self):
|
||||
return reverse("raid_detail", kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_guest_response_form_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)
|
||||
|
|
|
@ -38,7 +38,10 @@ class AttendanceView(TemplateView):
|
|||
context["raid_list"] = list(reversed(raids))
|
||||
|
||||
context["attendance_matrix"] = {user: {raid: None for raid in reversed(raids)} for user in users}
|
||||
for response in RaidResponse.objects.filter(raid__in=raids).select_related("raid", "character__user"):
|
||||
for response in RaidResponse.objects.filter(
|
||||
character__isnull=False,
|
||||
raid__in=raids
|
||||
).select_related("raid", "character__user"):
|
||||
with suppress(KeyError): # KeyError means user was in previous raid but is now inactive
|
||||
context["attendance_matrix"][response.character.user][response.raid] = response
|
||||
return context
|
||||
|
|
Loading…
Reference in a new issue