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 Forms
CRISPY_TEMPLATE_PACK = "bootstrap4" 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="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,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

@ -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">
{% for user in user_list %} <div class="card-body">
<li>{{ user.username }} {{ user.avg_attendance | floatformat:2 }}</li> <table class="table">
<ul> <thead class="table-borderless">
{% for character in user.characters.all %} <tr>
<li>{{ character.name }} {{ character.get_klass_display }} {{ character.get_role_display }}</li> <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 %} {% 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: