Compare commits

...

3 commits

18 changed files with 505 additions and 326 deletions

View file

@ -6,13 +6,15 @@
{% block content %} {% block content %}
<div class="d-flex justify-content-center align-items-center pt-5"> <div class="container">
<form class="m-auto" method="post" action="{% url 'login' %}"> <div class="d-flex justify-content-center align-items-center pt-5">
{% csrf_token %} <form class="m-auto" method="post" action="{% url 'login' %}">
{{ form | crispy }} {% csrf_token %}
<button type="submit" class="btn btn-primary btn-block">Log In</button> {{ form | crispy }}
<input type="hidden" name="next" value="{{ next }}"> <button type="submit" class="btn btn-primary btn-block">Log In</button>
<p class="text-center mt-2"><a href="{% url 'signup' %}">Sign Up</a></p> <input type="hidden" name="next" value="{{ next }}">
</form> <p class="text-center mt-2"><a href="{% url 'signup' %}">Sign Up</a></p>
</form>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View file

@ -6,13 +6,15 @@
{% block content %} {% block content %}
<div class="d-flex justify-content-center align-items-center mt-5"> <div class="container">
<form class="m-auto" method="post" action="{% url 'password_change' %}"> <div class="d-flex justify-content-center align-items-center mt-5">
<h2>Change Password</h2> <form class="m-auto" method="post" action="{% url 'password_change' %}">
<hr> <h2>Change Password</h2>
{% csrf_token %} <hr>
{{ form | crispy }} {% csrf_token %}
<button type="submit" class="btn btn-primary btn-block">Change Password</button> {{ form | crispy }}
</form> <button type="submit" class="btn btn-primary btn-block">Change Password</button>
</form>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View file

@ -6,19 +6,21 @@
{% block content %} {% block content %}
<div class="d-flex justify-content-center align-items-center pt-5"> <div class="container">
<form class="m-auto" method="post" action="{% url 'signup' %}"> <div class="d-flex justify-content-center align-items-center pt-5">
<h2>Sign Up</h2> <form class="m-auto" method="post" action="{% url 'signup' %}">
<hr> <h2>Sign Up</h2>
{% csrf_token %} <hr>
{{ user_form | crispy }} {% csrf_token %}
<h5 class="mt-5">Character</h5> {{ user_form | crispy }}
<hr> <h5 class="mt-5">Character</h5>
{{ character_form | crispy }} <hr>
<hr> {{ character_form | crispy }}
<button type="submit" class="btn btn-primary btn-block">Sign Up</button> <hr>
<input type="hidden" name="next" value="{{ next }}"> <button type="submit" class="btn btn-primary btn-block">Sign Up</button>
<p class="text-center mt-2"><a href="{% url 'login' %}">Log In</a></p> <input type="hidden" name="next" value="{{ next }}">
</form> <p class="text-center mt-2"><a href="{% url 'login' %}">Log In</a></p>
</form>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View file

@ -4,24 +4,26 @@
{% block content %} {% block content %}
<h2>Bank</h2> <div class="container">
<div class="card"> <h2>Bank</h2>
<div class="card-header d-flex"> <div class="card">
<div class="nav nav-tabs card-header-tabs mr-auto"> <div class="card-header d-flex">
<a class="nav-item nav-link {% active 'bank' %}" href="{% url 'bank' %}">Items</a> <div class="nav nav-tabs card-header-tabs mr-auto">
<a class="nav-item nav-link {% active 'bank_item_log' %}" href="{% url 'bank_item_log' %}">Log</a> <a class="nav-item nav-link {% active 'bank' %}" href="{% url 'bank' %}">Items</a>
<a class="nav-item nav-link {% active 'bank_money_log' %}" href="{% url 'bank_money_log' %}">Money</a> <a class="nav-item nav-link {% active 'bank_item_log' %}" href="{% url 'bank_item_log' %}">Log</a>
{% if perms.bank.add_itemtransaction and perms.bank.add_moneytransaction %} <a class="nav-item nav-link {% active 'bank_money_log' %}" href="{% url 'bank_money_log' %}">Money</a>
<a class="nav-item nav-link {% active 'bank_import' %}" href="{% url 'bank_import' %}">Import</a> {% if perms.bank.add_itemtransaction and perms.bank.add_moneytransaction %}
{% endif %} <a class="nav-item nav-link {% active 'bank_import' %}" href="{% url 'bank_import' %}">Import</a>
{% endif %}
</div>
{% block bank_header %}{% endblock bank_header %}
</div> </div>
{% block bank_header %}{% endblock bank_header %} <div class="card-body p-0">
{% block bank_content %}
{% endblock bank_content %}
</div>
{% block bank_footer %}
{% endblock bank_footer %}
</div> </div>
<div class="card-body p-0">
{% block bank_content %}
{% endblock bank_content %}
</div>
{% block bank_footer %}
{% endblock bank_footer %}
</div> </div>
{% endblock content %} {% endblock content %}

View file

@ -1,3 +1,13 @@
.container {
max-width: 1140px;
}
@media (min-width:1600px) {
.container-wide {
max-width: 1500px
}
}
/* WoW class-coloured text */ /* WoW class-coloured text */
.color-druid, .color-class-1 {color: #ff7d0a;} .color-druid, .color-class-1 {color: #ff7d0a;}
.color-hunter, .color-class-2 {color: #abd473;} .color-hunter, .color-class-2 {color: #abd473;}

View file

@ -43,10 +43,8 @@
</header> </header>
<main class="flex-shrink-0" role="main"> <main class="flex-shrink-0" role="main">
<div class="container"> {% block content %}
{% block content %} {% endblock content %}
{% endblock content %}
</div>
</main> </main>
<footer class="footer mt-auto"> <footer class="footer mt-auto">

View file

@ -1,6 +1,6 @@
from crispy_forms.bootstrap import StrictButton from crispy_forms.bootstrap import StrictButton
from crispy_forms.helper import FormHelper 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 django.forms import ModelForm, inlineformset_factory
from .models import RaidResponse, RaidComment, Raid from .models import RaidResponse, RaidComment, Raid
@ -68,6 +68,40 @@ class RaidResponseForm(ModelForm):
return self.cleaned_data["role"] or self.cleaned_data["character"].role 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>&nbsp;</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 RaidResponseChangeForm(ModelForm):
class Meta: class Meta:
model = RaidResponse model = RaidResponse
@ -120,7 +154,7 @@ class RaidCommentForm(ModelForm):
RaidResponseFormSet = inlineformset_factory( RaidResponseFormSet = inlineformset_factory(
Raid, # parent model Raid, # parent model
RaidResponse, RaidResponse,
fields=["character", "role", "status", "group", "note", "attendance"], fields=["character", "guest_name", "guest_klass", "role", "status", "group", "note", "attendance"],
can_delete=False, can_delete=False,
extra=1 extra=1
) )
@ -133,10 +167,12 @@ class RaidResponseFormSetHelper(FormHelper):
super().__init__(form) super().__init__(form)
self.layout = Layout( self.layout = Layout(
Field("character", css_class="character-select"), 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("role", css_class="role-select"),
Field("status", css_class="status-select"), Field("status", css_class="status-select"),
Field("group", css_class="group-select"), Field("group", css_class="group-select"),
Field("note"), Field("note"),
Field("attendance", css_class="attendance-input"), Field("attendance", css_class="attendance-input", style="width: 10ch"),
) )
self.form_tag = False self.form_tag = False

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

View file

@ -2,6 +2,7 @@ from datetime import timedelta
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator
from django.db import models from django.db import models
from drakul.users.models import Character from drakul.users.models import Character
@ -53,9 +54,26 @@ class RaidResponse(models.Model):
Character, Character,
related_name="raid_responses", related_name="raid_responses",
on_delete=models.CASCADE, on_delete=models.CASCADE,
blank=True,
null=True,
db_index=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( role = models.PositiveSmallIntegerField(
choices=Character.Role.choices, choices=Character.Role.choices,
blank=True, blank=True,
@ -116,9 +134,23 @@ class RaidResponse(models.Model):
self._original_status = self.status self._original_status = self.status
def clean(self): 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 # 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 self.status <= RaidResponse.Status.SIGNED_OFF:
self.character = self.character.user.main if not self.is_guest:
self.character = self.character.user.main
self.role = None self.role = None
elif self.role is None: elif self.role is None:
raise ValidationError({"role": "This field is required."}) raise ValidationError({"role": "This field is required."})
@ -128,9 +160,30 @@ class RaidResponse(models.Model):
self.attendance = RaidResponse.Status(self.status).default_attendance self.attendance = RaidResponse.Status(self.status).default_attendance
def save(self, *args, **kwargs): 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) super().save(*args, **kwargs)
self._original_status = self.status 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): def __str__(self):
return f"{self.character} {self.get_status_display()} for '{self.raid}'" return f"{self.character} {self.get_status_display()} for '{self.raid}'"

View file

@ -4,52 +4,54 @@
{% block content %} {% block content %}
<h2>Raids</h2> <div class="container">
<div class="d-flex justify-content-center mb-1"> <h2>Raids</h2>
<div class="btn-group" role="group" aria-label="Calendar navigation controls"> <div class="d-flex justify-content-center mb-1">
<a class="btn btn-secondary" role="button" href="{% url 'raid_calendar' previous_month.year previous_month.month %}">&lt;</a> <div class="btn-group" role="group" aria-label="Calendar navigation controls">
<a class="btn btn-secondary" role="button" href="{% url 'raid_calendar' %}">{{ month | date:"YEAR_MONTH_FORMAT" }}</a> <a class="btn btn-secondary" role="button" href="{% url 'raid_calendar' previous_month.year previous_month.month %}">&lt;</a>
<a class="btn btn-secondary" role="button" href="{% url 'raid_calendar' next_month.year next_month.month %}">&gt;</a> <a class="btn btn-secondary" role="button" href="{% url 'raid_calendar' %}">{{ month | date:"YEAR_MONTH_FORMAT" }}</a>
<a class="btn btn-secondary" role="button" href="{% url 'raid_calendar' next_month.year next_month.month %}">&gt;</a>
</div>
</div>
<table class="raid-calendar table table-bordered bg-white mb-1">
<thead class="thead-light text-center">
<tr>
<th scope="col" class="w-14">Monday</th>
<th scope="col" class="w-14">Tuesday</th>
<th scope="col" class="w-14">Wednesday</th>
<th scope="col" class="w-14">Thursday</th>
<th scope="col" class="w-14">Friday</th>
<th scope="col" class="w-14">Saturday</th>
<th scope="col" class="w-14">Sunday</th>
</tr>
</thead>
<tbody>
{% for week in calendar %}
<tr>
{% for day in week %}
<td class="{% if day.date.month != month.month %}o-50{% endif %} {% if day.date == today %}table-warning{% endif %}">
<p class="mb-2"><small>{{ day.date | date:"j" }}</small></p>
<div class="d-flex flex-column">
{% for instance_reset in day.instance_resets %}
<span class="mb-1 badge badge-light text-left text-wrap font-weight-normal">
<span class="text-monospace">{{ instance_reset.time }}</span> <span class="text-muted">{{ instance_reset.name }} Resets</span>
</span>
{% endfor %}
{% for raid in day.raids %}
<a class="mb-1 badge response-status-{{ raid.max_status | default:0 }}-bg text-left text-wrap font-weight-normal" href="{% url 'raid_detail' raid.id %}">
<span class="text-monospace">{{ raid.date.time }}</span> <strong>{{ raid.title }}</strong>
</a>
{% endfor %}
</div>
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<div class="d-flex justify-content-end">
{% if perms.raids.add_raid %}<a class="btn btn-success" role="button" href="{% url 'raid_create' %}">Add New</a>{% endif %}
</div> </div>
</div> </div>
<table class="raid-calendar table table-bordered bg-white mb-1">
<thead class="thead-light text-center">
<tr>
<th scope="col" class="w-14">Monday</th>
<th scope="col" class="w-14">Tuesday</th>
<th scope="col" class="w-14">Wednesday</th>
<th scope="col" class="w-14">Thursday</th>
<th scope="col" class="w-14">Friday</th>
<th scope="col" class="w-14">Saturday</th>
<th scope="col" class="w-14">Sunday</th>
</tr>
</thead>
<tbody>
{% for week in calendar %}
<tr>
{% for day in week %}
<td class="{% if day.date.month != month.month %}o-50{% endif %} {% if day.date == today %}table-warning{% endif %}">
<p class="mb-2"><small>{{ day.date | date:"j" }}</small></p>
<div class="d-flex flex-column">
{% for instance_reset in day.instance_resets %}
<span class="mb-1 badge badge-light text-left text-wrap font-weight-normal">
<span class="text-monospace">{{ instance_reset.time }}</span> <span class="text-muted">{{ instance_reset.name }} Resets</span>
</span>
{% endfor %}
{% for raid in day.raids %}
<a class="mb-1 badge response-status-{{ raid.max_status | default:0 }}-bg text-left text-wrap font-weight-normal" href="{% url 'raid_detail' raid.id %}">
<span class="text-monospace">{{ raid.date.time }}</span> <strong>{{ raid.title }}</strong>
</a>
{% endfor %}
</div>
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<div class="d-flex justify-content-end">
{% if perms.raids.add_raid %}<a class="btn btn-success" role="button" href="{% url 'raid_create' %}">Add New</a>{% endif %}
</div>
{% endblock content %} {% endblock content %}

View file

@ -6,100 +6,103 @@
{% block content %} {% block content %}
<div class="card mb-3"> <div class="container container-wide">
<h5 class="card-header">Raid Details</h5> <div class="card mb-3">
<div class="card-body"> <h5 class="card-header">Raid Details</h5>
{% crispy raid_form %} <div class="card-body">
{% crispy raid_form %}
</div>
</div> </div>
</div> </div>
<div class="container container-wide">
<div class="card mb-3"> <div class="card mb-3">
<h5 class="card-header">Raid Responses</h5> <h5 class="card-header">Raid Responses</h5>
<div class="card-body"> <div class="card-body">
<form class="responses-form table-borderless" method="post" action="{% url 'raid_change' raid.id %}"> <form class="responses-form table-borderless" method="post" action="{% url 'raid_change' raid.id %}">
{% crispy raid_response_formset raid_response_formset_helper %} {% crispy raid_response_formset raid_response_formset_helper %}
<hr> <hr>
<div class="row"> <div class="row">
<div class="form-group col-lg-6"> <div class="form-group col-lg-6">
<label>Change Status</label> <label>Change Status</label>
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<label class="input-group-text" for="change-status-from-status-select">Set all</label> <label class="input-group-text" for="change-status-from-status-select">Set all</label>
</div>
<select class="custom-select" id="change-status-from-status-select">
<option value="1">Signed Off</option>
<option value="2">Backup</option>
<option selected value="3">Signed Up</option>
<option value="4">Standby</option>
<option value="5">Confirmed</option>
</select>
<div class="input-group-append input-group-prepend">
<label class="input-group-text" for="change-status-to-status-select">to</label>
</div>
<select class="custom-select" id="change-status-to-status-select">
<option value="1">Signed Off</option>
<option value="2">Backup</option>
<option value="3">Signed Up</option>
<option value="4">Standby</option>
<option selected value="5">Confirmed</option>
</select>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"
onclick="changeSignupStatus(
from=document.querySelector('#change-status-from-status-select').value,
to=document.querySelector('#change-status-to-status-select').value
);"
>Go</button>
</div>
</div> </div>
<select class="custom-select" id="change-status-from-status-select"> </div>
<option value="1">Signed Off</option> <div class="form-group col-lg-6">
<option value="2">Backup</option> <label>Give Attendance</label>
<option selected value="3">Signed Up</option> <div class="input-group">
<option value="4">Standby</option> <div class="input-group-prepend">
<option value="5">Confirmed</option> <label class="input-group-text" for="set-attendance-status-select">Give all</label>
</select> </div>
<div class="input-group-append input-group-prepend"> <select class="custom-select" id="set-attendance-status-select">
<label class="input-group-text" for="change-status-to-status-select">to</label> <option value="1">Signed Off</option>
</div> <option value="2">Backup</option>
<select class="custom-select" id="change-status-to-status-select"> <option value="3">Signed Up</option>
<option value="1">Signed Off</option> <option value="4">Standby</option>
<option value="2">Backup</option> <option selected value="5">Confirmed</option>
<option value="3">Signed Up</option> </select>
<option value="4">Standby</option> <input type="number" class="form-control" id="set-attendance-value-input" aria-label="Attendance Input" value="1.0">
<option selected value="5">Confirmed</option> <div class="input-group-append">
</select> <button class="btn btn-outline-secondary" type="button"
<div class="input-group-append"> onclick="setAttendanceForStatus(
<button class="btn btn-outline-secondary" type="button" status=document.querySelector('#set-attendance-status-select').value,
onclick="changeSignupStatus( value=document.querySelector('#set-attendance-value-input').value
from=document.querySelector('#change-status-from-status-select').value, )"
to=document.querySelector('#change-status-to-status-select').value >Go</button>
);" </div>
>Go</button>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group col-lg-6"> <hr>
<label>Give Attendance</label> <div class="row">
<div class="input-group"> <div class="col-xl-7">
<div class="input-group-prepend"> <div class="alert alert-primary d-flex justify-content-around" role="alert">
<label class="input-group-text" for="set-attendance-status-select">Give all</label> <div><span id="total-signed-off" class="font-weight-bold">?</span> Signed Off</div>
<div><span id="total-backup" class="font-weight-bold">?</span> Backup</div>
<div><span id="total-signed-up" class="font-weight-bold">?</span> Signed Up</div>
<div><span id="total-standby" class="font-weight-bold">?</span> Standby</div>
<div><span id="total-confirmed" class="font-weight-bold">?</span> Confirmed</div>
</div> </div>
<select class="custom-select" id="set-attendance-status-select"> </div>
<option value="1">Signed Off</option> <div class="col-xl-5">
<option value="2">Backup</option> <div class="alert alert-success d-flex justify-content-around" role="alert">
<option value="3">Signed Up</option> <div class="font-weight-bolder">Confirmed</div>
<option value="4">Standby</option> <div><span id="total-confirmed-tank" class="font-weight-bold">?</span> Tanks</div>
<option selected value="5">Confirmed</option> <div><span id="total-confirmed-healer" class="font-weight-bold">?</span> Healers</div>
</select> <div><span id="total-confirmed-damage" class="font-weight-bold">?</span> Damage</div>
<input type="number" class="form-control" id="set-attendance-value-input" aria-label="Attendance Input" value="1.0">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"
onclick="setAttendanceForStatus(
status=document.querySelector('#set-attendance-status-select').value,
value=document.querySelector('#set-attendance-value-input').value
)"
>Go</button>
</div> </div>
</div> </div>
</div> </div>
</div> <input class="btn btn-primary float-right" type="submit" name="submit" value="Save">
<hr> </form>
<div class="row"> </div>
<div class="col-xl-7">
<div class="alert alert-primary d-flex justify-content-around" role="alert">
<div><span id="total-signed-off" class="font-weight-bold">?</span> Signed Off</div>
<div><span id="total-backup" class="font-weight-bold">?</span> Backup</div>
<div><span id="total-signed-up" class="font-weight-bold">?</span> Signed Up</div>
<div><span id="total-standby" class="font-weight-bold">?</span> Standby</div>
<div><span id="total-confirmed" class="font-weight-bold">?</span> Confirmed</div>
</div>
</div>
<div class="col-xl-5">
<div class="alert alert-success d-flex justify-content-around" role="alert">
<div class="font-weight-bolder">Confirmed</div>
<div><span id="total-confirmed-tank" class="font-weight-bold">?</span> Tanks</div>
<div><span id="total-confirmed-healer" class="font-weight-bold">?</span> Healers</div>
<div><span id="total-confirmed-damage" class="font-weight-bold">?</span> Damage</div>
</div>
</div>
</div>
<input class="btn btn-primary float-right" type="submit" name="submit" value="Save">
</form>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}
@ -121,7 +124,8 @@
function trIsExtra(tr) { function trIsExtra(tr) {
let characterSelect = tr.querySelector(".character-select"); 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() { function updateSelectTotals() {
@ -160,8 +164,8 @@
} }
updateSelectTotals(); updateSelectTotals();
responseForm.querySelectorAll("select").forEach((select) => { responseForm.querySelectorAll("select, input").forEach((element) => {
select.addEventListener("change", updateSelectTotals); element.addEventListener("change", updateSelectTotals);
}); });
function changeSignupStatus(from, to) { function changeSignupStatus(from, to) {

View file

@ -6,9 +6,11 @@
{% block content %} {% block content %}
<form method="post"> <div class="container">
{% csrf_token %} <form method="post">
<p>Are you sure you want to delete "{{ object }}"?</p> {% csrf_token %}
<button type="submit" class="btn btn-danger">Confirm</button> <p>Are you sure you want to delete "{{ object }}"?</p>
</form> <button type="submit" class="btn btn-danger">Confirm</button>
</form>
</div>
{% endblock content %} {% endblock content %}

View file

@ -6,5 +6,7 @@
{% block content %} {% block content %}
{% crispy form %} <div class="container">
{% crispy form %}
</div>
{% endblock content %} {% endblock content %}

View file

@ -6,132 +6,146 @@
{% block content %} {% block content %}
<div class="card mb-1"> <div class="container">
<div class="card mb-1">
<h4 class="card-header d-flex"> <h4 class="card-header d-flex">
<span class="mr-auto">{{ raid.title }} <small class="text-muted">{{ raid.date }}</small></span> <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 %} {% 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 %}
{% if perms.raids.delete_raid %}<a class="btn text-danger btn-link btn-sm ml-2" role="button" href="{% url 'raid_delete' raid.id %}">Delete</a>{% endif %} {% if perms.raids.delete_raid %}<a class="btn text-danger btn-link btn-sm ml-2" role="button" href="{% url 'raid_delete' raid.id %}">Delete</a>{% endif %}
</h4> </h4>
<div class="card-body"> <div class="card-body">
<p class="card-text">{{ raid.description | urlize | linebreaksbr | default:"<em>No description</em>" }}</p> <p class="card-text">{{ raid.description | urlize | linebreaksbr | default:"<em>No description</em>" }}</p>
{% if raid.is_optional %} {% if raid.is_optional %}
<p class="card-text mb-0"><small class="text-muted">This raid is optional.</small></p> <p class="card-text mb-0"><small class="text-muted">This raid is optional.</small></p>
{% endif %} {% endif %}
<p class="card-text"><small class="text-muted">Response deadline: {{ raid.response_deadline }}.</small></p> <p class="card-text"><small class="text-muted">Response deadline: {{ raid.response_deadline }}.</small></p>
</div>
</div>
{% if response_form %}
<div class="card">
<div class="card-body pb-0">
<form class="raid-response-form" method="post" action="{% url 'raid_detail' raid.id %}">
{% crispy response_form %}
</form>
</div> </div>
</div> </div>
{% endif %} {% if response_form %}
<div class="mb-4"></div> <div class="card">
<div class="card-body pb-0">
<div class="modal fade" id="exportModal" tabindex="-1" role="dialog" aria-labelledby="exportModalLabel" aria-hidden="true"> <form class="raid-response-form" method="post" action="{% url 'raid_detail' raid.id %}">
<div class="modal-dialog" role="document"> {% crispy response_form %}
<div class="modal-content"> </form>
<div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Export</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div> </div>
<div class="modal-body"> </div>
<p>Export list of characters for automatic invitation using <a href="https://www.curseforge.com/wow/addons/exorsus-raid-tools">Exorsus Raid Tools</a>.</p> {% endif %}
<div class="form-group"> {% if guest_response_form %}
<label for="characters-textinput" class="col-form-label">Characters:</label> <div class="mb-2"></div>
<textarea readonly class="form-control" id="characters-textinput" onclick="this.select();"></textarea> <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">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Export</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Export list of characters for automatic invitation using <a href="https://www.curseforge.com/wow/addons/exorsus-raid-tools">Exorsus Raid Tools</a>.</p>
<div class="form-group">
<label for="characters-textinput" class="col-form-label">Characters:</label>
<textarea readonly class="form-control" id="characters-textinput" onclick="this.select();"></textarea>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% for status, status_responses in raid_responses.items %} {% for status, status_responses in raid_responses.items %}
{% regroup status_responses by group as group_responses_list %} {% regroup status_responses by group as group_responses_list %}
{% for group, group_responses in group_responses_list %} {% for group, group_responses in group_responses_list %}
<div class="card mb-2 response-container" data-response-container-status="{{ status }}" data-response-container-group="{{ group }}"> <div class="card mb-2 response-container" data-response-container-status="{{ status }}" data-response-container-group="{{ group }}">
<h6 class="card-header d-flex response-status-{{ status }}-bg"> <h6 class="card-header d-flex response-status-{{ status }}-bg">
<span class="mr-auto"> <span class="mr-auto">
{{ status.label }}{% if group_responses_list|length > 1 %}: {{ group_responses.0.get_group_display }}{% endif %} ({{ group_responses | length }}) {{ status.label }}{% if group_responses_list|length > 1 %}: {{ group_responses.0.get_group_display }}{% endif %} ({{ group_responses | length }})
</span> </span>
<span type="button" class="badge badge-dark" data-toggle="modal" data-target="#exportModal">Export</span> <span type="button" class="badge badge-dark" data-toggle="modal" data-target="#exportModal">Export</span>
</h6> </h6>
<div class="card-body mb-n4"> <div class="card-body mb-n4">
{% regroup group_responses by get_role_display as role_responses_list %} {% regroup group_responses by get_role_display as role_responses_list %}
{% for role, role_responses in role_responses_list %} {% for role, role_responses in role_responses_list %}
{% if role is not None %} {% if role is not None %}
<h6 class="card-title border-bottom pb-2">{{ role }} ({{ role_responses | length }})</h6> <h6 class="card-title border-bottom pb-2">{{ role }} ({{ role_responses | length }})</h6>
{% endif %} {% endif %}
<div class="d-flex flex-wrap"> <div class="d-flex flex-wrap">
{% regroup role_responses by character.klass as class_responses_list %} {% regroup role_responses by character.klass as class_responses_list %}
{% for class, class_responses in class_responses_list %} {% for class, class_responses in class_responses_list %}
<div class="d-flex flex-column mr-2 mb-4"> <div class="d-flex flex-column mr-2 mb-4">
{% regroup class_responses by character.user.rank as rank_responses_list %} {% regroup class_responses by character.user.rank as rank_responses_list %}
{% for rank, rank_responses in rank_responses_list %} {% for rank, rank_responses in rank_responses_list %}
<!-- TODO: Uncomment to show user ranks in raid signup <!-- TODO: Uncomment to show user ranks in raid signup
<div class="d-flex justify-content-center align-items-center"> <div class="d-flex justify-content-center align-items-center">
<hr class="flex-grow-1 my-0"> <hr class="flex-grow-1 my-0">
<div class="mx-2 my-0 font-weight-light text-muted small">{{ rank }}</div> <div class="mx-2 my-0 font-weight-light text-muted small">{{ rank }}</div>
<hr class="flex-grow-1 my-0"> <hr class="flex-grow-1 my-0">
</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 }}"
{% 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>
{% else %}
{{ response.character.name }}
{% endif %}
</div> </div>
-->
{% for response in rank_responses %}
<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.is_guest %}
<span class="float-left">&#8652;</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 }}
{% endif %}
</div>
{% endfor %}
{% endfor %} {% endfor %}
</div>
{% endfor %} {% endfor %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endfor %}
</div> </div>
</div>
{% empty %}
{% if perms.raids.change_raid %}
<div class="card mb-2 response-container" style="opacity: 0.4" data-response-container-status="{{ status }}" data-response-container-group="1">
<h6 class="card-header d-flex response-status-{{ status }}-bg">
<span class="mr-auto">{{ status.label }}</span>
</h6>
</div>
{% endif %}
{% endfor %}
{% endfor %}
<div id="comments" class="card mt-4 mb-4">
<h6 class="card-header">Comments</h6>
<div class="card-body">
{% for comment in raid.comments.all %}
<div class="card mb-2">
<div class="card-body py-3">
<h6 class="card-title mb-1"><strong>{{ comment.user.main.name }}</strong> <small class="card-subtitle text-muted">&middot; {{ comment.date_created }}</small></h6>
<p class="card-text">
{{ comment.body | linebreaksbr }}
</p>
</div>
</div>
{% empty %} {% empty %}
<em>No comments.</em> {% if perms.raids.change_raid %}
<div class="card mb-2 response-container" style="opacity: 0.4" data-response-container-status="{{ status }}" data-response-container-group="1">
<h6 class="card-header d-flex response-status-{{ status }}-bg">
<span class="mr-auto">{{ status.label }}</span>
</h6>
</div>
{% endif %}
{% endfor %} {% endfor %}
{% endfor %}
{% if comment_form %} <div id="comments" class="card mt-4 mb-4">
<hr class="my-4"> <h6 class="card-header">Comments</h6>
<h6>Add Comment</h6> <div class="card-body">
<form class="form" method="post" action="{% url 'raid_detail' raid.id %}"> {% for comment in raid.comments.all %}
{% crispy comment_form %} <div class="card mb-2">
</form> <div class="card-body py-3">
{% endif %} <h6 class="card-title mb-1"><strong>{{ comment.user.main.name }}</strong> <small class="card-subtitle text-muted">&middot; {{ comment.date_created }}</small></h6>
<p class="card-text">
{{ comment.body | linebreaksbr }}
</p>
</div>
</div>
{% empty %}
<em>No comments.</em>
{% endfor %}
{% if comment_form %}
<hr class="my-4">
<h6>Add Comment</h6>
<form class="form" method="post" action="{% url 'raid_detail' raid.id %}">
{% crispy comment_form %}
</form>
{% endif %}
</div>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

View file

@ -14,7 +14,7 @@ from django.views.generic.edit import BaseUpdateView
from drakul.base.views import MultiModelFormView from drakul.base.views import MultiModelFormView
from .forms import RaidResponseForm, RaidCommentForm, RaidForm, RaidResponseFormSetHelper, RaidResponseFormSet, \ from .forms import RaidResponseForm, RaidCommentForm, RaidForm, RaidResponseFormSetHelper, RaidResponseFormSet, \
RaidResponseChangeForm RaidResponseChangeForm, GuestRaidResponseForm
from .models import Raid, RaidResponse, InstanceReset from .models import Raid, RaidResponse, InstanceReset
User = get_user_model() User = get_user_model()
@ -117,6 +117,8 @@ class RaidDetailView(SingleObjectMixin, MultiModelFormView):
} }
if self.object.response_deadline > timezone.now(): if self.object.response_deadline > timezone.now():
classes["response_form"] = RaidResponseForm classes["response_form"] = RaidResponseForm
if self.request.user.has_perm("raids.change_raid"):
classes["guest_response_form"] = GuestRaidResponseForm
return classes return classes
def get_response_form_instance(self): def get_response_form_instance(self):
@ -129,6 +131,10 @@ class RaidDetailView(SingleObjectMixin, MultiModelFormView):
form.instance.raid = self.object form.instance.raid = self.object
form.save() form.save()
def guest_response_form_valid(self, form):
form.instance.raid = self.object
form.save()
def comment_form_valid(self, form): def comment_form_valid(self, form):
form.instance.raid = self.object form.instance.raid = self.object
form.instance.user = self.request.user form.instance.user = self.request.user
@ -138,12 +144,19 @@ class RaidDetailView(SingleObjectMixin, MultiModelFormView):
kwargs["user"] = self.request.user kwargs["user"] = self.request.user
return kwargs return kwargs
def get_guest_response_form_kwargs(self, kwargs):
kwargs["prefix"] = "guest"
return kwargs
def get_comment_form_success_url(self): def get_comment_form_success_url(self):
return reverse("raid_detail", kwargs={"pk": self.object.pk}) + "#comments" return reverse("raid_detail", kwargs={"pk": self.object.pk}) + "#comments"
def get_response_form_success_url(self): def get_response_form_success_url(self):
return reverse("raid_detail", kwargs={"pk": self.object.pk}) 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): def get(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)

View file

@ -23,6 +23,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0002_create_default_ranks'), ('users', '0002_create_default_ranks'),
('raids', '0004_raid_is_optional'),
] ]
operations = [ operations = [

View file

@ -6,29 +6,31 @@
{% block content %} {% block content %}
<h2>Attendance</h2> <div class="container">
<table class="table table-dark table-bordered text-center"> <h2>Attendance</h2>
<thead class="thead-light"> <table class="table table-dark table-bordered text-center">
<tr> <thead class="thead-light">
{% for raid in raid_list %}
<th><a class="text-reset" href="{% url 'raid_detail' raid.id %}">{{ raid.date | date:"d/m"}}</a></th>
{% endfor %}
<th>Avg</th>
<th></th>
</tr>
</thead>
<tbody class="table-sm">
{% for user, user_responses in attendance_matrix.items %}
<tr> <tr>
{% for response in user_responses.values %} {% for raid in raid_list %}
<td class="response-status-{{ response.status | default_if_none:'no-color' }}-bg"> <th><a class="text-reset" href="{% url 'raid_detail' raid.id %}">{{ raid.date | date:"d/m"}}</a></th>
{% attendance_cell response %}
</td>
{% endfor %} {% endfor %}
<td class="font-weight-bold" style="color: {% avg_attendance_color user.avg_attendance %}">{{ user.avg_attendance | floatformat:2 }}</td> <th>Avg</th>
<td class="text-left color-class-{{ user.main.klass }}">{{ user.main }}</td> <th></th>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody class="table-sm">
</table> {% for user, user_responses in attendance_matrix.items %}
<tr>
{% for response in user_responses.values %}
<td class="response-status-{{ response.status | default_if_none:'no-color' }}-bg">
{% attendance_cell response %}
</td>
{% endfor %}
<td class="font-weight-bold" style="color: {% avg_attendance_color user.avg_attendance %}">{{ user.avg_attendance | floatformat:2 }}</td>
<td class="text-left color-class-{{ user.main.klass }}">{{ user.main }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %} {% endblock %}

View file

@ -38,7 +38,10 @@ class AttendanceView(TemplateView):
context["raid_list"] = list(reversed(raids)) context["raid_list"] = list(reversed(raids))
context["attendance_matrix"] = {user: {raid: None for raid in reversed(raids)} for user in users} 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 with suppress(KeyError): # KeyError means user was in previous raid but is now inactive
context["attendance_matrix"][response.character.user][response.raid] = response context["attendance_matrix"][response.character.user][response.raid] = response
return context return context