Add ranks and auto-attendance. Soft-resets migrations.
This commit is contained in:
parent
d5e65c8191
commit
9af691f8ca
|
@ -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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -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 = [
|
|
@ -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?
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
26
drakul/users/migrations/0002_create_default_ranks.py
Normal file
26
drakul/users/migrations/0002_create_default_ranks.py
Normal 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),
|
||||||
|
]
|
|
@ -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 = [
|
|
@ -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(
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue