Compare commits

...

3 commits

15 changed files with 168 additions and 54 deletions

View file

@ -155,3 +155,8 @@ SITE_ID = 1
# Crispy Forms # Crispy Forms
CRISPY_TEMPLATE_PACK = "bootstrap4" CRISPY_TEMPLATE_PACK = "bootstrap4"
# Drakul
DEFAULT_ATTENDANCE_ATTENDING = 1.0
DEFAULT_ATTENDANCE_NOT_ATTENDING = 0.0

View file

@ -1,13 +1,23 @@
/* WoW class-coloured text */ /* WoW class-coloured text */
.class-druid {color: #ff7d0a;} .color-druid, .color-class-1 {color: #ff7d0a;}
.class-hunter {color: #abd473;} .color-hunter, .color-class-2 {color: #abd473;}
.class-mage {color: #69ccf0;} .color-mage, .color-class-3 {color: #69ccf0;}
.class-paladin {color: #f58cba;} .color-paladin, .color-class-4 {color: #f58cba;}
.class-priest {color: #ffffff;} .color-priest, .color-class-5 {color: #ffffff;}
.class-rogue {color: #fff569;} .color-rogue, .color-class-6 {color: #fff569;}
.class-shaman {color: #0070de;} .color-shaman, .color-class-7 {color: #0070de;}
.class-warlock {color: #9482c9;} .color-warlock, .color-class-8 {color: #9482c9;}
.class-warrior {color: #c79c6e;} .color-warrior, .color-class-9 {color: #c79c6e;}
/* WoW class-coloured backgrounds */
.bg-druid, .bg-class-1 {background-color: #ff7d0a; color: #fff;}
.bg-hunter, .bg-class-2 {background-color: #abd473; color: #343a40;}
.bg-mage, .bg-class-3 {background-color: #69ccf0; color: #fff;}
.bg-paladin, .bg-class-4 {background-color: #f58cba; color: #fff;}
.bg-priest, .bg-class-5 {background-color: #ffffff; color: #343a40;}
.bg-rogue, .bg-class-6 {background-color: #fff569; color: #343a40;}
.bg-shaman, .bg-class-7 {background-color: #0070de; color: #fff;}
.bg-warlock, .bg-class-8 {background-color: #9482c9; color: #fff;}
.bg-warrior, .bg-class-9 {background-color: #c79c6e; color: #fff;}
/* WoW class-coloured buttons */ /* WoW class-coloured buttons */

View file

@ -23,7 +23,7 @@
<div class="collapse navbar-collapse" id="navbar-collapse"> <div class="collapse navbar-collapse" id="navbar-collapse">
<div class="navbar-nav mr-auto"> <div class="navbar-nav mr-auto">
<a class="nav-item nav-link" href="{% url 'raid_calendar' %}">Raids</a> <a class="nav-item nav-link" href="{% url 'raid_calendar' %}">Raids</a>
<!--<a class="nav-item nav-link" href="{% url 'user_list' %}">Users</a>--> <a class="nav-item nav-link" href="{% url 'user_list' %}">Users</a>
<!--<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">DKP</a>--> <!--<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">DKP</a>-->
</div> </div>
{% if user.is_authenticated %} {% if user.is_authenticated %}

View file

@ -1,7 +1,7 @@
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
from django.forms import ModelForm, modelformset_factory, inlineformset_factory from django.forms import ModelForm, inlineformset_factory
from .models import RaidResponse, RaidComment, Raid from .models import RaidResponse, RaidComment, Raid

View file

@ -1,4 +1,4 @@
# Generated by Django 2.2.6 on 2019-10-25 13:23 # Generated by Django 2.2.6 on 2019-11-19 03:10
from django.db import migrations, models from django.db import migrations, models
@ -55,7 +55,7 @@ class Migration(migrations.Migration):
('role', models.PositiveSmallIntegerField(blank=True, choices=[(1, 'Tank'), (2, 'Healer'), (3, 'Damage')], null=True)), ('role', models.PositiveSmallIntegerField(blank=True, choices=[(1, 'Tank'), (2, 'Healer'), (3, 'Damage')], null=True)),
('status', models.PositiveSmallIntegerField(choices=[(1, 'Signed Off'), (2, 'Signed Up'), (3, 'Stand By'), (4, 'Confirmed')], default=2)), ('status', models.PositiveSmallIntegerField(choices=[(1, 'Signed Off'), (2, 'Signed Up'), (3, 'Stand By'), (4, 'Confirmed')], default=2)),
('note', models.CharField(blank=True, max_length=100, null=True)), ('note', models.CharField(blank=True, max_length=100, null=True)),
('attendance', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True)), ('attendance', models.DecimalField(blank=True, decimal_places=2, max_digits=3)),
], ],
options={ options={
'ordering': ['-status', 'role', 'character__klass', 'character__name'], 'ordering': ['-status', 'role', 'character__klass', 'character__name'],

View file

@ -1,4 +1,4 @@
# Generated by Django 2.2.6 on 2019-10-25 13:23 # Generated by Django 2.2.6 on 2019-11-19 03:10
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -10,9 +10,9 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('raids', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('users', '0001_initial'), ('users', '0001_initial'),
('raids', '0001_initial'),
] ]
operations = [ operations = [

View file

@ -29,7 +29,6 @@ class Raid(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.response_deadline is None: if self.response_deadline is None:
self.response_deadline = self.date - timedelta(hours=24) self.response_deadline = self.date - timedelta(hours=24)
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
def __str__(self): def __str__(self):
@ -85,8 +84,7 @@ class RaidResponse(models.Model):
attendance = models.DecimalField( attendance = models.DecimalField(
max_digits=3, max_digits=3,
decimal_places=2, decimal_places=2,
blank=True, blank=True
null=True
) )
class Meta: class Meta:
@ -102,6 +100,15 @@ class RaidResponse(models.Model):
elif self.role is None: elif self.role is None:
raise ValidationError({"role": "This field is required."}) raise ValidationError({"role": "This field is required."})
def save(self, *args, **kwargs):
# Set attendance if it hasn't been set manually
if self.attendance is None:
if self.status >= RaidResponse.SIGNED_UP:
self.attendance = settings.DEFAULT_ATTENDANCE_ATTENDING # 1.0 by default
else:
self.attendance = settings.DEFAULT_ATTENDANCE_NOT_ATTENDING # 0.0 by default
super().save(*args, **kwargs)
def __str__(self): def __str__(self):
return super().__str__() # TODO? return super().__str__() # TODO?

View file

@ -19,7 +19,6 @@
<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-md-6"> <div class="form-group col-md-6">
<label>Change Status</label> <label>Change Status</label>

View file

@ -1,7 +1,13 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from .models import User, Character from .models import User, Character, Rank
@admin.register(Rank)
class RankAdmin(admin.ModelAdmin):
list_display = ["position", "name"]
list_display_links = ["name"]
class CharacterInline(admin.TabularInline): class CharacterInline(admin.TabularInline):
@ -22,13 +28,13 @@ class UserAdmin(DjangoUserAdmin):
("Important dates", { ("Important dates", {
"fields": ("last_login", "date_joined") "fields": ("last_login", "date_joined")
}), }),
("Character", { ("World of Warcraft", {
"fields": ("main",) "fields": ("rank", "main")
}), }),
) )
inlines = [CharacterInline] inlines = [CharacterInline]
ordering = None # use default model ordering ordering = None # use default model ordering
list_display = ["username", "main", "avg_attendance", "is_active", "is_staff", "is_superuser"] list_display = ["username", "rank", "main", "is_active", "is_staff", "is_superuser"]
list_filter = ["is_active", "is_staff", "is_superuser"] list_filter = ["is_active", "is_staff", "is_superuser"]
search_fields = ["username", "main__name"] search_fields = ["username", "main__name"]

View file

@ -1,12 +1,12 @@
# Generated by Django 2.2.6 on 2019-10-25 13:23 # Generated by Django 2.2.6 on 2019-11-19 03:10
from django.conf import settings from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
import django.core.validators import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
import drakul.users.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -33,12 +33,23 @@ class Migration(migrations.Migration):
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
], ],
options={ options={
'ordering': ['-is_superuser', '-is_staff', 'username'], 'ordering': ['rank', 'username'],
}, },
managers=[ managers=[
('objects', drakul.users.models.UserManager()), ('objects', django.contrib.auth.models.UserManager()),
], ],
), ),
migrations.CreateModel(
name='Rank',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveSmallIntegerField(unique=True)),
('name', models.CharField(max_length=30, unique=True)),
],
options={
'ordering': ['position'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='Character', name='Character',
fields=[ fields=[
@ -57,6 +68,11 @@ class Migration(migrations.Migration):
name='main', name='main',
field=models.OneToOneField(blank=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.Character'), field=models.OneToOneField(blank=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='users.Character'),
), ),
migrations.AddField(
model_name='user',
name='rank',
field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.PROTECT, to='users.Rank'),
),
migrations.AddField( migrations.AddField(
model_name='user', model_name='user',
name='user_permissions', name='user_permissions',

View file

@ -0,0 +1,26 @@
# Generated by Django 2.2.6 on 2019-11-18 22:56
from django.db import migrations
def create_default_ranks(apps, schema_editor):
"""
https://docs.djangoproject.com/en/2.2/topics/migrations/#data-migrations
"""
Rank = apps.get_model("users", "Rank")
Rank.objects.create(position=1, name="Guild Master")
Rank.objects.create(position=2, name="Officer")
Rank.objects.create(position=3, name="Veteran")
Rank.objects.create(position=4, name="Member")
Rank.objects.create(position=5, name="Initiate")
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.RunPython(create_default_ranks),
]

View file

@ -10,17 +10,19 @@ def create_admin(apps, schema_editor):
# The docs says to use 'User = apps.get_model("users", "User")', but when doing that # The docs says to use 'User = apps.get_model("users", "User")', but when doing that
# 'self.model.normalize_username()', which is called from create_superuser(), cannot be accessed.. # 'self.model.normalize_username()', which is called from create_superuser(), cannot be accessed..
from drakul.users.models import User from drakul.users.models import User
Rank = apps.get_model("users", "Rank")
User.objects.create_superuser( User.objects.create_superuser(
username="admin", username="admin",
email="admin@localhost", email="admin@localhost",
password="admin" password="admin",
rank_id=Rank.objects.first().id
) )
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('users', '0001_initial'), ('users', '0002_create_default_ranks'),
] ]
operations = [ operations = [

View file

@ -1,21 +1,26 @@
from django.contrib.auth.models import AbstractUser, UserManager as DjangoUserManager from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator, MinLengthValidator from django.core.validators import RegexValidator, MinLengthValidator
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Avg
class UserManager(DjangoUserManager): class Rank(models.Model):
def get_queryset(self): position = models.PositiveSmallIntegerField(
qs = super().get_queryset() unique=True
return qs.annotate(
avg_attendance=Avg("characters__raid_responses__attendance")
) )
name = models.CharField(
max_length=30,
unique=True
)
class Meta:
ordering = ["position"]
def __str__(self):
return self.name
class User(AbstractUser): class User(AbstractUser):
objects = UserManager()
first_name = None first_name = None
last_name = None last_name = None
@ -26,8 +31,14 @@ class User(AbstractUser):
blank=True blank=True
) )
rank = models.ForeignKey(
Rank,
on_delete=models.PROTECT,
blank=True
)
class Meta: class Meta:
ordering = ["-is_superuser", "-is_staff", "username"] ordering = ["rank", "username"]
def clean(self): def clean(self):
if hasattr(self, "main") and self.main.user != self: if hasattr(self, "main") and self.main.user != self:
@ -35,6 +46,9 @@ class User(AbstractUser):
@transaction.atomic @transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not hasattr(self, "rank"):
self.rank = Rank.objects.last()
if not hasattr(self, "main"): if not hasattr(self, "main"):
self.main = Character.objects.create( self.main = Character.objects.create(
user=None, user=None,
@ -43,15 +57,11 @@ class User(AbstractUser):
role=Character.DAMAGE role=Character.DAMAGE
) )
self.main.save() self.main.save()
user = super().save(*args, **kwargs) user = super().save(*args, **kwargs)
self.main.user = self self.main.user = self
self.main.save() self.main.save()
return user return user
def avg_attendance(self):
return self.avg_attendance
class Character(models.Model): class Character(models.Model):
user = models.ForeignKey( user = models.ForeignKey(

View file

@ -1,15 +1,34 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Users{% endblock %}
{% block content %} {% block content %}
<h2>Users</h2> <h2>Users</h2>
<ul> <div class="card">
<div class="card-body">
<table class="table">
<thead class="table-borderless">
<tr>
<th scope="col">Character Name</th>
<th scope="col">Rank</th>
<th scope="col">Class</th>
<th scope="col">Role</th>
<th scope="col">Avg Attendance (Month, Total)</th>
</tr>
</thead>
<tbody>
{% for user in user_list %} {% for user in user_list %}
<li>{{ user.username }} {{ user.avg_attendance | floatformat:2 }}</li> <tr>
<ul> <th scope="row">{{ user.main.name }}</th>
{% for character in user.characters.all %} <td>{{ user.rank }}</td>
<li>{{ character.name }} {{ character.get_klass_display }} {{ character.get_role_display }}</li> <td>{{ user.main.get_klass_display }}</td>
<td>{{ user.main.get_role_display }}</td>
<td>{{ user.avg_attendance_month | floatformat:2 }}, {{ user.avg_attendance_total | floatformat:2 }}</td>
</tr>
{% endfor %} {% endfor %}
</ul> </tbody>
{% endfor %} </table>
</ul> </div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,3 +1,5 @@
from django.db.models import Avg, Q
from django.utils import timezone
from django.views.generic import ListView from django.views.generic import ListView
from .models import User from .models import User
@ -5,7 +7,19 @@ from .models import User
class UserListView(ListView): class UserListView(ListView):
def get_queryset(self): def get_queryset(self):
return User.objects.prefetch_related("characters").all() return User.objects.select_related("main").annotate(
avg_attendance_total=Avg(
"characters__raid_responses__attendance",
filter=Q(characters__raid_responses__raid__date__lte=timezone.now()) # only count past raids
),
avg_attendance_month=Avg(
"characters__raid_responses__attendance",
filter=Q(
characters__raid_responses__raid__date__lte=timezone.now(),
characters__raid_responses__raid__date__gte=timezone.now() - timezone.timedelta(days=31)
)
)
).order_by("rank", "main__name")
# CharacterDetailView: # CharacterDetailView: