bornhack-website/src/teams/models.py

413 lines
13 KiB
Python

import logging
from django.conf import settings
from django.contrib.postgres.fields import DateTimeRangeField
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse_lazy
from django.utils.text import slugify
from utils.models import CampRelatedModel, CreatedUpdatedModel, UUIDModel
logger = logging.getLogger("bornhack.%s" % __name__)
TEAM_GUIDE_TEMPLATE = """
## Preparations
...
## Camp setup
...
## During camp
...
## Takedown
...
## Notes for next year
1. Remember to take notes
1. ...
"""
class Team(CampRelatedModel):
camp = models.ForeignKey(
"camps.Camp", related_name="teams", on_delete=models.PROTECT
)
name = models.CharField(max_length=255, help_text="The team name")
slug = models.SlugField(
max_length=255,
blank=True,
help_text="Url slug for this team. Leave blank to generate based on team name",
)
shortslug = models.SlugField(
help_text="Abbreviated version of the slug. Used in places like IRC channel names where space is limited"
)
description = models.TextField()
permission_set = models.CharField(
max_length=100,
blank=True,
default="",
help_text="The name of this Teams set of permissions. Must be a value from camps.models.Permission.Meta.permissions.",
)
needs_members = models.BooleanField(
default=True, help_text="Check to indicate that this team needs more members"
)
members = models.ManyToManyField(
"auth.User", related_name="teams", through="teams.TeamMember"
)
# mailing list related fields
mailing_list = models.EmailField(blank=True)
mailing_list_archive_public = models.BooleanField(
default=False, help_text="Check if the mailing list archive is public"
)
mailing_list_nonmember_posts = models.BooleanField(
default=False,
help_text="Check if the mailinglist allows non-list-members to post",
)
# IRC related fields
public_irc_channel_name = models.CharField(
blank=True,
null=True,
unique=True,
max_length=50,
help_text="The public IRC channel for this team. Will be shown on the team page so people know how to reach the team. Leave empty if the team has no public IRC channel.",
)
public_irc_channel_bot = models.BooleanField(
default=False,
help_text="Check to make the bot join the teams public IRC channel. Leave unchecked to disable the IRC bot for this channel.",
)
public_irc_channel_managed = models.BooleanField(
default=False,
help_text="Check to make the bot manage the teams public IRC channel by registering it with NickServ and setting +Oo for all teammembers.",
)
public_irc_channel_fix_needed = models.BooleanField(
default=False,
help_text="Used to indicate to the IRC bot that this teams public IRC channel is in need of a permissions and ACL fix.",
)
private_irc_channel_name = models.CharField(
blank=True,
null=True,
unique=True,
max_length=50,
help_text="The private IRC channel for this team. Will be shown to team members on the team page. Leave empty if the team has no private IRC channel.",
)
private_irc_channel_bot = models.BooleanField(
default=False,
help_text="Check to make the bot join the teams private IRC channel. Leave unchecked to disable the IRC bot for this channel.",
)
private_irc_channel_managed = models.BooleanField(
default=False,
help_text="Check to make the bot manage the private IRC channel by registering it with NickServ, setting +I and maintaining the ACL.",
)
private_irc_channel_fix_needed = models.BooleanField(
default=False,
help_text="Used to indicate to the IRC bot that this teams private IRC channel is in need of a permissions and ACL fix.",
)
shifts_enabled = models.BooleanField(
default=False,
help_text="Does this team have shifts? This enables defining shifts for this team.",
)
class Meta:
ordering = ["name"]
unique_together = (("name", "camp"), ("slug", "camp"))
guide = models.TextField(
blank=True,
help_text="HowTo guide for this year (and next year)",
verbose_name="team guide (Markdown)",
default=TEAM_GUIDE_TEMPLATE,
)
def __str__(self):
return "{} ({})".format(self.name, self.camp)
def get_absolute_url(self):
return reverse_lazy(
"teams:general",
kwargs={"camp_slug": self.camp.slug, "team_slug": self.slug},
)
def save(self, **kwargs):
# generate slug if needed
if not self.pk or not self.slug:
slug = slugify(self.name)
self.slug = slug
# set shortslug if needed
if not self.shortslug:
self.shortslug = self.slug
super().save(**kwargs)
def clean(self):
# make sure the public irc channel name is prefixed with a # if it is set
if self.public_irc_channel_name and self.public_irc_channel_name[0] != "#":
self.public_irc_channel_name = "#%s" % self.public_irc_channel_name
# make sure the private irc channel name is prefixed with a # if it is set
if self.private_irc_channel_name and self.private_irc_channel_name[0] != "#":
self.private_irc_channel_name = "#%s" % self.private_irc_channel_name
# make sure the channel names are not reserved
if (
self.public_irc_channel_name == settings.IRCBOT_PUBLIC_CHANNEL
or self.public_irc_channel_name == settings.IRCBOT_VOLUNTEER_CHANNEL
):
raise ValidationError("The public IRC channel name is reserved")
if (
self.private_irc_channel_name == settings.IRCBOT_PUBLIC_CHANNEL
or self.private_irc_channel_name == settings.IRCBOT_VOLUNTEER_CHANNEL
):
raise ValidationError("The private IRC channel name is reserved")
# make sure public_irc_channel_name is not in use as public or private irc channel for another team, case insensitive
if self.public_irc_channel_name:
if (
Team.objects.filter(
private_irc_channel_name__iexact=self.public_irc_channel_name
)
.exclude(pk=self.pk)
.exists()
or Team.objects.filter(
public_irc_channel_name__iexact=self.public_irc_channel_name
)
.exclude(pk=self.pk)
.exists()
):
raise ValidationError(
"The public IRC channel name is already in use on another team!"
)
# make sure private_irc_channel_name is not in use as public or private irc channel for another team, case insensitive
if self.private_irc_channel_name:
if (
Team.objects.filter(
private_irc_channel_name__iexact=self.private_irc_channel_name
)
.exclude(pk=self.pk)
.exists()
or Team.objects.filter(
public_irc_channel_name__iexact=self.private_irc_channel_name
)
.exclude(pk=self.pk)
.exists()
):
raise ValidationError(
"The private IRC channel name is already in use on another team!"
)
@property
def memberships(self):
"""
Returns all TeamMember objects for this team.
Use self.members.all() to get User objects for all members,
or use self.memberships.all() to get TeamMember objects for all members.
"""
return TeamMember.objects.filter(team=self)
@property
def approved_members(self):
"""
Returns only approved members (returns User objects, not TeamMember objects)
"""
return self.members.filter(teammember__approved=True)
@property
def unapproved_members(self):
"""
Returns only unapproved members (returns User objects, not TeamMember objects)
"""
return self.members.filter(teammember__approved=False)
@property
def responsible_members(self):
"""
Return only approved and responsible members
Used to handle permissions for team management
"""
return self.members.filter(
teammember__approved=True, teammember__responsible=True
)
@property
def regular_members(self):
"""
Return only approved and not responsible members with
an approved public_credit_name.
Used on the people pages.
"""
return self.members.filter(
teammember__approved=True, teammember__responsible=False
)
@property
def unnamed_members(self):
"""
Returns only approved and not responsible members,
without an approved public_credit_name.
"""
return self.members.filter(
teammember__approved=True,
teammember__responsible=False,
profile__public_credit_name_approved=False,
)
class TeamMember(CampRelatedModel):
user = models.ForeignKey(
"auth.User",
on_delete=models.PROTECT,
help_text="The User object this team membership relates to",
)
team = models.ForeignKey(
"teams.Team",
on_delete=models.PROTECT,
help_text="The Team this membership relates to",
)
approved = models.BooleanField(
default=False, help_text="True if this membership is approved. False if not."
)
responsible = models.BooleanField(
default=False,
help_text="True if this teammember is responsible for this Team. False if not.",
)
irc_acl_fix_needed = models.BooleanField(
default=False,
help_text="Maintained by the IRC bot, manual editing should not be needed. Will be set to true when a teammember sets or changes NickServ username, and back to false after the ACL has been fixed by the bot.",
)
class Meta:
ordering = ["-responsible", "-approved"]
def __str__(self):
return "{} is {} {} member of team {}".format(
self.user,
"" if self.approved else "an unapproved",
"" if not self.responsible else "a responsible",
self.team,
)
@property
def camp(self):
""" All CampRelatedModels must have a camp FK or a camp property """
return self.team.camp
camp_filter = "team__camp"
class TeamTask(CampRelatedModel):
team = models.ForeignKey(
"teams.Team",
related_name="tasks",
on_delete=models.PROTECT,
help_text="The team this task belongs to",
)
name = models.CharField(max_length=100, help_text="Short name of this task")
slug = models.SlugField(
max_length=255, blank=True, help_text="url slug, leave blank to autogenerate"
)
description = models.TextField(
help_text="Description of the task. Markdown is supported."
)
when = DateTimeRangeField(
blank=True,
null=True,
help_text="When does this task need to be started and/or finished?",
)
completed = models.BooleanField(
help_text="Check to mark this task as completed.", default=False
)
class Meta:
ordering = ["completed", "when", "name"]
unique_together = (("name", "team"), ("slug", "team"))
def get_absolute_url(self):
return reverse_lazy(
"teams:task_detail",
kwargs={
"camp_slug": self.team.camp.slug,
"team_slug": self.team.slug,
"slug": self.slug,
},
)
@property
def camp(self):
""" All CampRelatedModels must have a camp FK or a camp property """
return self.team.camp
camp_filter = "team__camp"
def save(self, **kwargs):
# generate slug if needed
if not self.slug:
self.slug = slugify(self.name)
super().save(**kwargs)
class TaskComment(UUIDModel, CreatedUpdatedModel):
task = models.ForeignKey(
"teams.TeamTask", on_delete=models.PROTECT, related_name="comments"
)
author = models.ForeignKey("teams.TeamMember", on_delete=models.PROTECT)
comment = models.TextField()
class TeamShift(CampRelatedModel):
class Meta:
ordering = ("shift_range",)
team = models.ForeignKey(
"teams.Team",
related_name="shifts",
on_delete=models.PROTECT,
help_text="The team this shift belongs to",
)
shift_range = DateTimeRangeField()
team_members = models.ManyToManyField(TeamMember, blank=True)
people_required = models.IntegerField(default=1)
@property
def camp(self):
""" All CampRelatedModels must have a camp FK or a camp property """
return self.team.camp
camp_filter = "team__camp"
def __str__(self):
return "{} team shift from {} to {}".format(
self.team.name, self.shift_range.lower, self.shift_range.upper
)
@property
def users(self):
return [member.user for member in self.team_members.all()]