00af109e2f
* add flake8 to pre-commit config, and fixup many things to make flake8 happy * add isort and sort all imports, add to pre-commit and requirements
408 lines
13 KiB
Python
408 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()
|
|
|
|
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()]
|