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_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 */
.class-druid {color: #ff7d0a;}
.class-hunter {color: #abd473;}
.class-mage {color: #69ccf0;}
.class-paladin {color: #f58cba;}
.class-priest {color: #ffffff;}
.class-rogue {color: #fff569;}
.class-shaman {color: #0070de;}
.class-warlock {color: #9482c9;}
.class-warrior {color: #c79c6e;}
.color-druid, .color-class-1 {color: #ff7d0a;}
.color-hunter, .color-class-2 {color: #abd473;}
.color-mage, .color-class-3 {color: #69ccf0;}
.color-paladin, .color-class-4 {color: #f58cba;}
.color-priest, .color-class-5 {color: #ffffff;}
.color-rogue, .color-class-6 {color: #fff569;}
.color-shaman, .color-class-7 {color: #0070de;}
.color-warlock, .color-class-8 {color: #9482c9;}
.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 */

View file

@ -23,7 +23,7 @@
<div class="collapse navbar-collapse" id="navbar-collapse">
<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 '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>-->
</div>
{% if user.is_authenticated %}

View file

@ -1,7 +1,7 @@
from crispy_forms.bootstrap import StrictButton
from crispy_forms.helper import FormHelper
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

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
@ -55,7 +55,7 @@ class Migration(migrations.Migration):
('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)),
('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={
'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.db import migrations, models
@ -10,9 +10,9 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('raids', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('users', '0001_initial'),
('raids', '0001_initial'),
]
operations = [

View file

@ -29,7 +29,6 @@ class Raid(models.Model):
def save(self, *args, **kwargs):
if self.response_deadline is None:
self.response_deadline = self.date - timedelta(hours=24)
return super().save(*args, **kwargs)
def __str__(self):
@ -85,8 +84,7 @@ class RaidResponse(models.Model):
attendance = models.DecimalField(
max_digits=3,
decimal_places=2,
blank=True,
null=True
blank=True
)
class Meta:
@ -102,6 +100,15 @@ class RaidResponse(models.Model):
elif self.role is None:
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):
return super().__str__() # TODO?

View file

@ -19,7 +19,6 @@
<form class="responses-form table-borderless" method="post" action="{% url 'raid_change' raid.id %}">
{% crispy raid_response_formset raid_response_formset_helper %}
<hr>
<div class="row">
<div class="form-group col-md-6">
<label>Change Status</label>

View file

@ -1,7 +1,13 @@
from django.contrib import admin
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):
@ -22,13 +28,13 @@ class UserAdmin(DjangoUserAdmin):
("Important dates", {
"fields": ("last_login", "date_joined")
}),
("Character", {
"fields": ("main",)
("World of Warcraft", {
"fields": ("rank", "main")
}),
)
inlines = [CharacterInline]
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"]
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
import django.contrib.auth.models
import django.contrib.auth.validators
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import drakul.users.models
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')),
],
options={
'ordering': ['-is_superuser', '-is_staff', 'username'],
'ordering': ['rank', 'username'],
},
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(
name='Character',
fields=[
@ -57,6 +68,11 @@ class Migration(migrations.Migration):
name='main',
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(
model_name='user',
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
# 'self.model.normalize_username()', which is called from create_superuser(), cannot be accessed..
from drakul.users.models import User
Rank = apps.get_model("users", "Rank")
User.objects.create_superuser(
username="admin",
email="admin@localhost",
password="admin"
password="admin",
rank_id=Rank.objects.first().id
)
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
('users', '0002_create_default_ranks'),
]
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.validators import RegexValidator, MinLengthValidator
from django.db import models, transaction
from django.db.models import Avg
class UserManager(DjangoUserManager):
def get_queryset(self):
qs = super().get_queryset()
return qs.annotate(
avg_attendance=Avg("characters__raid_responses__attendance")
class Rank(models.Model):
position = models.PositiveSmallIntegerField(
unique=True
)
name = models.CharField(
max_length=30,
unique=True
)
class Meta:
ordering = ["position"]
def __str__(self):
return self.name
class User(AbstractUser):
objects = UserManager()
first_name = None
last_name = None
@ -26,8 +31,14 @@ class User(AbstractUser):
blank=True
)
rank = models.ForeignKey(
Rank,
on_delete=models.PROTECT,
blank=True
)
class Meta:
ordering = ["-is_superuser", "-is_staff", "username"]
ordering = ["rank", "username"]
def clean(self):
if hasattr(self, "main") and self.main.user != self:
@ -35,6 +46,9 @@ class User(AbstractUser):
@transaction.atomic
def save(self, *args, **kwargs):
if not hasattr(self, "rank"):
self.rank = Rank.objects.last()
if not hasattr(self, "main"):
self.main = Character.objects.create(
user=None,
@ -43,15 +57,11 @@ class User(AbstractUser):
role=Character.DAMAGE
)
self.main.save()
user = super().save(*args, **kwargs)
self.main.user = self
self.main.save()
return user
def avg_attendance(self):
return self.avg_attendance
class Character(models.Model):
user = models.ForeignKey(

View file

@ -1,15 +1,34 @@
{% extends "base.html" %}
{% block title %}Users{% endblock %}
{% block content %}
<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 %}
<li>{{ user.username }} {{ user.avg_attendance | floatformat:2 }}</li>
<ul>
{% for character in user.characters.all %}
<li>{{ character.name }} {{ character.get_klass_display }} {{ character.get_role_display }}</li>
<tr>
<th scope="row">{{ user.main.name }}</th>
<td>{{ user.rank }}</td>
<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 %}
</ul>
{% endfor %}
</ul>
</tbody>
</table>
</div>
</div>
{% 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 .models import User
@ -5,7 +7,19 @@ from .models import User
class UserListView(ListView):
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: