Merge teamcomms branch. Refactor team app and add events app.

* Primary commit towards improved team communications. Add new events app to handle team notifications when various events happen, with a Type model which contain event types and a Routing model which controls routing of events to teams. Add shortslug for Camp and Team models. events.handler.py contains the code for sending irc and email notifications for teams. The first two eventtypes have been added in datamigrations, 'ticket_created' and 'public_credit_name_changed', and the tickets and profile apps have been adjusted accordingly. Team IRC channels can be marked as managed and if so the IRC bot will register the team channel with ChanServ if possible. Team IRC channels can be marked as private and the bot will set invite only and maintain an ACL with team members. Users can set their NickServ username in their profile to get on the ACL. Rework all team views and templates. Remove TeamArea model and make Team have an FK to Camp directly. Add docstrings a whole bunch of places. Move signal handlers to apps.py and signal_handlers.py in a few apps. Add basic team mailing list handling, more work to be done. Update bootstrap-devsite script to add more teammembers and add some team event routing for the two eventtypes we have.

* default to the console backend for email unless we specifically ask for realworld email

* fix signal for public_credit_name approval irc message

* fix name display on /people/ page

* fix the text on people pages when all non-responsible team members are anonymous

* handle cases where we fallback to the area responsible properly

* readd removed property, it is used in team_detail view

* make it possible to filter profiles by public_credit_name_approved

* add method for sending IRC messages in ircbot.utils.add_irc_message(), extend periodic bot method to do more than check for outgoing messages so rename it, refactor chanserv and nickserv handling code, create methods to check and join/part IRC channels as needed, maintain channel ACLs for private channels, do not autojoin any channels when instatiating the bot instead rely on the new check_irc_channels() method to join them, rename profile presave signal, add checking for changed nickserv usernames for acl handling, add teammember.irc_channel_acl_ok boolean to track ACL state, add missing help_text properties to TeamMember fields, rename teammember postsave signal, add teammember deleted signal, readd wrongly deleted EnsureTeamMemberResponsibleMixin

* add a few missing early returns
This commit is contained in:
Thomas Steen Rasmussen 2018-04-09 23:11:05 +02:00 committed by GitHub
parent 03fc20a459
commit edcf363027
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1934 additions and 411 deletions

69
scripts/schemagif.sh Executable file
View File

@ -0,0 +1,69 @@
#!/bin/sh
#################################
# Loop over migrations in the
# BornHack website project, apply
# one by one, and run
# postgresql_autodoc for each.
#
# Use the generated .dot files
# to generate PNGs and watermark
# the PNG with the migration name.
#
# Finally use $whatever to combine
# all the PNGs to an animation and
# marvel at the ingenuity of Man.
#
# This scripts makes a million
# assumptions about the local env.
# and installed packages. Enjoy!
#
# /Tykling, April 2018
#################################
#set -x
# warn the user
read -p "WARNING: This scripts deletes and recreates the local pg database named bornhackdb several times. Continue? "
# wipe database
sudo su postgres -c "dropdb bornhackdb; createdb -O bornhack bornhackdb"
# run migrate with --fake to get list of migrations
MIGRATIONS=$(python manage.py migrate --fake | grep FAKED | cut -d " " -f 4 | cut -d "." -f 1-2)
# wipe database again
sudo su postgres -c "dropdb bornhackdb; createdb -O bornhack bornhackdb"
# create output folder
sudo rm -rf postgres_autodoc
mkdir postgres_autodoc
sudo chown postgres:postgres postgres_autodoc
# loop over migrations
COUNTER=0
for MIGRATION in $MIGRATIONS; do
COUNTER=$(( $COUNTER + 1 ))
ALFACOUNTER=$(printf "%04d" $COUNTER)
echo "processing migration #${COUNTER}: $MIG"
APP=$(echo $MIGRATION | cut -d "." -f 1)
MIG=$(echo $MIGRATION | cut -d "." -f 2)
echo "--- running migration: APP: $APP MIGRATION: $MIG ..."
python manage.py migrate --no-input $APP $MIG
echo "--- running postgresql_autodoc and dot..."
cd postgres_autodoc
sudo su postgres -c "mkdir ${ALFACOUNTER}-$MIGRATION"
cd "${ALFACOUNTER}-${MIGRATION}"
# run postgresql_autodoc
sudo su postgres -c "postgresql_autodoc -d bornhackdb"
# create PNG from .dot file
sudo su postgres -c "dot -Tpng bornhackdb.dot -o bornhackdb.png"
# create watermark image with migration name as white on black text
sudo su postgres -c "convert -background none -undercolor black -fill white -font DejaVu-Sans-Mono-Bold -size 5316x4260 -pointsize 72 -gravity SouthEast label:${ALFACOUNTER}-${MIGRATION} background.png"
# combine the images
sudo su postgres -c "composite -gravity center bornhackdb.png background.png final.png"
cd ..
cd ..
done

View File

@ -36,6 +36,9 @@ CAMP_REDIRECT_PERCENT=25
### changes below here are only needed for production
# email settings
{% if not django_email_realworld | default(False) %}
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
{% endif %}
EMAIL_HOST='{{ django_email_host }}'
EMAIL_PORT={{ django_email_port }}
EMAIL_HOST_USER='{{ django_email_user }}'
@ -76,13 +79,13 @@ SCHEDULE_EVENT_NOTIFICATION_MINUTES=10
# irc bot settings
IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS=10
IRCBOT_NICK='{{ django_ircbot_nickname }}'
IRCBOT_CHANSERV_MASK='{{ django_ircbot_chanserv_mask }}'
IRCBOT_NICKSERV_MASK='{{ django_ircbot_nickserv_mask }}'
IRCBOT_NICKSERV_PASSWORD='{{ django_ircbot_nickserv_password }}'
IRCBOT_NICKSERV_EMAIL='{{ django_ircbot_nickserv_email }}'
IRCBOT_NICKSERV_IDENTIFY_STRING="This nickname is registered. Please choose a different nickname, or identify via \x02/msg NickServ identify <password>\x02."
IRCBOT_SERVER_HOSTNAME='{{ django_ircbot_server }}'
IRCBOT_SERVER_PORT=6697
IRCBOT_SERVER_USETLS=True
IRCBOT_CHANNELS={
'default': '{{ django_ircbot_default_channel }}',
'orga': '{{ django_ircbot_orga_channel }}',
'public': '{{ django_ircbot_public_channel }}'
}
IRCBOT_PUBLIC_CHANNEL='{{ django_ircbot_public_channel }}'

View File

@ -45,6 +45,7 @@ INSTALLED_APPS = [
'tickets',
'bar',
'backoffice',
'events',
'allauth',
'allauth.account',

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-18 11:44
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0022_camp_colour'),
]
operations = [
migrations.AddField(
model_name='camp',
name='shortslug',
field=models.SlugField(blank=True, help_text='Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.', verbose_name='Short Slug'),
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-18 11:45
from __future__ import unicode_literals
from django.db import migrations
def populate_camp_shortslugs(apps, schema_editor):
Camp = apps.get_model('camps', 'Camp')
for camp in Camp.objects.all():
if not camp.shortslug:
camp.shortslug = camp.slug
camp.save()
class Migration(migrations.Migration):
dependencies = [
('camps', '0023_camp_shortslug'),
]
operations = [
migrations.RunPython(populate_camp_shortslugs),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-18 11:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('camps', '0024_populate_camp_shortslugs'),
]
operations = [
migrations.AlterField(
model_name='camp',
name='shortslug',
field=models.SlugField(help_text='Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.', verbose_name='Short Slug'),
),
]

View File

@ -34,6 +34,11 @@ class Camp(CreatedUpdatedModel, UUIDModel):
help_text='The url slug to use for this camp'
)
shortslug = models.SlugField(
verbose_name='Short Slug',
help_text='Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.',
)
buildup = DateTimeRangeField(
verbose_name='Buildup Period',
help_text='The camp buildup period.',
@ -189,9 +194,3 @@ class Camp(CreatedUpdatedModel, UUIDModel):
else:
return True
@property
def teams(self):
""" Return a queryset with all teams under all TeamAreas under this Camp """
from teams.models import Team
return Team.objects.filter(area__in=self.teamareas.all())

0
src/events/__init__.py Normal file
View File

12
src/events/admin.py Normal file
View File

@ -0,0 +1,12 @@
from django.contrib import admin
from .models import Type, Routing
@admin.register(Type)
class TypeAdmin(admin.ModelAdmin):
pass
@admin.register(Routing)
class RoutingAdmin(admin.ModelAdmin):
pass

5
src/events/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class EventsConfig(AppConfig):
name = 'events'

81
src/events/handler.py Normal file
View File

@ -0,0 +1,81 @@
from django.utils import timezone
from datetime import timedelta
from ircbot.utils import add_irc_message
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
def handle_team_event(eventtype, irc_message=None, irc_timeout=60, email_template=None, email_formatdict=None):
"""
This method is our basic event handler.
The type of event determines which teams receive notifications.
TODO: Add some sort of priority to messages
"""
logger.info("Inside handle_team_event, eventtype %s" % eventtype)
# get event type from database
from .models import Type
try:
eventtype = Type.objects.get(name=eventtype)
except Type.DoesNotExist:
# unknown event type, do nothing
logger.error("Unknown eventtype %s" % eventtype)
return
if not eventtype.teams:
# no routes found for this eventtype, do nothing
logger.error("No routes round for eventtype %s" % eventtype)
return
# loop over routes (teams) for this eventtype
for team in eventtype.teams:
logger.info("Handling eventtype %s for team %s" % (eventtype, team))
team_irc_notification(team=team, eventtype=eventtype, irc_message=irc_message, irc_timeout=irc_timeout)
team_email_notification(team=team, eventtype=eventtype, email_template=None, email_formatdict=None)
# handle any future notification types here..
def team_irc_notification(team, eventtype, irc_message=None, irc_timeout=60):
"""
Sends IRC notifications for events to team IRC channels
"""
logger.info("Inside team_irc_notification, message %s" % irc_message)
if not irc_message:
logger.error("No IRC message found")
return
if not eventtype.irc_notification:
logger.error("IRC notifications not enabled for eventtype %s" % eventtype)
return
if not team.irc_channel or not team.irc_channel_name:
logger.error("team %s is not IRC enabled" % team)
return
# send an IRC message to the the channel for this team
add_irc_message(
target=team.irc_channel_name,
message=irc_message,
timeout=60
)
logger.info("Added new IRC message for channel %s" % team.irc_channel_name)
def team_email_notification(team, eventtype, email_template=None, email_formatdict=None):
"""
Sends email notifications for events to team mailinglists (if possible,
otherwise directly to the team responsibles)
"""
if not email_template or not email_formatdict or not eventtype.email_notification:
# no email message found, or email notifications are not enabled for this event type
return
if team.mailing_list:
# send notification to the team mailing list
recipient_list = [team.mailing_list]
else:
# no team mailinglist, send to the team responsibles instead
recipient_list = [resp.email for resp in team.responsible_members.all()]
# TODO: actually send the email here

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-18 13:16
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('teams', '0025_auto_20180318_1318'),
]
operations = [
migrations.CreateModel(
name='Routing',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Type',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.TextField(help_text='The type of event', unique=True)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='routing',
name='eventtype',
field=models.ForeignKey(help_text='The type of event to route', on_delete=django.db.models.deletion.PROTECT, related_name='eventroutes', to='events.Type'),
),
migrations.AddField(
model_name='routing',
name='team',
field=models.ForeignKey(help_text='The team which should receive events of this type.', on_delete=django.db.models.deletion.PROTECT, related_name='eventroutes', to='teams.Team'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-18 13:18
from __future__ import unicode_literals
from django.db import migrations
def create_eventtypes(apps, schema_editor):
Type = apps.get_model('events', 'Type')
Type.objects.create(name='public_credit_name_changed')
class Migration(migrations.Migration):
dependencies = [
('events', '0001_initial'),
]
operations = [
migrations.RunPython(create_eventtypes),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-25 14:16
from __future__ import unicode_literals
from django.db import migrations
def create_eventtype(apps, schema_editor):
Type = apps.get_model('events', 'Type')
Type.objects.create(name='ticket_created')
class Migration(migrations.Migration):
dependencies = [
('events', '0002_create_eventtype'),
]
operations = [
migrations.RunPython(create_eventtype),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-03 10:28
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0003_create_another_eventtype'),
]
operations = [
migrations.AddField(
model_name='type',
name='email_notification',
field=models.BooleanField(default=False, help_text='Check to send email notifications for this type of event.'),
),
migrations.AddField(
model_name='type',
name='irc_notification',
field=models.BooleanField(default=False, help_text='Check to send IRC notifications for this type of event.'),
),
]

View File

61
src/events/models.py Normal file
View File

@ -0,0 +1,61 @@
from django.db import models
from utils.models import CreatedUpdatedModel
from teams.models import Team
class Type(CreatedUpdatedModel):
"""
The events.Type model contains different types of system events which can happen.
New event types should be added in data migrations.
The following types are currently used in the codebase:
- ticket_created: Whenever a new ShopTicket is created
- public_credit_name_changed: Whenever a user changes public_credit_name in the profile
"""
name = models.TextField(
unique=True,
help_text='The type of event'
)
irc_notification = models.BooleanField(
default=False,
help_text='Check to send IRC notifications for this type of event.',
)
email_notification = models.BooleanField(
default=False,
help_text='Check to send email notifications for this type of event.',
)
def __str__(self):
return self.name
@property
def teams(self):
"""
This property returns a queryset with all the teams that should receive this type of events
"""
team_ids = Routing.objects.filter(eventtype=self).values_list('team', flat=True)
return Team.objects.filter(pk__in=team_ids)
class Routing(CreatedUpdatedModel):
"""
The events.Routing model contains routings for system events.
Add a new entry to route events of a certain type to a team.
Several teams can receive the same type of event.
"""
eventtype = models.ForeignKey(
'events.Type',
related_name='eventroutes',
on_delete=models.PROTECT,
help_text='The type of event to route',
)
team = models.ForeignKey(
'teams.Team',
related_name='eventroutes',
on_delete=models.PROTECT,
help_text='The team which should receive events of this type.'
)
def __str__(self):
return "%s -> %s" % (self.eventtype, self.team)

3
src/events/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
src/events/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -1,7 +1,9 @@
import irc3
import irc3, re
from ircbot.models import OutgoingIrcMessage
from teams.models import Team, TeamMember
from django.conf import settings
from django.utils import timezone
from events.models import Routing
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
@ -26,6 +28,12 @@ class Plugin(object):
"""triggered after the server sent the MOTD (require core plugin)"""
logger.debug("inside server_ready(), kwargs: %s" % kwargs)
logger.info("Identifying with %s" % settings.IRCBOT_NICKSERV_MASK)
self.bot.privmsg(settings.IRCBOT_NICKSERV_MASK, "identify %s %s" % (settings.IRCBOT_NICK, settings.IRCBOT_NICKSERV_PASSWORD))
logger.info("Calling self.bot.do_stuff() in %s seconds.." % settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS)
self.bot.loop.call_later(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS, self.bot.do_stuff)
def connection_lost(self, **kwargs):
"""triggered when connection is lost"""
@ -36,9 +44,6 @@ class Plugin(object):
"""triggered when connection is up"""
logger.debug("inside connection_made(), kwargs: %s" % kwargs)
# wait 5 secs before starting the loop to check for outgoing messages
self.bot.loop.call_later(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS, self.bot.get_outgoing_messages)
###############################################################################################
### decorated irc3 event methods
@ -48,16 +53,38 @@ class Plugin(object):
"""triggered when there is a join part or quit on a channel the bot is in"""
logger.debug("inside on_join_part_quit(), kwargs: %s" % kwargs)
# TODO: on part or quit check if the bot is the only remaining member of a channel,
# if so, check if the channel should be managed, and if so, part and join the channel
# to gain @ and register with ChanServ
@irc3.event(irc3.rfc.JOIN)
def on_join(self, mask, channel, **kwargs):
"""Triggered when a channel is joined by someone, including the bot itself"""
if mask.nick == self.bot.nick:
# the bot just joined a channel
if channel in self.get_managed_team_channels():
logger.debug("Just joined a channel I am supposed to be managing, asking ChanServ for info about %s" % channel)
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "info %s" % channel)
return
@irc3.event(irc3.rfc.PRIVMSG)
def on_privmsg(self, **kwargs):
"""triggered when a privmsg is sent to the bot or to a channel the bot is in"""
logger.debug("inside on_privmsg(), kwargs: %s" % kwargs)
# nickserv
if kwargs['mask'] == "NickServ!NickServ@services.baconsvin.org" and kwargs['event'] == "NOTICE" and kwargs['data'] == "This nickname is registered. Please choose a different nickname, or identify via \x02/msg NickServ identify <password>\x02.":
logger.info("Nickserv identify needed, fixing...")
self.bot.privmsg("NickServ@services.baconsvin.org", "identify %s %s" % (settings.IRCBOT_NICK, settings.IRCBOT_NICKSERV_PASSWORD))
# we only handle NOTICEs for now
if kwargs['event'] != "NOTICE":
return
# check if this is a message from nickserv
if kwargs['mask'] == "NickServ!%s" % settings.IRCBOT_NICKSERV_MASK:
self.bot.handle_nickserv_privmsg(**kwargs)
# check if this is a message from chanserv
if kwargs['mask'] == "ChanServ!%s" % settings.IRCBOT_CHANSERV_MASK:
self.bot.handle_chanserv_privmsg(**kwargs)
@irc3.event(irc3.rfc.KICK)
@ -68,13 +95,28 @@ class Plugin(object):
###############################################################################################
### custom irc3 methods
@irc3.extend
def do_stuff(self):
"""
Main periodic method called every N seconds.
"""
#logger.debug("inside do_stuff()")
# call the methods we need to
self.bot.check_irc_channels()
self.bot.fix_missing_acls()
self.bot.get_outgoing_messages()
# schedule a call of this function again in N seconds
self.bot.loop.call_later(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS, self.bot.do_stuff)
@irc3.extend
def get_outgoing_messages(self):
"""
This method gets unprocessed OutgoingIrcMessage objects and attempts to send them to
the target channel. Messages are skipped if the bot is not in the channel.
"""
#logger.debug("inside get_outgoing_messages()")
for msg in OutgoingIrcMessage.objects.filter(processed=False).order_by('created'):
logger.info("processing irc message to %s: %s" % (msg.target, msg.message))
# if this message expired mark it as expired and processed without doing anything
@ -99,7 +141,198 @@ class Plugin(object):
else:
logger.warning("skipping message to %s" % msg.target)
# call this function again in X seconds
self.bot.loop.call_later(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS, self.bot.get_outgoing_messages)
###############################################################################################
### irc channel methods
@irc3.extend
def check_irc_channels(self):
"""
Compare the list of IRC channels the bot is currently in with the list of IRC channels the bot is supposed to be in.
Join or part channels as needed.
"""
desired_channel_list = list(set(list(self.get_managed_team_channels()) + list(self.get_unmanaged_team_channels()) + [settings.IRCBOT_PUBLIC_CHANNEL]))
#logger.debug("Inside check_irc_channels(), desired_channel_list is: %s and self.bot.channels is: %s" % (desired_channel_list, self.bot.channels.keys()))
# loop over desired_channel_list, join as needed
for channel in desired_channel_list:
if channel not in self.bot.channels:
logger.debug("I should be in %s but I am not, attempting to join..." % channel)
self.bot.join(channel)
# loop over self.bot.channels, part as needed
for channel in self.bot.channels:
if channel not in desired_channel_list:
logger.debug("I am in %s but I shouldn't be, parting..." % channel)
self.bot.part(channel, "I am no longer needed here")
@irc3.extend
def get_managed_team_channels(self):
"""
Return a unique list of team IRC channels which the bot is supposed to be managing.
"""
return Team.objects.filter(
irc_channel=True,
irc_channel_managed=True
).values_list("irc_channel_name", flat=True)
@irc3.extend
def get_unmanaged_team_channels(self):
"""
Return a unique list of team IRC channels which the bot is not supposed to be managing.
"""
return Team.objects.filter(
irc_channel=True,
irc_channel_managed=False
).values_list("irc_channel_name", flat=True)
@irc3.extend
def setup_private_channel(self, team):
"""
Configures a private team IRC channel by setting modes and adding all members to ACL
"""
# basic private channel modes
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s mlock +inpst" % team.irc_channel_name)
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE on" % team.irc_channel_name)
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED on" % team.irc_channel_name)
# add the bot to the ACL
self.bot.add_user_to_team_channel_acl(
username=settings.IRCBOT_NICK,
channel=team.irc_channel_name
)
# add all members to the acl
for membership in team.memberships.all():
if membership.approved and membership.user.profile.nickserv_username:
self.bot.add_user_to_team_channel_acl(
username=membership.user.profile.nickserv_username,
channel=membership.team.irc_channel_name,
)
# mark membership as irc_channel_acl_ok=True and save
membership.irc_channel_acl_ok=True
membership.save()
@irc3.extend
def add_user_to_team_channel_acl(self, username, channel):
"""
Add user to team IRC channel ACL
"""
# set autoop for this username
self.bot.privmsg(
settings.IRCBOT_CHANSERV_MASK,
"flags %(channel)s %(user)s +oO" % {
'channel': channel,
'user': username,
},
)
# also add autoinvite for this username
self.bot.mode(channel, '+I', '$a:%s' % username)
@irc3.extend
def fix_missing_acls(self):
"""
Called periodically by do_stuff()
Loops over TeamMember objects and adds and removes ACL entries as needed
"""
missing_acls = TeamMember.objects.filter(
team__irc_channel=True,
team__irc_channel_managed=True,
team__irc_channel_private=True,
irc_channel_acl_ok=False
).exclude(
user__profile__nickserv_username=''
)
if not missing_acls:
return
logger.debug("Found %s memberships which need IRC ACL fixing.." % missing_acls.count())
for membership in missing_acls:
self.bot.add_user_to_team_channel_acl(
username=membership.user.profile.nickserv_username,
channel=membership.team.irc_channel_name,
)
# mark membership as irc_channel_acl_ok=True and save
membership.irc_channel_acl_ok=True
membership.save()
###############################################################################################
### services (ChanServ & NickServ) methods
@irc3.extend
def handle_chanserv_privmsg(self, **kwargs):
"""
Handle messages from ChanServ on networks with Services.
"""
logger.debug("Got a message from ChanServ")
###############################################
# handle "Channel \x02#example\x02 is not registered." message
###############################################
match = re.compile("Channel (#[a-zA-Z0-9-]+) is not registered.").match(kwargs['data'].replace("\x02", ""))
if match:
# the irc channel is not registered
channel = match.group(1)
# get a list of the channels we are supposed to be managing
if channel in self.bot.get_managed_team_channels():
# we want to register this channel! but we can only do so if we have a @ in the channel
if self.bot.nick in self.bot.channels[channel].modes['@']:
logger.debug("ChanServ says channel %s is not registered, bot is supposed to be managing this channel, registering it with chanserv" % channel)
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "register %s" % channel)
else:
logger.debug("ChanServ says channel %s is not registered, bot is supposed to be managing this channel, but the bot cannot register without @ in the channel" % channel)
self.bot.privmsg(channel, "I need @ before I can register this channel with ChanServ")
return
###############################################
# handle "\x02#example\x02 is now registered to \x02tykbhdev\x02" message
###############################################
match = re.compile("(#[a-zA-Z0-9-]+) is now registered to ([a-zA-Z0-9-]+)\\.").match(kwargs['data'].replace("\x02", ""))
if match:
# the irc channel is now registered
channel = match.group(1)
botnick = match.group(2)
logger.debug("Channel %s was registered with ChanServ, looking up Team..." % channel)
# if this channel is a private team IRC channel set modes and add initial ACL
try:
team = Team.objects.get(irc_channel_name=channel)
except Team.DoesNotExist:
logger.debug("Unable to find Team matching IRC channel %s" % channel)
return
if not team.irc_channel_private:
# this channel is not private, no mode change and ACL needed
return
# set channel modes and ACL
self.bot.setup_private_channel(team)
return
logger.debug("Unhandled ChanServ message: %s" % kwargs['data'])
@irc3.extend
def handle_nickserv_privmsg(self, **kwargs):
"""th
Handles messages from NickServ on networks with Services.
"""
logger.debug("Got a message from NickServ")
# handle "\x02botnick\x02 is not a registered nickname." message
if kwargs['data'] == '\x02%s\x02 is not a registered nickname.' % self.bot.nick:
# the bots nickname is not registered, register new account with nickserv
self.bot.privmsg(settings.IRCBOT_NICKSERV_MASK, "register %s %s" % (settings.IRCBOT_NICKSERV_PASSWORD, settings.IRCBOT_NICKSERV_EMAIL))
return
logger.debug("Unhandled NickServ message: %s" % kwargs['data'])

View File

@ -1,6 +1,7 @@
from django.conf import settings
import logging
import irc3
from events.models import Routing
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('bornhack.%s' % __name__)
@ -9,9 +10,13 @@ def do_work():
"""
Run irc3 module code, wait for events on IRC and wait for messages in OutgoingIrcMessage
"""
if hasattr(settings, 'IRCBOT_CHANNELS'):
logger.error("settings.IRCBOT_CHANNELS is deprecated. Please define settings.IRCBOT_PUBLIC_CHANNEL and use team channels for the rest.")
return False
config = {
'nick': settings.IRCBOT_NICK,
'autojoins': list(set(settings.IRCBOT_CHANNELS.values())),
'autojoins': [],
'host': settings.IRCBOT_SERVER_HOSTNAME,
'port': settings.IRCBOT_SERVER_PORT,
'ssl': settings.IRCBOT_SERVER_USETLS,

21
src/ircbot/utils.py Normal file
View File

@ -0,0 +1,21 @@
from django.conf import settings
from django.utils import timezone
from datetime import timedelta
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
def add_irc_message(target, message, timeout=10):
"""
Convenience function for adding OutgoingIrcMessage objects.
Defaults to a message timeout of 10 minutes
"""
from .models import OutgoingIrcMessage
OutgoingIrcMessage.objects.create(
target=target,
message=message,
timeout=timezone.now()+timedelta(minutes=timeout),
)

View File

@ -21,6 +21,7 @@ People | {{ block.super }}
<thead>
<tr>
<th>Team Name</th>
<th>Team Responsible</th>
<th>Team Members</th>
</tr>
</thead>
@ -31,19 +32,20 @@ People | {{ block.super }}
{{ team.name }} Team
</td>
<td>
{% if team.anoncount == 0 and team.approvedmembers.count == 0 %}
<b>No team member(s)
{% elif team.approvedmembers.count == team.anoncount %}
<b>{{ team.anoncount }}</b> anonymous member(s)
{% endif %}
{% for member in team.approvedmembers.all %}
{% if member.user.profile.approved_public_credit_name %}
{{ member.user.profile.approved_public_credit_name }}{% if member in team.responsible.all %} (responsible){% endif %}<br>
{% endif %}
{% for resp in team.responsible_members.all %}
{{ resp.profile.get_public_credit_name }}<br>
{% endfor %}
{% if team.anoncount and team.anoncount != team.approvedmembers.count %}
plus <b>{{ team.anoncount }}</b> anonymous member(s).
</td>
<td>
{% for member in team.regular_members.all %}
{% if member.profile.get_public_credit_name != "Unnamed" %}
{{ member.profile.get_public_credit_name }}<br>
{% endif %}
{% empty %}
No team members
{% endfor %}
{% if team.unnamed_members %}
{% if team.unnamed_members.count < team.regular_members.count %}Plus {% endif %}<b>{{ team.unnamed_members.count }}</b> anonymous member(s).
{% endif %}
</td>
</tr>

View File

@ -0,0 +1,2 @@
default_app_config = 'profiles.apps.ProfilesConfig'

View File

@ -14,6 +14,10 @@ class OrderAdmin(admin.ModelAdmin):
'public_credit_name_approved',
]
list_filter = [
'public_credit_name_approved',
]
def approve_public_credit_names(self, request, queryset):
for profile in queryset.filter(public_credit_name_approved=False):
profile.approve_public_credit_name()

16
src/profiles/apps.py Normal file
View File

@ -0,0 +1,16 @@
from django.apps import AppConfig
from django.db.models.signals import pre_save, post_save
from .signal_handlers import create_profile, profile_pre_save
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
class ProfilesConfig(AppConfig):
name = 'profiles'
def ready(self):
# remember to include a dispatch_uid to prevent signals being called multiple times in certain corner cases
from django.contrib.auth.models import User
post_save.connect(create_profile, sender=User, dispatch_uid='user_post_save_signal')
pre_save.connect(profile_pre_save, sender='profiles.Profile', dispatch_uid='profile_pre_save_signal')

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-25 18:22
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('profiles', '0007_auto_20170711_2025'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='public_credit_name',
field=models.CharField(blank=True, help_text='The name you want to appear on in the credits section of the public website (on Team and People pages). Leave this empty if you want your name hidden on the public webpages.', max_length=100),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-03 00:01
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('profiles', '0008_auto_20180325_2022'),
]
operations = [
migrations.AddField(
model_name='profile',
name='nickserv_username',
field=models.CharField(blank=True, help_text='Your NickServ username is used to manage team IRC channel access lists.', max_length=50),
),
]

View File

@ -1,9 +1,5 @@
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import (
post_save,
pre_save
)
from django.conf import settings
from django.utils import timezone
from django.dispatch import receiver
@ -11,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _
from datetime import timedelta
from ircbot.models import OutgoingIrcMessage
from utils.models import UUIDModel, CreatedUpdatedModel
@ -42,7 +37,7 @@ class Profile(CreatedUpdatedModel, UUIDModel):
public_credit_name = models.CharField(
blank=True,
max_length=100,
help_text='The name you want to appear on in the credits section of the public website (the People pages). Leave empty if you want no public credit.'
help_text='The name you want to appear on in the credits section of the public website (on Team and People pages). Leave this empty if you want your name hidden on the public webpages.'
)
public_credit_name_approved = models.BooleanField(
@ -50,6 +45,12 @@ class Profile(CreatedUpdatedModel, UUIDModel):
help_text='Check this box to approve this users public_credit_name. This will be unchecked automatically when the user edits public_credit_name'
)
nickserv_username = models.CharField(
blank=True,
max_length=50,
help_text='Your NickServ username is used to manage team IRC channel access lists.',
)
@property
def email(self):
return self.user.email
@ -58,37 +59,21 @@ class Profile(CreatedUpdatedModel, UUIDModel):
return self.user.username
def approve_public_credit_name(self):
"""
This method just sets profile.public_credit_name_approved=True and calls save()
It is used in an admin action
"""
self.public_credit_name_approved = True
self.save()
@property
def approved_public_credit_name(self):
def get_public_credit_name(self):
"""
Convenience method to return profile.public_credit_name if it is approved,
and the string "Unnamed" otherwise
"""
if self.public_credit_name_approved:
return self.public_credit_name
else:
return False
return "Unnamed"
@receiver(post_save, sender=User)
def create_profile(sender, created, instance, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(pre_save, sender=Profile)
def changed_public_credit_name(sender, instance, **kwargs):
try:
original = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist:
# newly created object, just pass
pass
else:
if not original.public_credit_name == instance.public_credit_name:
OutgoingIrcMessage.objects.create(
target=settings.IRCBOT_CHANNELS['orga'] if 'orga' in settings.IRCBOT_CHANNELS else settings.IRCBOT_CHANNELS['default'],
message='User {username} changed public credit name. please review and act accordingly: https://bornhack.dk/admin/profiles/profile/{uuid}/change/'.format(
username=instance.name,
uuid=instance.uuid
),
timeout=timezone.now()+timedelta(minutes=60)
)

View File

@ -0,0 +1,81 @@
from django.db.models.signals import (
post_save,
pre_save
)
from events.handler import handle_team_event
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
def create_profile(sender, created, instance, **kwargs):
"""
Signal handler called after a User object is saved.
Creates a Profile object when the User object was just created.
"""
from .models import Profile
if created:
Profile.objects.create(user=instance)
def profile_pre_save(sender, instance, **kwargs):
"""
Signal handler called before a Profile object is saved.
"""
try:
original = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist:
original = None
logger.debug("inside profile_pre_save with instance.nickserv_username=%s and original.nickserv_username=%s" % (instance.nickserv_username, original.nickserv_username))
public_credit_name_changed(instance, original)
nickserv_username_changed(instance, original)
def public_credit_name_changed(instance, original):
"""
Checks if a users public_credit_name has been changed, and triggers a public_credit_name_changed event if so
"""
if original.public_credit_name == instance.public_credit_name:
# public_credit_name has not been changed
return
if original.public_credit_name and not original.public_credit_name_approved:
# the original.public_credit_name was not approved, no need to notify again
return
# put the message together
message='User {username} changed public credit name. please review and act accordingly: https://bornhack.dk/admin/profiles/profile/{uuid}/change/'.format(
username=instance.name,
uuid=instance.uuid
)
# trigger the event
handle_team_event(
eventtype='public_credit_name_changed',
irc_message=message,
)
def nickserv_username_changed(instance, original):
"""
Check if profile.nickserv_username was changed, and uncheck irc_channel_acl_ok if so
This will be picked up by the IRC bot and fixed as needed
"""
if instance.nickserv_username and instance.nickserv_username != original.nickserv_username:
logger.debug("profile.nickserv_username changed for user %s, setting irc_channel_acl_ok=False" % instance.user.username)
# find team memberships for this user
from teams.models import TeamMember
memberships = TeamMember.objects.filter(
user=instance.user,
approved=True,
team__irc_channel=True,
team__irc_channel_managed=True,
team__irc_channel_private=True,
)
# loop over memberships
for membership in memberships:
membership.irc_channel_acl_ok = False
membership.save()

View File

@ -14,9 +14,13 @@
<td>{{ profile.description|default:"N/A" }}</td>
</tr>
<tr>
<td><b>Public Credit Name (visible to the public, leave empty if you want no credits)</b></td>
<td><b>Public Credit Name (visible to the public, leave empty if you want no credits on this website)</b></td>
<td>{{ profile.public_credit_name|default:"N/A" }} {% if profile.public_credit_name %}({% if profile.public_credit_name_approved %}<span class="text-success">approved</span>{% else %}<span class="text-danger">pending approval</span>{% endif %}){% endif %}</td>
</tr>
<tr>
<td><b>NickServ username (visible to the public on IRC, used to handle team channel ACLs)</b></td>
<td>{{ profile.nickserv_username|default:"N/A" }}</td>
</tr>
</table>
<a href="{% url 'profiles:update' %}" class="btn btn-black"><i class="fa fa-edit"></i> Edit Profile</a>
{% endblock profile_content %}

View File

@ -16,7 +16,7 @@ class ProfileDetail(LoginRequiredMixin, DetailView):
class ProfileUpdate(LoginRequiredMixin, UpdateView):
model = models.Profile
fields = ['name', 'description', 'public_credit_name']
fields = ['name', 'description', 'public_credit_name', 'nickserv_username']
success_url = reverse_lazy('profiles:detail')
template_name = 'profile_form.html'
@ -28,6 +28,6 @@ class ProfileUpdate(LoginRequiredMixin, UpdateView):
# user changed the name (to something non blank)
form.instance.public_credit_name_approved = False
form.instance.save()
messages.info(self.request, 'Your profile has been updated.')
messages.success(self.request, 'Your profile has been updated.')
return super().form_valid(form, **kwargs)

View File

@ -14,12 +14,10 @@ class ProgramConfig(AppConfig):
from .signal_handlers import (
check_speaker_event_camp_consistency,
check_speaker_camp_change,
notify_proposal_submitted
)
m2m_changed.connect(
check_speaker_event_camp_consistency,
sender=Speaker.events.through
)
pre_save.connect(check_speaker_camp_change, sender=Speaker)
pre_save.connect(notify_proposal_submitted, sender=SpeakerProposal)
pre_save.connect(notify_proposal_submitted, sender=EventProposal)

View File

@ -8,7 +8,6 @@ from django.conf import settings
from .email import add_new_speakerproposal_email, add_new_eventproposal_email
from .models import EventProposal, SpeakerProposal
from ircbot.models import OutgoingIrcMessage
logger = logging.getLogger("bornhack.%s" % __name__)
@ -37,42 +36,3 @@ def check_speaker_camp_change(sender, instance, **kwargs):
if event.camp != instance.camp:
raise ValidationError({'camp': 'You cannot change the camp a speaker belongs to if the speaker is associated with one or more events.'})
# pre_save signal that notifies if a proposal changes status from draft to
# pending i.e. is submitted.
def notify_proposal_submitted(sender, instance, **kwargs):
try:
original = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist:
return False
target = settings.IRCBOT_CHANNELS['orga'] if 'orga' in settings.IRCBOT_CHANNELS else settings.IRCBOT_CHANNELS['default']
if original.proposal_status == 'draft' and instance.proposal_status == 'pending':
if isinstance(instance, EventProposal):
if not add_new_eventproposal_email(instance):
logger.error(
'Error adding event proposal email to outgoing queue for {}'.format(instance)
)
OutgoingIrcMessage.objects.create(
target=target,
message="New event proposal: {} - https://bornhack.dk/admin/program/eventproposal/{}/change/".format(
instance.title,
instance.uuid
),
timeout=timezone.now()+timedelta(minutes=10)
)
if isinstance(instance, SpeakerProposal):
if not add_new_speakerproposal_email(instance):
logger.error(
'Error adding speaker proposal email to outgoing queue for {}'.format(instance)
)
OutgoingIrcMessage.objects.create(
target=target,
message="New speaker proposal: {} - https://bornhack.dk/admin/program/speakerproposal/{}/change/".format(
instance.name,
instance.uuid
),
timeout=timezone.now()+timedelta(minutes=10)
)

View File

@ -1,5 +1,5 @@
from django.contrib import admin
from .models import Team, TeamArea, TeamMember, TeamTask
from .models import Team, TeamMember, TeamTask
from .email import add_added_membership_email, add_removed_membership_email
from camps.utils import CampPropertyListFilter
@ -17,12 +17,12 @@ class TeamTaskAdmin(admin.ModelAdmin):
@admin.register(Team)
class TeamAdmin(admin.ModelAdmin):
def get_responsible(self, obj):
return ", ".join([resp.get_full_name() for resp in obj.responsible])
return ", ".join([resp.profile.public_credit_name for resp in obj.responsible_members.all()])
get_responsible.short_description = 'Responsible'
list_display = [
'name',
'area',
'camp',
'get_responsible',
'needs_members',
]
@ -78,9 +78,3 @@ class TeamMemberAdmin(admin.ModelAdmin):
)
remove_member.description = 'Remove a user from the team.'
@admin.register(TeamArea)
class TeamAreaAdmin(admin.ModelAdmin):
list_filter = [
'camp'
]

View File

@ -1,5 +1,13 @@
from django.apps import AppConfig
from django.db.models.signals import post_save, post_delete
from .signal_handlers import teammember_saved, teammember_deleted
class TeamsConfig(AppConfig):
name = 'teams'
def ready(self):
# connect the post_save signal, always including a dispatch_uid to prevent it being called multiple times in corner cases
post_save.connect(teammember_saved, sender='teams.TeamMember', dispatch_uid='teammember_save_signal')
post_delete.connect(teammember_deleted, sender='teams.TeamMember', dispatch_uid='teammember_save_signal')

View File

@ -49,10 +49,11 @@ def add_new_membership_email(membership):
return add_outgoing_email(
text_template='emails/new_membership_email.txt',
html_template='emails/new_membership_email.html',
to_recipients=[resp.email for resp in membership.team.responsible],
to_recipients=[resp.email for resp in membership.team.responsible_members.all()],
formatdict=formatdict,
subject='New membership request for {} at {}'.format(
membership.team.name,
membership.team.camp.title
)
)

View File

@ -1,8 +0,0 @@
from django.forms import ModelForm
from .models import Team
class ManageTeamForm(ModelForm):
class Meta:
model = Team
fields = ['description', 'needs_members']

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-18 10:35
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teams', '0021_auto_20180318_0906'),
]
operations = [
migrations.AddField(
model_name='team',
name='irc_channel',
field=models.BooleanField(default=False, help_text='Check to make the IRC bot join the team IRC channel. Leave unchecked to disable IRC bot functionality for this team entirely.'),
),
migrations.AddField(
model_name='team',
name='irc_channel_managed',
field=models.BooleanField(default=True, help_text='Check to make the bot manage the team IRC channel. The bot will register the channel with ChanServ if possible, and manage ACLs as needed.'),
),
migrations.AddField(
model_name='team',
name='irc_channel_name',
field=models.TextField(blank=True, default='', help_text='Team IRC channel. Leave blank to generate channel name automatically, based on camp slug and team slug.'),
),
migrations.AddField(
model_name='team',
name='irc_channel_private',
field=models.BooleanField(default=True, help_text='Check to make the IRC channel private for team members only, also sets +s. Leave unchecked to make the IRC channel public and open for everyone.'),
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-18 11:56
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teams', '0022_auto_20180318_1135'),
]
operations = [
migrations.AddField(
model_name='team',
name='shortslug',
field=models.SlugField(blank=True, help_text='Abbreviated version of the slug. Used in places like IRC channel names where space is limited'),
),
migrations.AlterField(
model_name='team',
name='name',
field=models.CharField(help_text='The team name', max_length=255),
),
migrations.AlterField(
model_name='team',
name='slug',
field=models.SlugField(blank=True, help_text='Url slug for this team. Leave blank to generate based on team name', max_length=255),
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-18 11:45
from __future__ import unicode_literals
from django.db import migrations
def populate_team_shortslugs(apps, schema_editor):
Team = apps.get_model('teams', 'Team')
for team in Team.objects.all():
if not team.shortslug:
team.shortslug = team.slug
team.save()
class Migration(migrations.Migration):
dependencies = [
('teams', '0023_auto_20180318_1256'),
]
operations = [
migrations.RunPython(populate_team_shortslugs),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-18 12:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teams', '0024_populate_shortslugs'),
]
operations = [
migrations.AlterField(
model_name='team',
name='shortslug',
field=models.SlugField(help_text='Abbreviated version of the slug. Used in places like IRC channel names where space is limited'),
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-25 13:45
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('camps', '0025_auto_20180318_1250'),
('teams', '0025_auto_20180318_1318'),
]
operations = [
migrations.AddField(
model_name='team',
name='camp',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='teams', to='camps.Camp'),
),
]

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-25 13:45
from __future__ import unicode_literals
from django.db import migrations
def add_team_camp(apps, schema_editor):
Team = apps.get_model('teams', 'Team')
TeamArea = apps.get_model('teams', 'TeamArea')
TeamMember = apps.get_model('teams', 'TeamMember')
for team in Team.objects.all():
print("camp processing team %s..." % team.name)
team.camp = team.area.camp
team.save()
print("set camp %s for team %s" % (team.camp.slug, team.name))
def add_missing_team_responsibles(apps, schema_editor):
Team = apps.get_model('teams', 'Team')
TeamArea = apps.get_model('teams', 'TeamArea')
TeamMember = apps.get_model('teams', 'TeamMember')
for team in Team.objects.all():
print("responsible processing team %s..." % team.name)
responsibles = TeamMember.objects.filter(team=team, responsible=True)
if not responsibles:
# get the area responsibles instead
responsibles = team.area.responsible.all()
for responsible in responsibles:
if isinstance(responsible, TeamMember):
# we need User objects instead of TeamMember objects
responsible = responsible.user
try:
membership = TeamMember.objects.get(team=team, user=responsible)
if not membership.responsible:
# already a member of the team, but not responsible
membership.responsible=True
membership.save()
print("%s is now marked as responsible" % membership.user.username)
except TeamMember.DoesNotExist:
# add the responsible as a member of the team
membership = TeamMember.objects.create(
team=team,
user=responsible,
responsible=True,
approved=True
)
print("new membership has been created for team %s" % team.name)
class Migration(migrations.Migration):
dependencies = [
('teams', '0026_team_camp'),
]
operations = [
migrations.RunPython(add_team_camp),
migrations.RunPython(add_missing_team_responsibles),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-31 12:16
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('teams', '0027_fixup_teams'),
]
operations = [
migrations.AlterField(
model_name='team',
name='area',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='teams', to='teams.TeamArea'),
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-31 12:31
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('teams', '0028_auto_20180331_1416'),
]
operations = [
migrations.RemoveField(
model_name='team',
name='area',
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-02 13:14
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teams', '0029_remove_team_area'),
]
operations = [
migrations.AlterField(
model_name='team',
name='irc_channel_name',
field=models.TextField(blank=True, default='', help_text='Team IRC channel. Leave blank to generate channel name automatically, based on camp shortslug and team shortslug.'),
),
migrations.AlterField(
model_name='team',
name='irc_channel_private',
field=models.BooleanField(default=True, help_text='Check to make the IRC channel secret and +i (private for team members only using an ACL). Leave unchecked to make the IRC channel public and open for everyone.'),
),
migrations.AlterField(
model_name='team',
name='needs_members',
field=models.BooleanField(default=True, help_text='Check to indicate that this team needs more members'),
),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-02 19:46
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teams', '0030_auto_20180402_1514'),
]
operations = [
migrations.AddField(
model_name='team',
name='mailing_list_archive_public',
field=models.BooleanField(default=False, help_text='Check if the mailing list archive is public'),
),
migrations.AddField(
model_name='team',
name='mailing_list_nonmember_posts',
field=models.BooleanField(default=False, help_text='Check if the mailinglist allows non-list-members to post'),
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-02 19:48
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('teams', '0031_auto_20180402_2146'),
]
operations = [
migrations.AlterField(
model_name='team',
name='camp',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='teams', to='camps.Camp'),
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-02 20:04
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('teams', '0032_auto_20180402_2148'),
]
operations = [
migrations.AlterUniqueTogether(
name='teamarea',
unique_together=set([]),
),
migrations.RemoveField(
model_name='teamarea',
name='camp',
),
migrations.RemoveField(
model_name='teamarea',
name='responsible',
),
migrations.DeleteModel(
name='TeamArea',
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-02 21:34
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('teams', '0033_auto_20180402_2204'),
]
operations = [
migrations.AlterModelOptions(
name='teammember',
options={'ordering': ['-responsible', 'approved']},
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-02 21:44
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('teams', '0034_auto_20180402_2334'),
]
operations = [
migrations.AlterModelOptions(
name='teammember',
options={'ordering': ['-responsible', '-approved']},
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-03 00:01
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('camps', '0025_auto_20180318_1250'),
('teams', '0035_auto_20180402_2344'),
]
operations = [
migrations.AlterUniqueTogether(
name='team',
unique_together=set([('slug', 'camp'), ('name', 'camp')]),
),
]

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-08 12:16
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('teams', '0036_auto_20180403_0201'),
]
operations = [
migrations.AddField(
model_name='teammember',
name='irc_channel_acl_ok',
field=models.BooleanField(default=False, help_text='Maintained by the IRC bot, do not edit manually. True if the teammembers NickServ username has been added to the Team IRC channels ACL.'),
),
migrations.AlterField(
model_name='teammember',
name='approved',
field=models.BooleanField(default=False, help_text='True if this membership is approved. False if not.'),
),
migrations.AlterField(
model_name='teammember',
name='responsible',
field=models.BooleanField(default=False, help_text='True if this teammember is responsible for this Team. False if not.'),
),
migrations.AlterField(
model_name='teammember',
name='team',
field=models.ForeignKey(help_text='The Team this membership relates to', on_delete=django.db.models.deletion.PROTECT, to='teams.Team'),
),
migrations.AlterField(
model_name='teammember',
name='user',
field=models.ForeignKey(help_text='The User object this team membership relates to', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -3,7 +3,6 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.text import slugify
from utils.models import CampRelatedModel
from .email import add_new_membership_email
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse_lazy
@ -11,123 +10,219 @@ import logging
logger = logging.getLogger("bornhack.%s" % __name__)
class TeamArea(CampRelatedModel):
class Meta:
ordering = ['name']
unique_together = ('name', 'camp')
name = models.CharField(max_length=255)
description = models.TextField(default='')
camp = models.ForeignKey('camps.Camp', related_name="teamareas", on_delete=models.PROTECT)
responsible = models.ManyToManyField(
'auth.User',
related_name='responsible_team_areas'
)
def __str__(self):
return '{} ({})'.format(self.name, self.camp)
class Team(CampRelatedModel):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, blank=True)
area = models.ForeignKey(
'teams.TeamArea',
related_name='teams',
on_delete=models.PROTECT
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)
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 = models.EmailField(blank=True)
# 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
irc_channel = models.BooleanField(
default=False,
help_text='Check to make the IRC bot join the team IRC channel. Leave unchecked to disable IRC bot functionality for this team entirely.',
)
irc_channel_name = models.TextField(
default='',
blank=True,
help_text='Team IRC channel. Leave blank to generate channel name automatically, based on camp shortslug and team shortslug.',
)
irc_channel_managed = models.BooleanField(
default=True,
help_text='Check to make the bot manage the team IRC channel. The bot will register the channel with ChanServ if possible, and manage ACLs as needed.',
)
irc_channel_private = models.BooleanField(
default=True,
help_text='Check to make the IRC channel secret and +i (private for team members only using an ACL). Leave unchecked to make the IRC channel public and open for everyone.'
)
class Meta:
ordering = ['name']
unique_together = (('name', 'camp'), ('slug', 'camp'))
def __str__(self):
return '{} ({})'.format(self.name, self.camp)
def validate_unique(self, exclude):
"""
We cannot use unique_together with the camp field because it is a property,
so check uniqueness of team name and slug here instead
"""
# check if this team name is in use under this camp
if self.camp.teams.filter(name=self.name).exists():
raise ValidationError("This Team name already exists for this Camp")
if self.camp.teams.filter(slug=self.slug).exists():
raise ValidationError("This Team slug already exists for this Camp")
return True
@property
def camp(self):
return self.area.camp
def save(self, **kwargs):
if (
not self.pk or
not self.slug
):
# generate slug if needed
if not self.pk or not self.slug:
slug = slugify(self.name)
self.slug = slug
if not self.shortslug:
self.shortslug = self.slug
# generate IRC channel name if needed
if self.irc_channel and not self.irc_channel_name:
self.irc_channel_name = "#%s-%s" % (self.camp.shortslug, self.shortslug)
super().save(**kwargs)
def memberstatus(self, member):
if member not in self.members.all():
return "Not member"
else:
if TeamMember.objects.get(team=self, user=member).approved:
return "Member"
else:
return "Membership Pending"
def clean(self):
# make sure the irc channel name is prefixed with a # if it is set
if self.irc_channel_name and self.irc_channel_name[0] != "#":
self.irc_channel_name = "#%s" % self.irc_channel_name
if self.irc_channel_name:
if Team.objects.filter(irc_channel_name=self.irc_channel_name).exclude(pk=self.pk).exists():
raise ValidationError("This IRC channel name is already in use")
@property
def responsible(self):
if TeamMember.objects.filter(team=self, responsible=True).exists():
return User.objects.filter(
teammember__team=self,
teammember__responsible=True
)
else:
return self.area.responsible.all()
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 anoncount(self):
return self.approvedmembers.filter(user__profile__public_credit_name_approved=False).count()
def approved_members(self):
"""
Returns only approved members (returns User objects, not TeamMember objects)
"""
return self.members.filter(
teammember__approved=True
)
@property
def approvedmembers(self):
return TeamMember.objects.filter(team=self, approved=True)
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)
team = models.ForeignKey('teams.Team', on_delete=models.PROTECT)
approved = models.BooleanField(default=False)
responsible = models.BooleanField(default=False)
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_channel_acl_ok = models.BooleanField(
default=False,
help_text="Maintained by the IRC bot, do not edit manually. True if the teammembers NickServ username has been added to the Team IRC channels ACL.",
)
class Meta:
ordering = ['-responsible', '-approved']
def __str__(self):
return '{} is {} member of team {}'.format(
self.user, '' if self.approved else 'an unapproved', self.team
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
@receiver(post_save, sender=TeamMember)
def add_responsible_email(sender, instance, created, **kwargs):
if created:
if not add_new_membership_email(instance):
logger.error('Error adding email to outgoing queue')
class TeamTask(CampRelatedModel):
team = models.ForeignKey(
'teams.Team',
@ -157,14 +252,12 @@ class TeamTask(CampRelatedModel):
@property
def camp(self):
""" All CampRelatedModels must have a camp FK or a camp property """
return self.team.camp
def save(self, **kwargs):
# generate slug if needed
if not self.slug:
self.slug = slugify(self.name)
super().save(**kwargs)
@property
def responsible(self):
return self.team.responsible.all()

View File

@ -0,0 +1,31 @@
from .email import add_new_membership_email
from ircbot.utils import add_irc_message
from django.conf import settings
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
def teammember_saved(sender, instance, created, **kwargs):
"""
This signal handler is called whenever a TeamMember instance is saved
"""
# if this is a new unapproved teammember send a mail to team responsibles
if created and not instance.approved:
# call the mail sending function
if not add_new_membership_email(instance):
logger.error('Error adding email to outgoing queue')
# if this team has a private and bot-managed IRC channel check if we need to add this member to ACL
if instance.team.irc_channel and instance.team.irc_channel_managed and instance.team.irc_channel_private:
# if this membership is approved and the member has entered a nickserv_username which not yet been added to the ACL
if instance.approved and instance.user.profile.nickserv_username and not instance.irc_channel_acl_ok:
add_team_channel_acl(instance)
def teammember_deleted(sender, instance, **kwargs):
"""
This signal handler is called whenever a TeamMember instance is deleted
"""
if instance.irc_channel_acl_ok and instance.team.irc_channel and instance.team.irc_channel_managed and instance.team.irc_channel_private:
# TODO: we have an ACL entry that needs to be deleted but the bot does not handle it automatically
add_irc_message(instance.team.irc_channel_name, "Teammember %s removed from team. Please remove NickServ user %s from IRC channel ACL manually!" % (instance.user.get_public_credit_name, instance.user.profile.nickserv_username))

View File

@ -1,27 +1,47 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load teams_tags %}
{% load bootstrap3 %}
{% load teams_tags %}
{% block title %}
Team: {{ team.name }} | {{ block.super }}
{% endblock %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading"><h4>{{ team.name }} Team</h4></div>
<div class="panel-heading"><h4>{{ team.name }} Team Details</h4></div>
<div class="panel-body">
{{ team.description|unsafecommonmark }}
{% if request.user in team.responsible.all %}
<a href="{% url 'teams:manage' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary">Manage Team</a>
<hr>
<h4>{{ team.name }} Team Communications</h4>
{{ team.camp.title }} teams primarily use mailing lists and IRC to communicate. The <b>{{ team.name }} team</b> can be contacted in the following ways:</p>
<h5>Mailing List</h5>
{% if team.mailing_list and request.user in team.approved_members.all %}
<p>The {{ team.name }} Team mailinglist is <b>{{ team.mailing_list }}</b>{% if team.mailing_list_archive_public %}, and the archives are publicly available{% endif %}. You should sign up for the list if you haven't already.</p>
{% elif team.mailing_list and team.mailinglist_nonmember_posts %}
<p>The {{ team.name }} Team mailinglist is <b>{{ team.mailing_list }}</b>{% if team.mailing_list_archive_public %}, and the archives are publicly available{% endif %}. You do not need to be a member of the list to post to it.</p>
{% else %}
<p>The {{ team.name }} Team does not have a public mailing list, but it can be contacted through our main email <a href="mailto:info@bornhack.dk">info@bornhack.dk</a>.
{% endif %}
<h5>IRC Channel</h5>
{% if team.irc_channel and request.user in team.approved_members.all %}
<p>The {{ team.name }} Team IRC channel is <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ team.irc_channel_name }}">{{ team.irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}</a>.
{% if team.irc_channel_private %}The channel is not open to the public. Enter your nickserv username in your <a href="{% url 'profiles:detail' %}">Profile</a> to get access.{% else %}The IRC channel is open for everyone to join.{% endif %}</p>
{% elif team.irc_channel and not team.irc_channel_private %}
<p>The {{ team.name }} Team IRC channel is <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ team.irc_channel_name }}">{{ team.irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}</a> and it is open for everyone to join.</p>
{% else %}
<p>The {{ team.name }} Team does not have a public IRC channel, but it can be reached through our main IRC channel <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ IRCBOT_PUBLIC_CHANNEL }}">{{ IRCBOT_PUBLIC_CHANNEL }} on {{ IRCBOT_SERVER_HOSTNAME }}</a>.</p>
{% endif %}
<hr>
<h3>Members</h3>
<p>The following <b>{{ team.approvedmembers.count }}</b> people are members of the <b>{{ team.name }} team</b>:</p>
<table class="table">
<h4>{{ team.name }} Team Members</h4>
<p>The following <b>{{ team.approved_members.count }}</b> people {% if team.unapproved_members.count %}(and {{ team.unapproved_members.count }} pending){% endif %} are members of the <b>{{ team.name }} Team</b>:</p>
<table class="table table-hover">
<thead>
<tr>
<th>
@ -33,40 +53,39 @@ Team: {{ team.name }} | {{ block.super }}
</tr>
</thead>
<tbody>
{% for teammember in team.approvedmembers.all %}
{% for teammember in team.memberships.all %}
<tr>
<td>
{% if teammember.user.profile.approved_public_credit_name %}
{{ teammember.user.profile.approved_public_credit_name }}
{% else %}
anonymous
{% endif %}
{{ teammember.user.profile.get_public_credit_name }} {% if teammember.user == request.user %}(this is you!){% endif %}
</td>
<td>
{% if teammember.responsible %}Team Responsible{% else %}Team Member{% endif %}
Team {% if teammember.responsible %}Responsible{% else %}Member{% endif %}
{% if not teammember.approved %}(pending approval){% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if request.user in team.members.all %}
<p>Your membership status: <b>{% membershipstatus request.user team %}</b></p>
{% endif %}
<p>Your membership status: <b>{% membershipstatus user team %}</b></p>
{% if request.user in team.members.all %}
<a href="{% url 'teams:leave' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-danger">Leave Team</a>
<a href="{% url 'teams:leave' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-danger"><i class="fa fa-remove"></i> Leave Team</a>
{% else %}
{% if team.needs_members %}
<b>This team is looking for members!</b> <a href="{% url 'teams:join' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-xs btn-success">Join Team</a>
<b>This team is looking for members!</b> <a href="{% url 'teams:join' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-xs btn-success"><i class="fa fa-plus"></i> Join Team</a>
{% endif %}
{% endif %}
{% if request.user in team.responsible_members.all %}
<a href="{% url 'teams:manage' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fa fa-cog"></i> Manage Team</a>
{% endif %}
<hr>
<h3>Tasks</h3>
<p>This team is responsible for the following tasks</p>
<table class="table">
<h4>{{ team.name }} Team Tasks</h4>
<p>The {{ team.name }} Team is responsible for the following tasks</p>
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
@ -80,17 +99,17 @@ Team: {{ team.name }} | {{ block.super }}
<td><a href="{% url 'teams:task_detail' slug=task.slug camp_slug=camp.slug team_slug=team.slug %}">{{ task.name }}</a></td>
<td>{{ task.description }}</td>
<td>
<a href="{% url 'teams:task_detail' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm">Details</a>
{% if request.user in team.responsible.all %}
<a href="{% url 'teams:task_update' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm">Edit Task</a>
<a href="{% url 'teams:task_detail' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm"><i class="fa fa-search"></i> Details</a>
{% if request.user in team.responsible_members.all %}
<a href="{% url 'teams:task_update' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm"><i class="fa fa-edit"></i> Edit Task</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if request.user in team.responsible.all %}
<a href="{% url 'teams:task_create' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary">Create Task</a>
{% if request.user in team.responsible_members.all %}
<a href="{% url 'teams:task_create' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fa fa-plus"></i> Create Task</a>
{% endif %}
</div>
</div>

View File

@ -7,12 +7,17 @@ Join Team: {{ team.name }} | {{ block.super }}
{% block content %}
<h3>{{ team.name }} Team</h3>
<p class="lead">Really join the <b>{{ team.name }}</b> team? You will receive a message when your membership has been approved.<p>
<p class="lead">Really join the <b>{{ team.name }}</b> Team for <b>{{ team.camp.title }}</b>?</p>
<p>Your membership will need to be approved by a team responsible. You will receive an email when your membership request has been processed.<p>
<p>
<form method="POST">
{% csrf_token %}
{{ form }}
<button class="btn btn-success" type="submit"><i class="fa fa-check"></i> Join {{ team.name }} Team</button>
<a href="{% url 'teams:list' camp_slug=camp.slug %}" class="btn btn-default" type="submit"><i class="fa fa-remove"></i> Cancel</a>
</form>
</p>
{% endblock %}

View File

@ -12,7 +12,7 @@ Teams | {{ block.super }}
<p>This is a list of the teams for {{ camp.title }}. To join a team just press the Join button, but please put some info in your <a href="{% url 'profiles:detail' %}">profile</a> first, so the team responsible has some idea who you are.</p>
<p>You can also leave a team of course, but please let the team responsible know why :)</p>
<p>Team memberships need to be approved by a team responsible. You will receive a message when your membership has been approved.</p>
<p>At {{ camp.title }} all organisers and volunteers buy full tickets like everyone else. In the future our budget may allow for discounts or free tickets for volunteers, but not this year. However: Please let us know if you can't afford a ticket - we will figure something out!</p>
<p>At {{ camp.title }} all organisers and volunteers buy full tickets like everyone else. At future events our budget may allow for discounts or free tickets for volunteers, but currently it does not.</p>
<p>We currently have {{ teams.count }} teams for {{ camp.title }}:</p>
{% if teams %}
<table class="table table-hover">
@ -42,13 +42,13 @@ Teams | {{ block.super }}
</td>
<td>
{% for resp in team.responsible.all %}
{{ resp.profile.approved_public_credit_name|default:"Unnamed" }}{% if not forloop.last %},{% endif %}<br>
{% for resp in team.responsible_members.all %}
{{ resp.profile.get_public_credit_name }}{% if not forloop.last %},{% endif %}<br>
{% endfor %}
</td>
<td class="text-center">
<span class="badge">{{ team.approvedmembers.count }}</span><br>
<span class="badge">{{ team.members.count }}</span><br>
{% if team.needs_members %}(more needed){% endif %}
</td>
@ -58,30 +58,23 @@ Teams | {{ block.super }}
{% if request.user.is_authenticated %}
<td class="text-center">
{% membershipstatus request.user team as membership_status %}
{% if membership_status == 'Membership Pending' %}
<i class='fa fa-clock-o' title='Pending'></i>
{% else %}
{% if membership_status == 'Member' %}
<i class='fa fa-thumbs-o-up' title='Member'></i>
{% else %}
<i class='fa fa-times' title='Not a member'></i>
{% endif %}
{% endif %}
{% membershipstatus request.user team True %}
</td>
<td>
{% if request.user in team.members.all %}
<a href="{% url 'teams:leave' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-danger"><i class="fa fa-minus"></i> Leave</a>
{% else %}
{% if team.needs_members %}
<a href="{% url 'teams:join' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-success"><i class="fa fa-plus"></i> Join</a>
{% endif %}
{% endif %}
{% if request.user in team.responsible.all %}
<div class="btn-group-vertical">
<a class="btn btn-primary" href="{% url 'teams:detail' camp_slug=camp.slug team_slug=team.slug %}"><i class="fa fa-search"></i> Details</a>
{% if request.user in team.responsible_members.all %}
<a href="{% url 'teams:manage' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fa fa-cog"></i> Manage</a>
{% endif %}
{% if request.user in team.members.all %}
<a href="{% url 'teams:leave' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-danger"><i class="fa fa-remove"></i> Leave</a>
{% else %}
{% if team.needs_members %}
<a href="{% url 'teams:join' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-success"><i class="fa fa-plus"></i> Join</a>
{% endif %}
{% endif %}
</div>
{% endif %}
</td>
</tr>

View File

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load teams_tags %}
{% load bootstrap3 %}
{% block title %}
@ -8,81 +7,92 @@ Manage Team: {{ team.name }} | {{ block.super }}
{% endblock %}
{% block content %}
<h3>Manage {{ team.name }} Team</h3>
<form method="post" class="form-horizontal">
{% csrf_token %}
<div class="panel panel-default">
<div class="panel-heading"><h4>Manage {{ team.name }} Team</h4></div>
<div class="panel-body" style="margin-left: 1em; margin-right: 1em;">
<div class="form-group">
<form method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_form form %}
{% buttons %}
<button class="btn btn-success pull-right" type="submit"><i class="fa fa-check"></i> Save Team</button>
<a class="btn btn-primary pull-right" href="{% url 'teams:detail' team_slug=team.slug camp_slug=camp.slug %}"><i class="fa fa-remove"></i> Cancel</a>&nbsp;
{% endbuttons %}
</form>
</div>
</div>
</div>
{% buttons %}
<button class="btn btn-primary pull-right" type="submit">Save</button>
{% endbuttons %}
</form>
<h3>{{ team.name }} Team Members</h3>
{% if team.teammember_set.exists %}
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>
Profile
</th>
<th>
Name
</th>
<th>
Email
</th>
<th>
Description
</th>
<th>
Public Credit Name
</th>
<th>
Membership
</th>
<th>
Action
</th>
</tr>
</thead>
<tbody>
{% for membership in team.teammember_set.all %}
<tr>
<td>
{{ membership.user }}
</td>
<td>
{{ membership.user.profile.name }}
</td>
<td>
{{ membership.user.profile.email }}
</td>
<td>
{{ membership.user.profile.description }}
</td>
<td>
{{ membership.user.profile.public_credit_name|default:"N/A" }}
</td>
<td>
{% if membership.approved %}member{% else %}pending{% endif %}
</td>
<td>
{% if membership.approved %}
<a class="btn btn-danger" href="{% url 'teams:teammember_remove' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-trash-o"></i> Remove</a>
{% else %}
<a class="btn btn-danger" href="{% url 'teams:teammember_remove' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-trash-o"></i> Remove</a>
<a class="btn btn-success" href="{% url 'teams:teammember_approve' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-check"></i> Approve</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No members found!</p>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading"><h4>Manage {{ team.name }} Team Members</h4></div>
<div class="panel-body" style="margin-left: 1em; margin-right: 1em;">
{% if team.teammember_set.exists %}
<table class="table table-hover">
<thead>
<tr>
<th>
Username
</th>
<th>
Name
</th>
<th>
Email
</th>
<th>
Description
</th>
<th>
Public Credit Name
</th>
<th>
Membership
</th>
<th>
Action
</th>
</tr>
</thead>
<tbody>
{% for membership in team.teammember_set.all %}
<tr>
<td>
{{ membership.user }}
</td>
<td>
{{ membership.user.profile.name }}
</td>
<td>
{{ membership.user.profile.email }}
</td>
<td>
{{ membership.user.profile.description }}
</td>
<td>
{{ membership.user.profile.public_credit_name|default:"N/A" }}
{% if membership.user.profile.public_credit_name and not membership.user.profile.public_credit_name_approved %}<span class="text-warning">(name not approved)</span>{% endif %}
</td>
<td>
{% if membership.approved %}member{% else %}pending{% endif %}
</td>
<td>
<div class="btn-group-vertical">
<a class="btn btn-danger" href="{% url 'teams:teammember_remove' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-trash-o"></i> Remove Member</a>
{% if not membership.approved %}
<a class="btn btn-success" href="{% url 'teams:teammember_approve' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-check"></i> Approve Member</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No members found!</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,8 +1,24 @@
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.simple_tag
def membershipstatus(user, team):
return team.memberstatus(user)
def membershipstatus(user, team, showicon=False):
if user in team.responsible_members.all():
text = "Responsible"
icon = "fa-star"
elif user in team.approved_members.all():
text = "Member"
icon = "fa-thumbs-o-up"
elif user in team.unapproved_members.all():
text = "Membership pending approval"
icon = "fa-clock-o"
else:
text = "Not member"
icon = "fa-times"
if showicon:
return mark_safe("<i class='fa %s' title='%s'></i>" % (icon, text))
else:
return text

View File

@ -9,6 +9,7 @@ from django.contrib import messages
from django.http import HttpResponseRedirect
from django.views.generic.detail import SingleObjectMixin
from django.core.urlresolvers import reverse_lazy
from django.conf import settings
from profiles.models import Profile
@ -17,9 +18,12 @@ logger = logging.getLogger("bornhack.%s" % __name__)
class EnsureTeamResponsibleMixin(object):
"""
Use to make sure request.user is responsible for the team specified by kwargs['team_slug']
"""
def dispatch(self, request, *args, **kwargs):
self.team = Team.objects.get(slug=kwargs['team_slug'], camp=self.camp)
if request.user not in self.team.responsible.all():
if request.user not in self.team.responsible_members.all():
messages.error(request, 'No thanks')
return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=self.team.slug)
@ -28,6 +32,22 @@ class EnsureTeamResponsibleMixin(object):
)
class EnsureTeamMemberResponsibleMixin(SingleObjectMixin):
"""
Use to make sure request.user is responsible for the team which TeamMember belongs to
"""
model = TeamMember
def dispatch(self, request, *args, **kwargs):
if request.user not in self.get_object().team.responsible_members.all():
messages.error(request, 'No thanks')
return redirect('teams:detail', camp_slug=self.get_object().team.camp.slug, team_slug=self.get_object().team.slug)
return super().dispatch(
request, *args, **kwargs
)
class TeamListView(CampViewMixin, ListView):
template_name = "team_list.html"
model = Team
@ -40,16 +60,26 @@ class TeamDetailView(CampViewMixin, DetailView):
model = Team
slug_url_kwarg = 'team_slug'
def get_context_data(self, **kwargs):
context = super(TeamDetailView, self).get_context_data(**kwargs)
context['IRCBOT_SERVER_HOSTNAME'] = settings.IRCBOT_SERVER_HOSTNAME
context['IRCBOT_PUBLIC_CHANNEL'] = settings.IRCBOT_PUBLIC_CHANNEL
return context
class TeamManageView(CampViewMixin, EnsureTeamResponsibleMixin, UpdateView):
model = Team
template_name = "team_manage.html"
fields = ['description', 'needs_members']
fields = ['description', 'needs_members', 'irc_channel', 'irc_channel_name', 'irc_channel_managed', 'irc_channel_private']
slug_url_kwarg = 'team_slug'
def get_success_url(self):
return reverse_lazy('teams:detail', kwargs={'camp_slug': self.camp.slug, 'team_slug': self.get_object().slug})
def form_valid(self, form):
messages.success(self.request, "Team has been saved")
return super().form_valid(form)
class TeamJoinView(LoginRequiredMixin, CampViewMixin, UpdateView):
template_name = "team_join.html"
@ -100,18 +130,6 @@ class TeamLeaveView(LoginRequiredMixin, CampViewMixin, UpdateView):
return redirect('teams:list', camp_slug=self.get_object().camp.slug)
class EnsureTeamMemberResponsibleMixin(SingleObjectMixin):
model = TeamMember
def dispatch(self, request, *args, **kwargs):
if request.user not in self.get_object().team.responsible.all():
messages.error(request, 'No thanks')
return redirect('teams:detail', camp_slug=self.get_object().team.camp.slug, team_slug=self.get_object().team.slug)
return super().dispatch(
request, *args, **kwargs
)
class TeamMemberRemoveView(LoginRequiredMixin, CampViewMixin, EnsureTeamMemberResponsibleMixin, UpdateView):
template_name = "teammember_remove.html"

View File

@ -5,21 +5,20 @@ from datetime import (
)
from django.db.models import Count
from django.utils import timezone
from events.handler import handle_team_event
def ticket_changed(sender, instance, created, **kwargs):
"""
This signal is called every time a ShopTicket is saved
"""
# only queue an IRC message when a new ticket is created
# only trigger an event when a new ticket is created
if not created:
return
# queue an IRC message to the orga channel if defined,
# otherwise for the default channel
target = settings.IRCBOT_CHANNELS['orga'] if 'orga' in settings.IRCBOT_CHANNELS else settings.IRCBOT_CHANNELS['default']
# get ticket stats
from .models import ShopTicket
# TODO: this is nasty, get the prefix some other way
ticket_prefix = "BornHack {}".format(datetime.now().year)
stats = ", ".join(
@ -50,19 +49,17 @@ def ticket_changed(sender, instance, created, **kwargs):
).count()
# queue the messages
from ircbot.models import OutgoingIrcMessage
OutgoingIrcMessage.objects.create(
target=target,
message="%s sold!" % instance.product.name,
timeout=timezone.now()+timedelta(minutes=10)
handle_team_event(
eventtype='ticket_created',
irc_message="%s sold!" % instance.product.name
)
OutgoingIrcMessage.objects.create(
target=target,
message="Totals: {}, 1day: {}, 1day child: {}".format(
# limit this one to a length of 200 because IRC is nice
handle_team_event(
eventtype='ticket_created',
irc_message="Totals: {}, 1day: {}, 1day child: {}".format(
stats,
onedaystats,
onedaychildstats
)[:200],
timeout=timezone.now()+timedelta(minutes=10)
)[:200]
)

View File

@ -23,9 +23,12 @@ from tickets.models import (
from teams.models import (
Team,
TeamTask,
TeamArea,
TeamMember
)
from events.models import (
Type,
Routing
)
from django.contrib.auth.models import User
from allauth.account.models import EmailAddress
from django.utils.text import slugify
@ -44,6 +47,7 @@ class Command(BaseCommand):
title='BornHack 2016',
tagline='Initial Commit',
slug='bornhack-2016',
shortslug='bh2016',
buildup=(
timezone.datetime(2016, 8, 25, 12, 0, tzinfo=timezone.utc),
timezone.datetime(2016, 8, 27, 11, 59, tzinfo=timezone.utc),
@ -63,6 +67,7 @@ class Command(BaseCommand):
title='BornHack 2017',
tagline='Make Tradition',
slug='bornhack-2017',
shortslug='bh2017',
buildup=(
timezone.datetime(2017, 8, 25, 12, 0, tzinfo=timezone.utc),
timezone.datetime(2017, 8, 27, 11, 59, tzinfo=timezone.utc),
@ -82,6 +87,7 @@ class Command(BaseCommand):
title='BornHack 2018',
tagline='Undecided',
slug='bornhack-2018',
shortslug='bh2018',
buildup=(
timezone.datetime(2018, 8, 25, 12, 0, tzinfo=timezone.utc),
timezone.datetime(2018, 8, 27, 11, 59, tzinfo=timezone.utc),
@ -104,6 +110,8 @@ class Command(BaseCommand):
)
user1.profile.name = 'John Doe'
user1.profile.description = 'one that once was'
user1.profile.public_credit_name = 'PublicDoe'
user1.profile.public_credit_name_approved = True
user1.profile.save()
email = EmailAddress.objects.create(
user=user1,
@ -112,6 +120,7 @@ class Command(BaseCommand):
verified=True
)
email.set_as_primary()
user2 = User.objects.create_user(
username='user2',
password='user2',
@ -127,6 +136,7 @@ class Command(BaseCommand):
verified=True
)
email.set_as_primary()
user3 = User.objects.create_user(
username='user3',
password='user3',
@ -134,6 +144,7 @@ class Command(BaseCommand):
)
user3.profile.name = 'Lorem Ipsum'
user3.profile.description = 'just a user'
user3.profile.public_credit_name = 'Lorem Ipsum'
user3.profile.save()
email = EmailAddress.objects.create(
user=user3,
@ -142,6 +153,7 @@ class Command(BaseCommand):
verified=True
)
email.set_as_primary()
user4 = User.objects.create_user(
username='user4',
password='user4',
@ -149,6 +161,8 @@ class Command(BaseCommand):
)
user4.profile.name = 'Ethe Reum'
user4.profile.description = 'I prefer doge'
user4.profile.public_credit_name = 'Dogefan'
user4.profile.public_credit_name_approved = True
user4.profile.save()
email = EmailAddress.objects.create(
user=user4,
@ -157,6 +171,96 @@ class Command(BaseCommand):
verified=True
)
email.set_as_primary()
user5 = User.objects.create_user(
username='user5',
password='user5',
is_staff=True
)
user5.profile.name = 'Pyra Mid'
user5.profile.description = 'This is not a scam'
user5.profile.public_credit_name = 'Ponziarne'
user5.profile.public_credit_name_approved = True
user5.profile.save()
email = EmailAddress.objects.create(
user=user5,
email='user5@example.com',
primary=False,
verified=True
)
email.set_as_primary()
user6 = User.objects.create_user(
username='user6',
password='user6',
is_staff=True
)
user6.profile.name = 'User Number 6'
user6.profile.description = 'some description'
user6.profile.public_credit_name = 'bruger 6'
user6.profile.public_credit_name_approved = True
user6.profile.save()
email = EmailAddress.objects.create(
user=user6,
email='user6@example.com',
primary=False,
verified=True
)
email.set_as_primary()
user7 = User.objects.create_user(
username='user7',
password='user7',
is_staff=True
)
user7.profile.name = 'Assembly Hacker'
user7.profile.description = 'Low level is best level'
user7.profile.public_credit_name = 'asm'
user7.profile.public_credit_name_approved = True
user7.profile.save()
email = EmailAddress.objects.create(
user=user7,
email='user7@example.com',
primary=False,
verified=True
)
email.set_as_primary()
user8 = User.objects.create_user(
username='user8',
password='user8',
is_staff=True
)
user8.profile.name = 'TCL'
user8.profile.description = 'Expect me'
user8.profile.public_credit_name = 'TCL lover'
user8.profile.public_credit_name_approved = True
user8.profile.save()
email = EmailAddress.objects.create(
user=user8,
email='user8@example.com',
primary=False,
verified=True
)
email.set_as_primary()
user9 = User.objects.create_user(
username='user9',
password='user9',
is_staff=True
)
user9.profile.name = 'John Windows'
user9.profile.description = 'Microsoft is best soft'
user9.profile.public_credit_name = 'msboy'
user9.profile.save()
email = EmailAddress.objects.create(
user=user9,
email='user9@example.com',
primary=False,
verified=True
)
email.set_as_primary()
admin = User.objects.create_superuser(
username='admin',
email='admin@example.com',
@ -1373,44 +1477,21 @@ Please note that sleeping in the parking lot is not permitted. If you want to sl
description='This village is representing TheCamp.dk, an annual danish tech camp held in July. The official subjects for this event is open source software, network and security. In reality we are interested in anything from computers to illumination soap bubbles and irish coffee'
)
self.output("Creating team areas for {}...".format(year))
pr_area = TeamArea.objects.create(
name='PR',
description="The Public Relations area covers website, social media and marketing related tasks.",
camp=camp
)
content_area = TeamArea.objects.create(
name='Content',
description="The Content area handles talks, A/V and photos.",
camp=camp
)
infrastructure_area = TeamArea.objects.create(
name='Infrastructure',
description="The Infrastructure area covers network/NOC, power, villages, CERT, logistics.",
camp=camp
)
bar_area = TeamArea.objects.create(
name='Bar',
description="The Bar area covers building and running the IRL bar, DJ booth and related tasks.",
camp=camp
)
self.output("Setting teamarea responsibles for {}...".format(year))
pr_area.responsible.add(user2)
content_area.responsible.add(user2, user3)
infrastructure_area.responsible.add(user3, user4)
bar_area.responsible.add(user4)
self.output("Creating teams for {}...".format(year))
orga_team = Team.objects.create(
name="Orga",
description="The Orga team are the main organisers. All tasks are Orga responsibility until they are delegated to another team",
camp=camp
)
noc_team = Team.objects.create(
name="NOC",
description="The NOC team is in charge of establishing and running a network onsite.".format(year),
area=infrastructure_area,
description="The NOC team is in charge of establishing and running a network onsite.",
camp=camp
)
bar_team = Team.objects.create(
name="Bar",
description="The Bar team plans, builds and run the IRL bar!",
area=bar_area
camp=camp
)
self.output("Creating TeamTasks for {}...".format(year))
@ -1461,29 +1542,93 @@ Please note that sleeping in the parking lot is not permitted. If you want to sl
)
self.output("Setting team members for {}...".format(year))
# noc team
TeamMember.objects.create(
team=noc_team,
user=user4,
approved=True
approved=True,
responsible=True
)
TeamMember.objects.create(
team=noc_team,
user=user1
user=user1,
approved=True,
)
TeamMember.objects.create(
team=noc_team,
user=user5,
approved=True,
)
TeamMember.objects.create(
team=noc_team,
user=user6,
)
# bar team
TeamMember.objects.create(
team=bar_team,
user=user1,
approved=True
approved=True,
responsible=True
)
TeamMember.objects.create(
team=bar_team,
user=user3,
approved=True
approved=True,
responsible=True
)
TeamMember.objects.create(
team=bar_team,
user=user2
user=user2,
approved=True,
)
TeamMember.objects.create(
team=bar_team,
user=user7,
approved=True,
)
TeamMember.objects.create(
team=bar_team,
user=user8,
)
# orga team
TeamMember.objects.create(
team=orga_team,
user=user1,
approved=True,
responsible=True
)
TeamMember.objects.create(
team=orga_team,
user=user3,
approved=True,
responsible=True
)
TeamMember.objects.create(
team=orga_team,
user=user8,
approved=True,
)
TeamMember.objects.create(
team=orga_team,
user=user9,
approved=True,
)
TeamMember.objects.create(
team=orga_team,
user=user4,
)
self.output("Adding event routing...")
Routing.objects.create(
team=orga_team,
eventtype=Type.objects.get(name="public_credit_name_changed")
)
Routing.objects.create(
team=orga_team,
eventtype=Type.objects.get(name="ticket_created")
)
self.output("marking 2016 as read_only...")