Merge branch 'master' into django-and-channels-upgrade

This commit is contained in:
Víðir Valberg Guðmundsson 2018-04-13 11:19:48 +02:00 committed by GitHub
commit 4d6caf6947
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 2055 additions and 412 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

@ -37,6 +37,9 @@ CAMP_REDIRECT_PERCENT=25
### changes below here are only needed for production ### changes below here are only needed for production
# email settings # email settings
{% if not django_email_realworld | default(False) %}
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
{% endif %}
EMAIL_HOST='{{ django_email_host }}' EMAIL_HOST='{{ django_email_host }}'
EMAIL_PORT={{ django_email_port }} EMAIL_PORT={{ django_email_port }}
EMAIL_HOST_USER='{{ django_email_user }}' EMAIL_HOST_USER='{{ django_email_user }}'
@ -77,13 +80,13 @@ SCHEDULE_EVENT_NOTIFICATION_MINUTES=10
# irc bot settings # irc bot settings
IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS=10 IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS=10
IRCBOT_NICK='{{ django_ircbot_nickname }}' 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_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_HOSTNAME='{{ django_ircbot_server }}'
IRCBOT_SERVER_PORT=6697 IRCBOT_SERVER_PORT=6697
IRCBOT_SERVER_USETLS=True IRCBOT_SERVER_USETLS=True
IRCBOT_CHANNELS={ IRCBOT_PUBLIC_CHANNEL='{{ django_ircbot_public_channel }}'
'default': '{{ django_ircbot_default_channel }}',
'orga': '{{ django_ircbot_orga_channel }}',
'public': '{{ django_ircbot_public_channel }}'
}

View file

@ -45,6 +45,7 @@ INSTALLED_APPS = [
'tickets', 'tickets',
'bar', 'bar',
'backoffice', 'backoffice',
'events',
'allauth', 'allauth',
'allauth.account', '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' 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( buildup = DateTimeRangeField(
verbose_name='Buildup Period', verbose_name='Buildup Period',
help_text='The camp buildup period.', help_text='The camp buildup period.',
@ -189,9 +194,3 @@ class Camp(CreatedUpdatedModel, UUIDModel):
else: else:
return True 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 ircbot.models import OutgoingIrcMessage
from teams.models import Team, TeamMember
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from events.models import Routing
import logging import logging
logger = logging.getLogger("bornhack.%s" % __name__) logger = logging.getLogger("bornhack.%s" % __name__)
@ -26,6 +28,12 @@ class Plugin(object):
"""triggered after the server sent the MOTD (require core plugin)""" """triggered after the server sent the MOTD (require core plugin)"""
logger.debug("inside server_ready(), kwargs: %s" % kwargs) 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): def connection_lost(self, **kwargs):
"""triggered when connection is lost""" """triggered when connection is lost"""
@ -36,9 +44,6 @@ class Plugin(object):
"""triggered when connection is up""" """triggered when connection is up"""
logger.debug("inside connection_made(), kwargs: %s" % kwargs) 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 ### 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""" """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) 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) @irc3.event(irc3.rfc.PRIVMSG)
def on_privmsg(self, **kwargs): def on_privmsg(self, **kwargs):
"""triggered when a privmsg is sent to the bot or to a channel the bot is in""" """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) logger.debug("inside on_privmsg(), kwargs: %s" % kwargs)
# nickserv # we only handle NOTICEs for now
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.": if kwargs['event'] != "NOTICE":
logger.info("Nickserv identify needed, fixing...") return
self.bot.privmsg("NickServ@services.baconsvin.org", "identify %s %s" % (settings.IRCBOT_NICK, settings.IRCBOT_NICKSERV_PASSWORD))
# 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) @irc3.event(irc3.rfc.KICK)
@ -68,13 +95,28 @@ class Plugin(object):
############################################################################################### ###############################################################################################
### custom irc3 methods ### 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 @irc3.extend
def get_outgoing_messages(self): def get_outgoing_messages(self):
""" """
This method gets unprocessed OutgoingIrcMessage objects and attempts to send them to 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. 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'): for msg in OutgoingIrcMessage.objects.filter(processed=False).order_by('created'):
logger.info("processing irc message to %s: %s" % (msg.target, msg.message)) 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 # if this message expired mark it as expired and processed without doing anything
@ -99,7 +141,209 @@ class Plugin(object):
else: else:
logger.warning("skipping message to %s" % msg.target) 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 setup_public_channel(self, team):
"""
Configures a public team IRC channel (by unsetting SECURE and RESTRICTED modes used by private channels and setting mlock back to the default +nt-lk)
"""
# basic private channel modes
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s mlock +nt-lk" % team.irc_channel_name)
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE off" % team.irc_channel_name)
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED off" % team.irc_channel_name)
@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 from django.conf import settings
import logging import logging
import irc3 import irc3
from events.models import Routing
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('bornhack.%s' % __name__) 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 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 = { config = {
'nick': settings.IRCBOT_NICK, 'nick': settings.IRCBOT_NICK,
'autojoins': list(set(settings.IRCBOT_CHANNELS.values())), 'autojoins': [],
'host': settings.IRCBOT_SERVER_HOSTNAME, 'host': settings.IRCBOT_SERVER_HOSTNAME,
'port': settings.IRCBOT_SERVER_PORT, 'port': settings.IRCBOT_SERVER_PORT,
'ssl': settings.IRCBOT_SERVER_USETLS, '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> <thead>
<tr> <tr>
<th>Team Name</th> <th>Team Name</th>
<th>Team Responsible</th>
<th>Team Members</th> <th>Team Members</th>
</tr> </tr>
</thead> </thead>
@ -31,19 +32,20 @@ People | {{ block.super }}
{{ team.name }} Team {{ team.name }} Team
</td> </td>
<td> <td>
{% if team.anoncount == 0 and team.approvedmembers.count == 0 %} {% for resp in team.responsible_members.all %}
<b>No team member(s) {{ resp.profile.get_public_credit_name }}<br>
{% 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 %}
{% endfor %} {% endfor %}
{% if team.anoncount and team.anoncount != team.approvedmembers.count %} </td>
plus <b>{{ team.anoncount }}</b> anonymous member(s). <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 %} {% endif %}
</td> </td>
</tr> </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', 'public_credit_name_approved',
] ]
list_filter = [
'public_credit_name_approved',
]
def approve_public_credit_names(self, request, queryset): def approve_public_credit_names(self, request, queryset):
for profile in queryset.filter(public_credit_name_approved=False): for profile in queryset.filter(public_credit_name_approved=False):
profile.approve_public_credit_name() 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

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-04-11 21:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('profiles', '0009_profile_nickserv_username'),
]
operations = [
migrations.AlterField(
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. Make sure you register with NickServ _before_ you enter the username here!', max_length=50),
),
]

View file

@ -1,9 +1,5 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models.signals import (
post_save,
pre_save
)
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.dispatch import receiver from django.dispatch import receiver
@ -11,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _
from datetime import timedelta from datetime import timedelta
from ircbot.models import OutgoingIrcMessage
from utils.models import UUIDModel, CreatedUpdatedModel from utils.models import UUIDModel, CreatedUpdatedModel
@ -43,7 +38,7 @@ class Profile(CreatedUpdatedModel, UUIDModel):
public_credit_name = models.CharField( public_credit_name = models.CharField(
blank=True, blank=True,
max_length=100, 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( public_credit_name_approved = models.BooleanField(
@ -51,6 +46,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' 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. Make sure you register with NickServ _before_ you enter the username here!',
)
@property @property
def email(self): def email(self):
return self.user.email return self.user.email
@ -59,37 +60,21 @@ class Profile(CreatedUpdatedModel, UUIDModel):
return self.user.username return self.user.username
def approve_public_credit_name(self): 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.public_credit_name_approved = True
self.save() self.save()
@property @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: if self.public_credit_name_approved:
return self.public_credit_name return self.public_credit_name
else: 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,80 @@
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
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 and original.public_credit_name == instance.public_credit_name:
# public_credit_name has not been changed
return
if original and 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 original 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> <td>{{ profile.description|default:"N/A" }}</td>
</tr> </tr>
<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> <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>
<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> </table>
<a href="{% url 'profiles:update' %}" class="btn btn-black"><i class="fa fa-edit"></i> Edit Profile</a> <a href="{% url 'profiles:update' %}" class="btn btn-black"><i class="fa fa-edit"></i> Edit Profile</a>
{% endblock profile_content %} {% endblock profile_content %}

View file

@ -16,7 +16,7 @@ class ProfileDetail(LoginRequiredMixin, DetailView):
class ProfileUpdate(LoginRequiredMixin, UpdateView): class ProfileUpdate(LoginRequiredMixin, UpdateView):
model = models.Profile model = models.Profile
fields = ['name', 'description', 'public_credit_name'] fields = ['name', 'description', 'public_credit_name', 'nickserv_username']
success_url = reverse_lazy('profiles:detail') success_url = reverse_lazy('profiles:detail')
template_name = 'profile_form.html' template_name = 'profile_form.html'
@ -28,6 +28,6 @@ class ProfileUpdate(LoginRequiredMixin, UpdateView):
# user changed the name (to something non blank) # user changed the name (to something non blank)
form.instance.public_credit_name_approved = False form.instance.public_credit_name_approved = False
form.instance.save() 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) return super().form_valid(form, **kwargs)

View file

@ -14,12 +14,10 @@ class ProgramConfig(AppConfig):
from .signal_handlers import ( from .signal_handlers import (
check_speaker_event_camp_consistency, check_speaker_event_camp_consistency,
check_speaker_camp_change, check_speaker_camp_change,
notify_proposal_submitted
) )
m2m_changed.connect( m2m_changed.connect(
check_speaker_event_camp_consistency, check_speaker_event_camp_consistency,
sender=Speaker.events.through sender=Speaker.events.through
) )
pre_save.connect(check_speaker_camp_change, sender=Speaker) 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 .email import add_new_speakerproposal_email, add_new_eventproposal_email
from .models import EventProposal, SpeakerProposal from .models import EventProposal, SpeakerProposal
from ircbot.models import OutgoingIrcMessage
logger = logging.getLogger("bornhack.%s" % __name__) logger = logging.getLogger("bornhack.%s" % __name__)
@ -37,42 +36,3 @@ def check_speaker_camp_change(sender, instance, **kwargs):
if event.camp != instance.camp: 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.'}) 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 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 .email import add_added_membership_email, add_removed_membership_email
from camps.utils import CampPropertyListFilter from camps.utils import CampPropertyListFilter
@ -17,12 +17,12 @@ class TeamTaskAdmin(admin.ModelAdmin):
@admin.register(Team) @admin.register(Team)
class TeamAdmin(admin.ModelAdmin): class TeamAdmin(admin.ModelAdmin):
def get_responsible(self, obj): 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' get_responsible.short_description = 'Responsible'
list_display = [ list_display = [
'name', 'name',
'area', 'camp',
'get_responsible', 'get_responsible',
'needs_members', 'needs_members',
] ]
@ -35,6 +35,7 @@ class TeamAdmin(admin.ModelAdmin):
@admin.register(TeamMember) @admin.register(TeamMember)
class TeamMemberAdmin(admin.ModelAdmin): class TeamMemberAdmin(admin.ModelAdmin):
list_filter = [ list_filter = [
CampPropertyListFilter,
'team', 'team',
'approved', 'approved',
] ]
@ -78,9 +79,3 @@ class TeamMemberAdmin(admin.ModelAdmin):
) )
remove_member.description = 'Remove a user from the team.' 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.apps import AppConfig
from django.db.models.signals import post_save, post_delete
from .signal_handlers import teammember_saved, teammember_deleted
class TeamsConfig(AppConfig): class TeamsConfig(AppConfig):
name = 'teams' 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( return add_outgoing_email(
text_template='emails/new_membership_email.txt', text_template='emails/new_membership_email.txt',
html_template='emails/new_membership_email.html', 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, formatdict=formatdict,
subject='New membership request for {} at {}'.format( subject='New membership request for {} at {}'.format(
membership.team.name, membership.team.name,
membership.team.camp.title 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.dispatch import receiver
from django.utils.text import slugify from django.utils.text import slugify
from utils.models import CampRelatedModel from utils.models import CampRelatedModel
from .email import add_new_membership_email
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -11,123 +10,219 @@ import logging
logger = logging.getLogger("bornhack.%s" % __name__) 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): class Team(CampRelatedModel):
name = models.CharField(max_length=255) camp = models.ForeignKey(
slug = models.SlugField(max_length=255, blank=True) 'camps.Camp',
area = models.ForeignKey( related_name="teams",
'teams.TeamArea', on_delete=models.PROTECT,
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() 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( members = models.ManyToManyField(
'auth.User', 'auth.User',
related_name='teams', related_name='teams',
through='teams.TeamMember' 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: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = (('name', 'camp'), ('slug', 'camp'))
def __str__(self): def __str__(self):
return '{} ({})'.format(self.name, self.camp) 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): def save(self, **kwargs):
if ( # generate slug if needed
not self.pk or if not self.pk or not self.slug:
not self.slug
):
slug = slugify(self.name) slug = slugify(self.name)
self.slug = slug 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) super().save(**kwargs)
def memberstatus(self, member): def clean(self):
if member not in self.members.all(): # make sure the irc channel name is prefixed with a # if it is set
return "Not member" if self.irc_channel_name and self.irc_channel_name[0] != "#":
else: self.irc_channel_name = "#%s" % self.irc_channel_name
if TeamMember.objects.get(team=self, user=member).approved:
return "Member" if self.irc_channel_name:
else: if Team.objects.filter(irc_channel_name=self.irc_channel_name).exclude(pk=self.pk).exists():
return "Membership Pending" raise ValidationError("This IRC channel name is already in use")
@property @property
def responsible(self): def memberships(self):
if TeamMember.objects.filter(team=self, responsible=True).exists(): """
return User.objects.filter( Returns all TeamMember objects for this team.
teammember__team=self, Use self.members.all() to get User objects for all members,
or use self.memberships.all() to get TeamMember objects for all members.
"""
return TeamMember.objects.filter(
team=self
)
@property
def approved_members(self):
"""
Returns only approved members (returns User objects, not TeamMember objects)
"""
return self.members.filter(
teammember__approved=True
)
@property
def unapproved_members(self):
"""
Returns only unapproved members (returns User objects, not TeamMember objects)
"""
return self.members.filter(
teammember__approved=False
)
@property
def responsible_members(self):
"""
Return only approved and responsible members
Used to handle permissions for team management
"""
return self.members.filter(
teammember__approved=True,
teammember__responsible=True teammember__responsible=True
) )
else:
return self.area.responsible.all()
@property @property
def anoncount(self): def regular_members(self):
return self.approvedmembers.filter(user__profile__public_credit_name_approved=False).count() """
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 @property
def approvedmembers(self): def unnamed_members(self):
return TeamMember.objects.filter(team=self, approved=True) """
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): class TeamMember(CampRelatedModel):
user = models.ForeignKey('auth.User', on_delete=models.PROTECT) user = models.ForeignKey(
team = models.ForeignKey('teams.Team', on_delete=models.PROTECT) 'auth.User',
approved = models.BooleanField(default=False) on_delete=models.PROTECT,
responsible = models.BooleanField(default=False) 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): def __str__(self):
return '{} is {} member of team {}'.format( return '{} is {} {} member of team {}'.format(
self.user, '' if self.approved else 'an unapproved', self.team self.user,
'' if self.approved else 'an unapproved',
'' if not self.responsible else 'a responsible',
self.team
) )
@property @property
def camp(self): def camp(self):
""" All CampRelatedModels must have a camp FK or a camp property """
return self.team.camp 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): class TeamTask(CampRelatedModel):
team = models.ForeignKey( team = models.ForeignKey(
'teams.Team', 'teams.Team',
@ -157,14 +252,12 @@ class TeamTask(CampRelatedModel):
@property @property
def camp(self): def camp(self):
""" All CampRelatedModels must have a camp FK or a camp property """
return self.team.camp return self.team.camp
def save(self, **kwargs): def save(self, **kwargs):
# generate slug if needed
if not self.slug: if not self.slug:
self.slug = slugify(self.name) self.slug = slugify(self.name)
super().save(**kwargs) 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

@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% load commonmark %}
{% block title %}
Fix IRC permissions for NickServ user {{ request.user.profile.nickserv_username }} for IRC channel {{ team.irc_channel_name }}
{% endblock %}
{% block content %}
<h3>Fix IRC permissions</h3>
<p class="lead">This will make the bot re-add IRC ACL for your NickServ user <b>{{ request.user.profile.nickserv_username }}</b> for IRC channel <b>{{ team.irc_channel_name }}</b>. Use this in cases where you are unable to join the team IRC channel after entering your NickServ username in your profile.</p>
<form method="POST">
{% csrf_token %}
{{ form }}
<button class="btn btn-success" type="submit"><i class="fa fa-check"></i> Yes Please</button>
<a href="{% url 'teams:detail' camp_slug=team.camp.slug team_slug=team.slug %}" class="btn btn-default" type="submit"><i class="fa fa-remove"></i> Cancel</a>
</form>
{% endblock %}

View file

@ -1,27 +1,47 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load commonmark %} {% load commonmark %}
{% load teams_tags %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load teams_tags %}
{% block title %} {% block title %}
Team: {{ team.name }} | {{ block.super }} Team: {{ team.name }} | {{ block.super }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="panel panel-default"> <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"> <div class="panel-body">
{{ team.description|unsafecommonmark }} {{ 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 %} {% endif %}
<hr> <hr>
<h3>Members</h3> <h4>{{ team.name }} Team Members</h4>
<p>The following <b>{{ team.approvedmembers.count }}</b> people are members of the <b>{{ team.name }} team</b>:</p> <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 class="table table-hover">
<thead> <thead>
<tr> <tr>
<th> <th>
@ -33,40 +53,42 @@ Team: {{ team.name }} | {{ block.super }}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for teammember in team.approvedmembers.all %} {% for teammember in team.memberships.all %}
<tr> <tr>
<td> <td>
{% if teammember.user.profile.approved_public_credit_name %} {{ teammember.user.profile.get_public_credit_name }} {% if teammember.user == request.user %}(this is you!){% endif %}
{{ teammember.user.profile.approved_public_credit_name }}
{% else %}
anonymous
{% endif %}
</td> </td>
<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> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if request.user in team.members.all %} <p>Your membership status: <b>{% membershipstatus user team %}</b></p>
<p>Your membership status: <b>{% membershipstatus request.user team %}</b></p>
{% endif %}
{% if request.user in team.members.all %} {% 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> {% if team.irc_channel and team.irc_channel_managed and request.user.profile.nickserv_username %}
<a href="{% url 'teams:fix_irc_acl' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fa fa-wrench"></i> Fix IRC ACL</a>&nbsp;
{% endif %}
<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 %} {% else %}
{% if team.needs_members %} {% 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 %}
{% 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> <hr>
<h3>Tasks</h3> <h4>{{ team.name }} Team Tasks</h4>
<p>This team is responsible for the following tasks</p> <p>The {{ team.name }} Team is responsible for the following tasks</p>
<table class="table"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@ -80,17 +102,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><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>{{ task.description }}</td>
<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> <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.all %} {% 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">Edit Task</a> <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 %} {% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if request.user in team.responsible.all %} {% 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">Create Task</a> <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 %} {% endif %}
</div> </div>
</div> </div>

View file

@ -7,12 +7,17 @@ Join Team: {{ team.name }} | {{ block.super }}
{% block content %} {% block content %}
<h3>{{ team.name }} Team</h3> <p class="lead">Really join the <b>{{ team.name }}</b> Team for <b>{{ team.camp.title }}</b>?</p>
<p class="lead">Really join the <b>{{ team.name }}</b> team? You will receive a message when your membership has been approved.<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"> <form method="POST">
{% csrf_token %} {% csrf_token %}
{{ form }} {{ form }}
<button class="btn btn-success" type="submit"><i class="fa fa-check"></i> Join {{ team.name }} Team</button> <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> <a href="{% url 'teams:list' camp_slug=camp.slug %}" class="btn btn-default" type="submit"><i class="fa fa-remove"></i> Cancel</a>
</form> </form>
</p>
{% endblock %} {% 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>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>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>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> <p>We currently have {{ teams.count }} teams for {{ camp.title }}:</p>
{% if teams %} {% if teams %}
<table class="table table-hover"> <table class="table table-hover">
@ -42,13 +42,13 @@ Teams | {{ block.super }}
</td> </td>
<td> <td>
{% for resp in team.responsible.all %} {% for resp in team.responsible_members.all %}
{{ resp.profile.approved_public_credit_name|default:"Unnamed" }}{% if not forloop.last %},{% endif %}<br> {{ resp.profile.get_public_credit_name }}{% if not forloop.last %},{% endif %}<br>
{% endfor %} {% endfor %}
</td> </td>
<td class="text-center"> <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 %} {% if team.needs_members %}(more needed){% endif %}
</td> </td>
@ -58,30 +58,23 @@ Teams | {{ block.super }}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<td class="text-center"> <td class="text-center">
{% membershipstatus request.user team as membership_status %} {% membershipstatus request.user team True %}
{% 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 %}
</td> </td>
<td> <td>
<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 %} {% 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> <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 %} {% else %}
{% if team.needs_members %} {% 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> <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 %}
{% endif %} {% endif %}
</div>
{% if request.user in team.responsible.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 %}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View file

@ -1,6 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load commonmark %} {% load commonmark %}
{% load teams_tags %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block title %} {% block title %}
@ -8,25 +7,33 @@ Manage Team: {{ team.name }} | {{ block.super }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h3>Manage {{ team.name }} Team</h3> <div class="panel panel-default">
<form method="post" class="form-horizontal"> <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 %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
{% buttons %} {% buttons %}
<button class="btn btn-primary pull-right" type="submit">Save</button> <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 %} {% endbuttons %}
</form> </form>
</div>
</div>
</div>
<h3>{{ team.name }} Team Members</h3> <div class="panel panel-default">
{% if team.teammember_set.exists %} <div class="panel-heading"><h4>Manage {{ team.name }} Team Members</h4></div>
<table class="table table-bordered table-hover"> <div class="panel-body" style="margin-left: 1em; margin-right: 1em;">
{% if team.teammember_set.exists %}
<table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th> <th>
Profile Username
</th> </th>
<th> <th>
Name Name
@ -65,24 +72,27 @@ Manage Team: {{ team.name }} | {{ block.super }}
</td> </td>
<td> <td>
{{ membership.user.profile.public_credit_name|default:"N/A" }} {{ 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>
<td> <td>
{% if membership.approved %}member{% else %}pending{% endif %} {% if membership.approved %}member{% else %}pending{% endif %}
</td> </td>
<td> <td>
{% if membership.approved %} <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</a> <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>
{% else %} {% if not 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> <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>
<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 %} {% endif %}
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<p>No members found!</p> <p>No members found!</p>
{% endif %} {% endif %}
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,8 +1,24 @@
from django import template from django import template
from django.utils.safestring import mark_safe
register = template.Library() register = template.Library()
@register.simple_tag @register.simple_tag
def membershipstatus(user, team): def membershipstatus(user, team, showicon=False):
return team.memberstatus(user) 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

@ -46,6 +46,11 @@ urlpatterns = [
TeamManageView.as_view(), TeamManageView.as_view(),
name='manage' name='manage'
), ),
url(
r'^fix_irc_acl/$',
FixIrcAclView.as_view(),
name='fix_irc_acl',
),
url( url(
r'^tasks/', include([ r'^tasks/', include([
url( url(

View file

@ -9,7 +9,7 @@ from django.contrib import messages
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.conf import settings
from profiles.models import Profile from profiles.models import Profile
import logging import logging
@ -17,9 +17,12 @@ logger = logging.getLogger("bornhack.%s" % __name__)
class EnsureTeamResponsibleMixin(object): 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): def dispatch(self, request, *args, **kwargs):
self.team = Team.objects.get(slug=kwargs['team_slug'], camp=self.camp) 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') messages.error(request, 'No thanks')
return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=self.team.slug) return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=self.team.slug)
@ -28,6 +31,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): class TeamListView(CampViewMixin, ListView):
template_name = "team_list.html" template_name = "team_list.html"
model = Team model = Team
@ -40,16 +59,26 @@ class TeamDetailView(CampViewMixin, DetailView):
model = Team model = Team
slug_url_kwarg = 'team_slug' 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): class TeamManageView(CampViewMixin, EnsureTeamResponsibleMixin, UpdateView):
model = Team model = Team
template_name = "team_manage.html" 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' slug_url_kwarg = 'team_slug'
def get_success_url(self): def get_success_url(self):
return reverse_lazy('teams:detail', kwargs={'camp_slug': self.camp.slug, 'team_slug': self.get_object().slug}) 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): class TeamJoinView(LoginRequiredMixin, CampViewMixin, UpdateView):
template_name = "team_join.html" template_name = "team_join.html"
@ -100,18 +129,6 @@ class TeamLeaveView(LoginRequiredMixin, CampViewMixin, UpdateView):
return redirect('teams:list', camp_slug=self.get_object().camp.slug) 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): class TeamMemberRemoveView(LoginRequiredMixin, CampViewMixin, EnsureTeamMemberResponsibleMixin, UpdateView):
template_name = "teammember_remove.html" template_name = "teammember_remove.html"
@ -197,3 +214,66 @@ class TaskUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMix
def get_success_url(self): def get_success_url(self):
return self.get_object().get_absolute_url() return self.get_object().get_absolute_url()
class FixIrcAclView(LoginRequiredMixin, CampViewMixin, UpdateView):
template_name = "fix_irc_acl.html"
model = Team
fields = []
slug_url_kwarg = 'team_slug'
def dispatch(self, request, *args, **kwargs):
# we need to call the super().dispatch() method early so self.camp gets populated by CampViewMixin,
# because the lookups below depend on self.camp being set :)
response = super().dispatch(
request, *args, **kwargs
)
# check if the logged in user has an approved membership of this team
if request.user not in self.get_object().approved_members.all():
messages.error(request, 'No thanks')
return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug)
# check if we manage the channel for this team
if not self.get_object().irc_channel or not self.get_object().irc_channel_managed:
messages.error(request, 'IRC functionality is disabled for this team, or the team channel is not managed by the bot')
return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug)
# check if user has a nickserv username
if not request.user.profile.nickserv_username:
messages.error(request, 'Please go to your profile and set your NickServ username first. Make sure the account is registered with NickServ first!')
return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug)
return response
def get(self, request, *args, **kwargs):
# get membership
try:
TeamMember.objects.get(
user=request.user,
team=self.get_object(),
approved=True,
irc_channel_acl_ok=True
)
except TeamMember.DoesNotExist:
# this membership is already marked as membership.irc_channel_acl_ok=False, no need to do anything
messages.error(request, 'No need, this membership is already marked as irc_channel_acl_ok=False, so the bot will fix the ACL soon')
return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug)
return super().get(
request, *args, **kwargs
)
def form_valid(self, form):
membership = TeamMember.objects.get(
user=self.request.user,
team=self.get_object(),
approved=True,
irc_channel_acl_ok=True
)
membership.irc_channel_acl_ok = False
membership.save()
messages.success(self.request, "OK, hang on while we fix the permissions for your NickServ user '%s' for IRC channel '%s'" % (self.request.user.profile.nickserv_username, form.instance.irc_channel_name))
return redirect('teams:detail', camp_slug=form.instance.camp.slug, team_slug=form.instance.slug)

View file

@ -74,6 +74,7 @@
<li><a href="{% url 'people' %}">People</a></li> <li><a href="{% url 'people' %}">People</a></li>
{% if user.is_authenticated and user.is_staff %} {% if user.is_authenticated and user.is_staff %}
<li><a href="{% url 'backoffice:index' %}">Backoffice</a></li> <li><a href="{% url 'backoffice:index' %}">Backoffice</a></li>
<li><a href="{% url 'admin:index' %}">Django Admin</a></li>
{% endif %} {% endif %}
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">

View file

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

View file

@ -23,9 +23,12 @@ from tickets.models import (
from teams.models import ( from teams.models import (
Team, Team,
TeamTask, TeamTask,
TeamArea,
TeamMember TeamMember
) )
from events.models import (
Type,
Routing
)
from django.contrib.auth.models import User from django.contrib.auth.models import User
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress
from django.utils.text import slugify from django.utils.text import slugify
@ -44,6 +47,7 @@ class Command(BaseCommand):
title='BornHack 2016', title='BornHack 2016',
tagline='Initial Commit', tagline='Initial Commit',
slug='bornhack-2016', slug='bornhack-2016',
shortslug='bh2016',
buildup=( buildup=(
timezone.datetime(2016, 8, 25, 12, 0, tzinfo=timezone.utc), timezone.datetime(2016, 8, 25, 12, 0, tzinfo=timezone.utc),
timezone.datetime(2016, 8, 27, 11, 59, tzinfo=timezone.utc), timezone.datetime(2016, 8, 27, 11, 59, tzinfo=timezone.utc),
@ -63,6 +67,7 @@ class Command(BaseCommand):
title='BornHack 2017', title='BornHack 2017',
tagline='Make Tradition', tagline='Make Tradition',
slug='bornhack-2017', slug='bornhack-2017',
shortslug='bh2017',
buildup=( buildup=(
timezone.datetime(2017, 8, 25, 12, 0, tzinfo=timezone.utc), timezone.datetime(2017, 8, 25, 12, 0, tzinfo=timezone.utc),
timezone.datetime(2017, 8, 27, 11, 59, tzinfo=timezone.utc), timezone.datetime(2017, 8, 27, 11, 59, tzinfo=timezone.utc),
@ -82,6 +87,7 @@ class Command(BaseCommand):
title='BornHack 2018', title='BornHack 2018',
tagline='Undecided', tagline='Undecided',
slug='bornhack-2018', slug='bornhack-2018',
shortslug='bh2018',
buildup=( buildup=(
timezone.datetime(2018, 8, 25, 12, 0, tzinfo=timezone.utc), timezone.datetime(2018, 8, 25, 12, 0, tzinfo=timezone.utc),
timezone.datetime(2018, 8, 27, 11, 59, 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.name = 'John Doe'
user1.profile.description = 'one that once was' user1.profile.description = 'one that once was'
user1.profile.public_credit_name = 'PublicDoe'
user1.profile.public_credit_name_approved = True
user1.profile.save() user1.profile.save()
email = EmailAddress.objects.create( email = EmailAddress.objects.create(
user=user1, user=user1,
@ -112,6 +120,7 @@ class Command(BaseCommand):
verified=True verified=True
) )
email.set_as_primary() email.set_as_primary()
user2 = User.objects.create_user( user2 = User.objects.create_user(
username='user2', username='user2',
password='user2', password='user2',
@ -127,6 +136,7 @@ class Command(BaseCommand):
verified=True verified=True
) )
email.set_as_primary() email.set_as_primary()
user3 = User.objects.create_user( user3 = User.objects.create_user(
username='user3', username='user3',
password='user3', password='user3',
@ -134,6 +144,7 @@ class Command(BaseCommand):
) )
user3.profile.name = 'Lorem Ipsum' user3.profile.name = 'Lorem Ipsum'
user3.profile.description = 'just a user' user3.profile.description = 'just a user'
user3.profile.public_credit_name = 'Lorem Ipsum'
user3.profile.save() user3.profile.save()
email = EmailAddress.objects.create( email = EmailAddress.objects.create(
user=user3, user=user3,
@ -142,6 +153,7 @@ class Command(BaseCommand):
verified=True verified=True
) )
email.set_as_primary() email.set_as_primary()
user4 = User.objects.create_user( user4 = User.objects.create_user(
username='user4', username='user4',
password='user4', password='user4',
@ -149,6 +161,8 @@ class Command(BaseCommand):
) )
user4.profile.name = 'Ethe Reum' user4.profile.name = 'Ethe Reum'
user4.profile.description = 'I prefer doge' user4.profile.description = 'I prefer doge'
user4.profile.public_credit_name = 'Dogefan'
user4.profile.public_credit_name_approved = True
user4.profile.save() user4.profile.save()
email = EmailAddress.objects.create( email = EmailAddress.objects.create(
user=user4, user=user4,
@ -157,6 +171,96 @@ class Command(BaseCommand):
verified=True verified=True
) )
email.set_as_primary() 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( admin = User.objects.create_superuser(
username='admin', username='admin',
email='admin@example.com', 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' 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)) 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( noc_team = Team.objects.create(
name="NOC", name="NOC",
description="The NOC team is in charge of establishing and running a network onsite.".format(year), description="The NOC team is in charge of establishing and running a network onsite.",
area=infrastructure_area, camp=camp
) )
bar_team = Team.objects.create( bar_team = Team.objects.create(
name="Bar", name="Bar",
description="The Bar team plans, builds and run the IRL bar!", description="The Bar team plans, builds and run the IRL bar!",
area=bar_area camp=camp
) )
self.output("Creating TeamTasks for {}...".format(year)) self.output("Creating TeamTasks for {}...".format(year))
@ -1461,28 +1542,92 @@ Please note that sleeping in the parking lot is not permitted. If you want to sl
) )
self.output("Setting team members for {}...".format(year)) self.output("Setting team members for {}...".format(year))
# noc team
TeamMember.objects.create( TeamMember.objects.create(
team=noc_team, team=noc_team,
user=user4, user=user4,
approved=True approved=True,
responsible=True
) )
TeamMember.objects.create( TeamMember.objects.create(
team=noc_team, 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( TeamMember.objects.create(
team=bar_team, team=bar_team,
user=user1, user=user1,
approved=True approved=True,
responsible=True
) )
TeamMember.objects.create( TeamMember.objects.create(
team=bar_team, team=bar_team,
user=user3, user=user3,
approved=True approved=True,
responsible=True
) )
TeamMember.objects.create( TeamMember.objects.create(
team=bar_team, 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")
) )