Allow changing raid responses character/roles through JavaScript dropdown menu.
This commit is contained in:
parent
41eff3bec2
commit
15896cb0b6
|
@ -3,3 +3,7 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
class BaseConfig(AppConfig):
|
class BaseConfig(AppConfig):
|
||||||
name = "drakul.base"
|
name = "drakul.base"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
from . import signals
|
||||||
|
|
17
drakul/base/exceptions.py
Normal file
17
drakul/base/exceptions.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError, ImproperlyConfigured
|
||||||
|
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||||
|
|
||||||
|
|
||||||
|
def drf_validation_error(exception: DjangoValidationError):
|
||||||
|
"""
|
||||||
|
Transform Django model validation errors into an equivalent DRF ValidationError.
|
||||||
|
"""
|
||||||
|
if hasattr(exception, "message_dict"):
|
||||||
|
detail = exception.message_dict
|
||||||
|
elif hasattr(exception, "message"):
|
||||||
|
detail = exception.message
|
||||||
|
elif hasattr(exception, "messages"):
|
||||||
|
detail = exception.messages
|
||||||
|
else:
|
||||||
|
raise ImproperlyConfigured
|
||||||
|
return DRFValidationError(detail=detail)
|
|
@ -22,6 +22,7 @@ INSTALLED_APPS = (
|
||||||
"drakul.users.apps.UsersConfig",
|
"drakul.users.apps.UsersConfig",
|
||||||
|
|
||||||
# Third party apps
|
# Third party apps
|
||||||
|
"rest_framework",
|
||||||
"crispy_forms",
|
"crispy_forms",
|
||||||
|
|
||||||
# Django apps
|
# Django apps
|
||||||
|
@ -154,6 +155,17 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/")
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
||||||
|
|
||||||
|
# Django REST Framework
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||||
|
"rest_framework.authentication.SessionAuthentication",
|
||||||
|
],
|
||||||
|
"DEFAULT_PERMISSION_CLASSES": [
|
||||||
|
"rest_framework.permissions.DjangoModelPermissions",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Crispy Forms
|
# Crispy Forms
|
||||||
CRISPY_TEMPLATE_PACK = "bootstrap4"
|
CRISPY_TEMPLATE_PACK = "bootstrap4"
|
||||||
|
|
||||||
|
|
19
drakul/base/signals.py
Normal file
19
drakul/base/signals.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from django.contrib.sessions.models import Session
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
|
from django.db.models.signals import pre_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from drakul.base.exceptions import drf_validation_error
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_save)
|
||||||
|
def pre_save_full_clean_handler(sender, instance, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Force all models to call full_clean() before save(), to keep validation logic DRY between DRF and forms.
|
||||||
|
"""
|
||||||
|
if sender == Session:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
instance.full_clean()
|
||||||
|
except DjangoValidationError as e:
|
||||||
|
raise drf_validation_error(e) # we must be in a DRF view, otherwise Django would already have caught the error
|
|
@ -30,71 +30,120 @@
|
||||||
.bg-warrior, .bg-class-9 {background-color: #c79c6e; color: #fff;}
|
.bg-warrior, .bg-class-9 {background-color: #c79c6e; color: #fff;}
|
||||||
|
|
||||||
|
|
||||||
/* WoW class-coloured alerts */
|
/* WoW class-coloured buttons */
|
||||||
.alert-druid, .alert-class-1 {
|
.btn-druid, .btn-class-1 {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #ff7d0a;
|
background-color: #ff7d0a;
|
||||||
border-color: #ff7d0a;
|
border-color: #ff7d0a;
|
||||||
}
|
}
|
||||||
|
.btn-druid:hover, .btn-class-1:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #eb7c0a;
|
||||||
|
border-color: #eb7c0a;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-hunter, .alert-class-2 {
|
.btn-hunter, .btn-class-2 {
|
||||||
color: #212529;
|
color: #212529;
|
||||||
background-color: #abd473;
|
background-color: #abd473;
|
||||||
border-color: #abd473;
|
border-color: #abd473;
|
||||||
}
|
}
|
||||||
|
.btn-hunter:hover, .btn-class-2:hover {
|
||||||
|
color: #212529;
|
||||||
|
background-color: #98c062;
|
||||||
|
border-color: #98c062;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-mage, .alert-class-3 {
|
.btn-mage, .btn-class-3 {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #69ccf0;
|
background-color: #69ccf0;
|
||||||
border-color: #69ccf0;
|
border-color: #69ccf0;
|
||||||
}
|
}
|
||||||
|
.btn-mage:hover, .btn-class-3:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #67b8dc;
|
||||||
|
border-color: #67b8dc;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-paladin, .alert-class-4 {
|
.btn-paladin, .btn-class-4 {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #f58cba;
|
background-color: #f58cba;
|
||||||
border-color: #f58cba;
|
border-color: #f58cba;
|
||||||
}
|
}
|
||||||
|
.btn-paladin:hover, .btn-class-4:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #e17aa7;
|
||||||
|
border-color: #e17aa7;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-priest, .alert-class-5 {
|
.btn-priest, .btn-class-5 {
|
||||||
color: #212529;
|
color: #212529;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-color: #ced4da;
|
border-color: #ced4da;
|
||||||
}
|
}
|
||||||
|
.btn-priest:hover, .btn-class-5:hover {
|
||||||
|
color: #212529;
|
||||||
|
background-color: #ebebeb;
|
||||||
|
border-color: #ced2d6;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-rogue, .alert-class-6 {
|
.btn-rogue, .btn-class-6 {
|
||||||
color: #212529;
|
color: #212529;
|
||||||
background-color: #fff569;
|
background-color: #fff569;
|
||||||
border-color: #f5eb63;
|
border-color: #f5eb63;
|
||||||
}
|
}
|
||||||
|
.btn-rogue:hover, .btn-class-6:hover {
|
||||||
|
color: #212529;
|
||||||
|
background-color: #ebe168;
|
||||||
|
border-color: #e1d761;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-shaman, .alert-class-7 {
|
.btn-shaman, .btn-class-7 {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #0070de;
|
background-color: #0070de;
|
||||||
border-color: #0070de;
|
border-color: #0070de;
|
||||||
}
|
}
|
||||||
|
.btn-shaman:hover, .btn-class-7:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #0065ca;
|
||||||
|
border-color: #0065ca;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-warlock, .alert-class-8 {
|
.btn-warlock, .btn-class-8 {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #9482c9;
|
background-color: #9482c9;
|
||||||
border-color: #9482c9;
|
border-color: #9482c9;
|
||||||
}
|
}
|
||||||
|
.btn-warlock:hover, .btn-class-8:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #816fb5;
|
||||||
|
border-color: #816fb5;
|
||||||
|
}
|
||||||
|
|
||||||
.alert-warrior, .alert-class-9 {
|
.btn-warrior, .btn-class-9 {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #c79c6e;
|
background-color: #c79c6e;
|
||||||
border-color: #c79c6e;
|
border-color: #c79c6e;
|
||||||
}
|
}
|
||||||
|
.btn-warrior:hover, .btn-class-9:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #b3895c;
|
||||||
|
border-color: #b3895c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-character-dropdown {
|
||||||
.response-character-alert {
|
|
||||||
width: 16ch;
|
|
||||||
padding: .375rem .75rem; /* from bootstrap's .btn */
|
|
||||||
text-align: center;
|
|
||||||
touch-action: none; /* required by interact.js */
|
touch-action: none; /* required by interact.js */
|
||||||
user-select: none; /* required by interact.js */
|
user-select: none; /* required by interact.js */
|
||||||
-moz-user-select: none; /* required by interact.js */
|
-moz-user-select: none; /* required by interact.js */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.response-character-btn {
|
||||||
|
width: 16ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-character-btn:disabled {
|
||||||
|
opacity: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Response bg and text colors from https://getbootstrap.com/docs/4.3/utilities/colors/ */
|
/* Response bg and text colors from https://getbootstrap.com/docs/4.3/utilities/colors/ */
|
||||||
.response-status-no-response-bg, .response-status-0-bg { /* secondary */
|
.response-status-no-response-bg, .response-status-0-bg { /* secondary */
|
||||||
|
|
9
drakul/base/viewsets.py
Normal file
9
drakul/base/viewsets.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from rest_framework import mixins, viewsets
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateViewSet(mixins.UpdateModelMixin,
|
||||||
|
viewsets.GenericViewSet):
|
||||||
|
"""
|
||||||
|
A viewset that provides `update()` and `partial_update()` actions.
|
||||||
|
"""
|
||||||
|
pass
|
|
@ -64,9 +64,6 @@ class RaidResponseForm(ModelForm):
|
||||||
self.helper.form_show_labels = False
|
self.helper.form_show_labels = False
|
||||||
self.helper.form_tag = False
|
self.helper.form_tag = False
|
||||||
|
|
||||||
def clean_role(self):
|
|
||||||
return self.cleaned_data["role"] or self.cleaned_data["character"].role
|
|
||||||
|
|
||||||
|
|
||||||
class GuestRaidResponseForm(ModelForm):
|
class GuestRaidResponseForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -104,15 +101,6 @@ class GuestRaidResponseForm(ModelForm):
|
||||||
self.fields["status"].initial = RaidResponse.Status.SIGNED_UP
|
self.fields["status"].initial = RaidResponse.Status.SIGNED_UP
|
||||||
|
|
||||||
|
|
||||||
class RaidResponseChangeForm(ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = RaidResponse
|
|
||||||
fields = ["role", "status", "group"]
|
|
||||||
|
|
||||||
def clean_role(self):
|
|
||||||
return self.cleaned_data["role"] or self.instance.character.role
|
|
||||||
|
|
||||||
|
|
||||||
class RaidForm(ModelForm):
|
class RaidForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Raid
|
model = Raid
|
||||||
|
|
|
@ -134,7 +134,7 @@ 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
|
# Either Character or Guest Name/Guest Class must be exclusively set
|
||||||
if self.character is None:
|
if self.character is None:
|
||||||
errors = {}
|
errors = {}
|
||||||
if self.guest_name is None:
|
if self.guest_name is None:
|
||||||
|
@ -149,10 +149,13 @@ class RaidResponse(models.Model):
|
||||||
|
|
||||||
# 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.role = None
|
||||||
if not self.is_guest:
|
if not self.is_guest:
|
||||||
self.character = self.character.user.main
|
self.character = self.character.user.main
|
||||||
self.role = None
|
|
||||||
elif self.role is None:
|
elif self.role is None:
|
||||||
|
if not self.is_guest:
|
||||||
|
self.role = self.character.role
|
||||||
|
else:
|
||||||
raise ValidationError({"role": "This field is required."})
|
raise ValidationError({"role": "This field is required."})
|
||||||
|
|
||||||
# Set attendance to one of the default values if status was changed
|
# Set attendance to one of the default values if status was changed
|
||||||
|
|
10
drakul/raids/serializers.py
Normal file
10
drakul/raids/serializers.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .models import RaidResponse
|
||||||
|
|
||||||
|
|
||||||
|
class RaidResponseSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = RaidResponse
|
||||||
|
fields = ["id", "character", "role", "status", "group"]
|
||||||
|
read_only_fields = ["id"]
|
|
@ -93,8 +93,12 @@
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
{% for response in rank_responses %}
|
{% 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 }}"
|
<div class="dropdown response-character-dropdown" data-response-id="{{ response.id }}">
|
||||||
{% if response.role is not None %} data-response-role="{{ response.role }}"{% endif %}>
|
<button id="response-character-btn-{{ response.id }}"
|
||||||
|
class="btn btn-class-{{ response.character_klass }} response-character-btn text-truncate mb-1"
|
||||||
|
type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||||
|
{% if not perms.raids.change_raid %}disabled{% endif %}
|
||||||
|
>
|
||||||
{% if response.is_guest %}
|
{% if response.is_guest %}
|
||||||
<span class="float-left">⇌</span>{{ response.character_name }}
|
<span class="float-left">⇌</span>{{ response.character_name }}
|
||||||
{% elif response.character != response.character.user.main %}
|
{% elif response.character != response.character.user.main %}
|
||||||
|
@ -102,6 +106,30 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ response.character_name }}
|
{{ response.character_name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
{% if perms.raids.change_raid %}
|
||||||
|
<div class="dropdown-menu" aria-labelledby="response-character-btn-{{ response.id }}">
|
||||||
|
{% if not response.is_guest %}
|
||||||
|
{% for character in response.character.user.characters.all %}
|
||||||
|
<button class="dropdown-item response-dropdown-btn" type="button"
|
||||||
|
data-response-character="{{ character.id }}"
|
||||||
|
data-response-role=""
|
||||||
|
{% if character == response.character %}disabled{% endif %}
|
||||||
|
>
|
||||||
|
{{ character }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
{% endif %}
|
||||||
|
{% for role_id, role_name in character_roles %}
|
||||||
|
<button class="dropdown-item response-dropdown-btn" type="button" data-response-role="{{ role_id }}"
|
||||||
|
{% if role_id == response.role %}disabled{% endif %}
|
||||||
|
>
|
||||||
|
{{ role_name }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -156,17 +184,31 @@
|
||||||
// Export button
|
// Export button
|
||||||
const charactersTextinput = document.querySelector("#characters-textinput");
|
const charactersTextinput = document.querySelector("#characters-textinput");
|
||||||
$("#exportModal").on("show.bs.modal", function (event) {
|
$("#exportModal").on("show.bs.modal", function (event) {
|
||||||
charactersTextinput.textContent = $(event.relatedTarget).closest(".card").find(".response-character-alert").map(function () {
|
charactersTextinput.textContent = $(event.relatedTarget).closest(".card").find(".response-character-btn").map(function () {
|
||||||
return this.innerText.trim();
|
return this.innerText.trim();
|
||||||
}).get().join("\n");
|
}).get().join("\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if perms.raids.change_raid %}
|
{% if perms.raids.change_raid %}
|
||||||
|
function updateResponse(id, data) {
|
||||||
|
document.body.style.cursor = "wait";
|
||||||
|
fetch(`/api/raids/responses/${id}/`, {
|
||||||
|
method: "PATCH",
|
||||||
|
mode: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"X-CSRFToken": getCookie("csrftoken"),
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}).then(function (response) {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Raid response dragging
|
// Raid response dragging
|
||||||
const csrftoken = getCookie("csrftoken");
|
|
||||||
let original;
|
let original;
|
||||||
const position = {};
|
const position = {};
|
||||||
interact(".response-character-alert").draggable({
|
interact(".response-character-dropdown").draggable({
|
||||||
manualStart: true,
|
manualStart: true,
|
||||||
listeners: {
|
listeners: {
|
||||||
start(event) {
|
start(event) {
|
||||||
|
@ -174,6 +216,7 @@
|
||||||
|
|
||||||
let boundingClientRect = original.getBoundingClientRect();
|
let boundingClientRect = original.getBoundingClientRect();
|
||||||
event.target.style.position = "fixed";
|
event.target.style.position = "fixed";
|
||||||
|
event.target.querySelector(".response-character-btn").style.cursor = "move";
|
||||||
event.target.style.top = boundingClientRect.top + "px";
|
event.target.style.top = boundingClientRect.top + "px";
|
||||||
event.target.style.left = boundingClientRect.left + "px";
|
event.target.style.left = boundingClientRect.left + "px";
|
||||||
position.x = 0;
|
position.x = 0;
|
||||||
|
@ -202,37 +245,30 @@
|
||||||
document.body.appendChild(clone);
|
document.body.appendChild(clone);
|
||||||
event.interaction.start({name: "drag"}, event.interactable, clone);
|
event.interaction.start({name: "drag"}, event.interactable, clone);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll(".response-character-alert").forEach(function (element) {
|
|
||||||
element.style.cursor = "move";
|
|
||||||
});
|
|
||||||
|
|
||||||
interact(".response-container").dropzone({
|
interact(".response-container").dropzone({
|
||||||
accept: ".response-character-alert",
|
accept: ".response-character-dropdown",
|
||||||
ondrop(event) {
|
ondrop(event) {
|
||||||
// ignore if dropped into its original dropzone
|
// ignore if dropped into its original dropzone
|
||||||
if (event.target.closest(".response-container") === original.closest(".response-container")) {
|
if (event.target.closest(".response-container") === original.closest(".response-container")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let form = new FormData();
|
updateResponse(event.relatedTarget.dataset.responseId, {
|
||||||
if (event.relatedTarget.dataset.responseRole) {
|
status: event.target.dataset.responseContainerStatus,
|
||||||
form.append("role", event.relatedTarget.dataset.responseRole);
|
group: event.target.dataset.responseContainerGroup,
|
||||||
}
|
|
||||||
form.append("status", event.target.dataset.responseContainerStatus);
|
|
||||||
form.append("group", event.target.dataset.responseContainerGroup);
|
|
||||||
fetch(`/raids/responses/${event.relatedTarget.dataset.responseId}/change/`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"X-CSRFToken": csrftoken,
|
|
||||||
},
|
|
||||||
mode: "same-origin",
|
|
||||||
body: form,
|
|
||||||
}).then(function (response) {
|
|
||||||
document.body.style.cursor = "wait";
|
|
||||||
location.reload();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Raid response dropdown
|
||||||
|
$(".response-dropdown-btn").on("click", function () {
|
||||||
|
let data = {
|
||||||
|
role: this.dataset.responseRole || null,
|
||||||
|
};
|
||||||
|
if (this.dataset.responseCharacter) {
|
||||||
|
data["character"] = this.dataset.responseCharacter;
|
||||||
|
}
|
||||||
|
updateResponse(this.parentElement.parentElement.dataset.responseId, data);
|
||||||
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
|
from rest_framework import routers
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register(r"raids/responses", views.RaidResponseUpdateViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("api/", include(router.urls)),
|
||||||
path("calendar/", views.RaidCalendar.as_view(), name="raid_calendar"),
|
path("calendar/", views.RaidCalendar.as_view(), name="raid_calendar"),
|
||||||
path("calendar/<int:year>/<int:month>/", views.RaidCalendar.as_view(), name="raid_calendar"),
|
path("calendar/<int:year>/<int:month>/", views.RaidCalendar.as_view(), name="raid_calendar"),
|
||||||
path("raids/create/", views.RaidCreateView.as_view(), name="raid_create"),
|
path("raids/create/", views.RaidCreateView.as_view(), name="raid_create"),
|
||||||
path("raids/<int:pk>/", views.RaidDetailView.as_view(), name="raid_detail"),
|
path("raids/<int:pk>/", views.RaidDetailView.as_view(), name="raid_detail"),
|
||||||
path("raids/<int:pk>/change/", views.RaidChangeView.as_view(), name="raid_change"),
|
path("raids/<int:pk>/change/", views.RaidChangeView.as_view(), name="raid_change"),
|
||||||
path("raids/<int:pk>/delete/", views.RaidDeleteView.as_view(), name="raid_delete"),
|
path("raids/<int:pk>/delete/", views.RaidDeleteView.as_view(), name="raid_delete"),
|
||||||
path("raids/responses/<int:pk>/change/", views.RaidResponseChangeView.as_view(), name="raid_response_change"),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,17 +5,19 @@ from datetime import timedelta
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.db.models import Q, Max
|
from django.db.models import Q, Max
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views.generic import DeleteView, CreateView, MonthArchiveView
|
from django.views.generic import DeleteView, CreateView, MonthArchiveView
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
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, GuestRaidResponseForm
|
GuestRaidResponseForm
|
||||||
from .models import Raid, RaidResponse, InstanceReset
|
from .models import Raid, RaidResponse, InstanceReset
|
||||||
|
from .serializers import RaidResponseSerializer
|
||||||
|
from ..base.viewsets import UpdateViewSet
|
||||||
|
from ..users.models import Character
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
@ -94,6 +96,7 @@ class RaidDetailView(SingleObjectMixin, MultiModelFormView):
|
||||||
return Raid.objects.prefetch_related(
|
return Raid.objects.prefetch_related(
|
||||||
"responses__character__user__rank",
|
"responses__character__user__rank",
|
||||||
"responses__character__user__main",
|
"responses__character__user__main",
|
||||||
|
"responses__character__user__characters",
|
||||||
"comments__user__main"
|
"comments__user__main"
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
@ -106,6 +109,7 @@ class RaidDetailView(SingleObjectMixin, MultiModelFormView):
|
||||||
for status, responses in itertools.groupby(context["raid"].responses.all(), key=lambda r: r.status):
|
for status, responses in itertools.groupby(context["raid"].responses.all(), key=lambda r: r.status):
|
||||||
raid_responses[status] = list(responses)
|
raid_responses[status] = list(responses)
|
||||||
context["raid_responses"] = raid_responses
|
context["raid_responses"] = raid_responses
|
||||||
|
context["character_roles"] = Character.Role.choices
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_form_classes(self):
|
def get_form_classes(self):
|
||||||
|
@ -166,22 +170,6 @@ class RaidDetailView(SingleObjectMixin, MultiModelFormView):
|
||||||
return super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RaidResponseChangeView(PermissionRequiredMixin, BaseUpdateView):
|
|
||||||
"""
|
|
||||||
For RaidResponse drag 'n dropping.
|
|
||||||
"""
|
|
||||||
permission_required = "raids.change_raid"
|
|
||||||
model = RaidResponse
|
|
||||||
form_class = RaidResponseChangeForm
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
form.save()
|
|
||||||
return HttpResponse(status=204)
|
|
||||||
|
|
||||||
def form_invalid(self, form):
|
|
||||||
return HttpResponse(form, status=400)
|
|
||||||
|
|
||||||
|
|
||||||
class RaidCreateView(PermissionRequiredMixin, CreateView):
|
class RaidCreateView(PermissionRequiredMixin, CreateView):
|
||||||
permission_required = "raids.add_raid"
|
permission_required = "raids.add_raid"
|
||||||
model = Raid
|
model = Raid
|
||||||
|
@ -229,3 +217,8 @@ class RaidDeleteView(PermissionRequiredMixin, DeleteView):
|
||||||
permission_required = "raids.delete_raid"
|
permission_required = "raids.delete_raid"
|
||||||
queryset = Raid.objects.all()
|
queryset = Raid.objects.all()
|
||||||
success_url = reverse_lazy("raid_calendar")
|
success_url = reverse_lazy("raid_calendar")
|
||||||
|
|
||||||
|
|
||||||
|
class RaidResponseUpdateViewSet(UpdateViewSet):
|
||||||
|
queryset = RaidResponse.objects.all()
|
||||||
|
serializer_class = RaidResponseSerializer
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
Django==3.1.1
|
Django==3.1.1
|
||||||
psycopg2-binary>=2.5.4 # required by Django for PostgreSQL support: https://docs.djangoproject.com/en/2.2/ref/databases/#postgresql-notes
|
psycopg2-binary>=2.5.4 # required by Django for PostgreSQL support: https://docs.djangoproject.com/en/2.2/ref/databases/#postgresql-notes
|
||||||
|
djangorestframework==3.12.1
|
||||||
django-crispy-forms==1.9.2
|
django-crispy-forms==1.9.2
|
||||||
|
|
Loading…
Reference in a new issue