Add ranks and auto-attendance. Soft-resets migrations.

This commit is contained in:
Casper V. Kristensen 2019-11-19 03:32:21 +01:00
parent d5e65c8191
commit 9af691f8ca
Signed by: caspervk
GPG key ID: 289CA03790535054
12 changed files with 148 additions and 43 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

@ -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,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

@ -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>
{% 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>
<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 %}
<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: