Merge teamcomms branch. Refactor team app and add events app.
* Primary commit towards improved team communications. Add new events app to handle team notifications when various events happen, with a Type model which contain event types and a Routing model which controls routing of events to teams. Add shortslug for Camp and Team models. events.handler.py contains the code for sending irc and email notifications for teams. The first two eventtypes have been added in datamigrations, 'ticket_created' and 'public_credit_name_changed', and the tickets and profile apps have been adjusted accordingly. Team IRC channels can be marked as managed and if so the IRC bot will register the team channel with ChanServ if possible. Team IRC channels can be marked as private and the bot will set invite only and maintain an ACL with team members. Users can set their NickServ username in their profile to get on the ACL. Rework all team views and templates. Remove TeamArea model and make Team have an FK to Camp directly. Add docstrings a whole bunch of places. Move signal handlers to apps.py and signal_handlers.py in a few apps. Add basic team mailing list handling, more work to be done. Update bootstrap-devsite script to add more teammembers and add some team event routing for the two eventtypes we have. * default to the console backend for email unless we specifically ask for realworld email * fix signal for public_credit_name approval irc message * fix name display on /people/ page * fix the text on people pages when all non-responsible team members are anonymous * handle cases where we fallback to the area responsible properly * readd removed property, it is used in team_detail view * make it possible to filter profiles by public_credit_name_approved * add method for sending IRC messages in ircbot.utils.add_irc_message(), extend periodic bot method to do more than check for outgoing messages so rename it, refactor chanserv and nickserv handling code, create methods to check and join/part IRC channels as needed, maintain channel ACLs for private channels, do not autojoin any channels when instatiating the bot instead rely on the new check_irc_channels() method to join them, rename profile presave signal, add checking for changed nickserv usernames for acl handling, add teammember.irc_channel_acl_ok boolean to track ACL state, add missing help_text properties to TeamMember fields, rename teammember postsave signal, add teammember deleted signal, readd wrongly deleted EnsureTeamMemberResponsibleMixin * add a few missing early returns
This commit is contained in:
parent
03fc20a459
commit
edcf363027
69
scripts/schemagif.sh
Executable file
69
scripts/schemagif.sh
Executable 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
|
||||||
|
|
|
@ -36,6 +36,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 }}'
|
||||||
|
@ -76,13 +79,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 }}'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ INSTALLED_APPS = [
|
||||||
'tickets',
|
'tickets',
|
||||||
'bar',
|
'bar',
|
||||||
'backoffice',
|
'backoffice',
|
||||||
|
'events',
|
||||||
|
|
||||||
'allauth',
|
'allauth',
|
||||||
'allauth.account',
|
'allauth.account',
|
||||||
|
|
20
src/camps/migrations/0023_camp_shortslug.py
Normal file
20
src/camps/migrations/0023_camp_shortslug.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
22
src/camps/migrations/0024_populate_camp_shortslugs.py
Normal file
22
src/camps/migrations/0024_populate_camp_shortslugs.py
Normal 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),
|
||||||
|
]
|
||||||
|
|
20
src/camps/migrations/0025_auto_20180318_1250.py
Normal file
20
src/camps/migrations/0025_auto_20180318_1250.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
0
src/events/__init__.py
Normal file
12
src/events/admin.py
Normal file
12
src/events/admin.py
Normal 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
5
src/events/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class EventsConfig(AppConfig):
|
||||||
|
name = 'events'
|
81
src/events/handler.py
Normal file
81
src/events/handler.py
Normal 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
|
||||||
|
|
51
src/events/migrations/0001_initial.py
Normal file
51
src/events/migrations/0001_initial.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
20
src/events/migrations/0002_create_eventtype.py
Normal file
20
src/events/migrations/0002_create_eventtype.py
Normal 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),
|
||||||
|
]
|
||||||
|
|
20
src/events/migrations/0003_create_another_eventtype.py
Normal file
20
src/events/migrations/0003_create_another_eventtype.py
Normal 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),
|
||||||
|
]
|
25
src/events/migrations/0004_auto_20180403_1228.py
Normal file
25
src/events/migrations/0004_auto_20180403_1228.py
Normal 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.'),
|
||||||
|
),
|
||||||
|
]
|
0
src/events/migrations/__init__.py
Normal file
0
src/events/migrations/__init__.py
Normal file
61
src/events/models.py
Normal file
61
src/events/models.py
Normal 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
3
src/events/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
src/events/views.py
Normal file
3
src/events/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
|
@ -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,198 @@ 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 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'])
|
||||||
|
|
||||||
|
|
|
@ -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
21
src/ircbot/utils.py
Normal 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),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
default_app_config = 'profiles.apps.ProfilesConfig'
|
||||||
|
|
|
@ -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
16
src/profiles/apps.py
Normal 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')
|
||||||
|
|
20
src/profiles/migrations/0008_auto_20180325_2022.py
Normal file
20
src/profiles/migrations/0008_auto_20180325_2022.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
20
src/profiles/migrations/0009_profile_nickserv_username.py
Normal file
20
src/profiles/migrations/0009_profile_nickserv_username.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,7 +37,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(
|
||||||
|
@ -50,6 +45,12 @@ class Profile(CreatedUpdatedModel, UUIDModel):
|
||||||
help_text='Check this box to approve this users public_credit_name. This will be unchecked automatically when the user edits public_credit_name'
|
help_text='Check this box to approve this users public_credit_name. This will be unchecked automatically when the user edits public_credit_name'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
nickserv_username = models.CharField(
|
||||||
|
blank=True,
|
||||||
|
max_length=50,
|
||||||
|
help_text='Your NickServ username is used to manage team IRC channel access lists.',
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email(self):
|
def email(self):
|
||||||
return self.user.email
|
return self.user.email
|
||||||
|
@ -58,37 +59,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)
|
|
||||||
)
|
|
||||||
|
|
81
src/profiles/signal_handlers.py
Normal file
81
src/profiles/signal_handlers.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
from django.db.models.signals import (
|
||||||
|
post_save,
|
||||||
|
pre_save
|
||||||
|
)
|
||||||
|
from events.handler import handle_team_event
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_profile(sender, created, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Signal handler called after a User object is saved.
|
||||||
|
Creates a Profile object when the User object was just created.
|
||||||
|
"""
|
||||||
|
from .models import Profile
|
||||||
|
if created:
|
||||||
|
Profile.objects.create(user=instance)
|
||||||
|
|
||||||
|
|
||||||
|
def profile_pre_save(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Signal handler called before a Profile object is saved.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
original = sender.objects.get(pk=instance.pk)
|
||||||
|
except sender.DoesNotExist:
|
||||||
|
original = None
|
||||||
|
logger.debug("inside profile_pre_save with instance.nickserv_username=%s and original.nickserv_username=%s" % (instance.nickserv_username, original.nickserv_username))
|
||||||
|
|
||||||
|
public_credit_name_changed(instance, original)
|
||||||
|
nickserv_username_changed(instance, original)
|
||||||
|
|
||||||
|
|
||||||
|
def public_credit_name_changed(instance, original):
|
||||||
|
"""
|
||||||
|
Checks if a users public_credit_name has been changed, and triggers a public_credit_name_changed event if so
|
||||||
|
"""
|
||||||
|
if original.public_credit_name == instance.public_credit_name:
|
||||||
|
# public_credit_name has not been changed
|
||||||
|
return
|
||||||
|
|
||||||
|
if original.public_credit_name and not original.public_credit_name_approved:
|
||||||
|
# the original.public_credit_name was not approved, no need to notify again
|
||||||
|
return
|
||||||
|
|
||||||
|
# put the message together
|
||||||
|
message='User {username} changed public credit name. please review and act accordingly: https://bornhack.dk/admin/profiles/profile/{uuid}/change/'.format(
|
||||||
|
username=instance.name,
|
||||||
|
uuid=instance.uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
# trigger the event
|
||||||
|
handle_team_event(
|
||||||
|
eventtype='public_credit_name_changed',
|
||||||
|
irc_message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def nickserv_username_changed(instance, original):
|
||||||
|
"""
|
||||||
|
Check if profile.nickserv_username was changed, and uncheck irc_channel_acl_ok if so
|
||||||
|
This will be picked up by the IRC bot and fixed as needed
|
||||||
|
"""
|
||||||
|
if instance.nickserv_username and instance.nickserv_username != original.nickserv_username:
|
||||||
|
logger.debug("profile.nickserv_username changed for user %s, setting irc_channel_acl_ok=False" % instance.user.username)
|
||||||
|
|
||||||
|
# find team memberships for this user
|
||||||
|
from teams.models import TeamMember
|
||||||
|
memberships = TeamMember.objects.filter(
|
||||||
|
user=instance.user,
|
||||||
|
approved=True,
|
||||||
|
team__irc_channel=True,
|
||||||
|
team__irc_channel_managed=True,
|
||||||
|
team__irc_channel_private=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# loop over memberships
|
||||||
|
for membership in memberships:
|
||||||
|
membership.irc_channel_acl_ok = False
|
||||||
|
membership.save()
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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)
|
|
||||||
)
|
|
||||||
|
|
|
@ -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',
|
||||||
]
|
]
|
||||||
|
@ -78,9 +78,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'
|
|
||||||
]
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.forms import ModelForm
|
|
||||||
from .models import Team
|
|
||||||
|
|
||||||
|
|
||||||
class ManageTeamForm(ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Team
|
|
||||||
fields = ['description', 'needs_members']
|
|
35
src/teams/migrations/0022_auto_20180318_1135.py
Normal file
35
src/teams/migrations/0022_auto_20180318_1135.py
Normal 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.'),
|
||||||
|
),
|
||||||
|
]
|
30
src/teams/migrations/0023_auto_20180318_1256.py
Normal file
30
src/teams/migrations/0023_auto_20180318_1256.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
22
src/teams/migrations/0024_populate_shortslugs.py
Normal file
22
src/teams/migrations/0024_populate_shortslugs.py
Normal 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),
|
||||||
|
]
|
||||||
|
|
20
src/teams/migrations/0025_auto_20180318_1318.py
Normal file
20
src/teams/migrations/0025_auto_20180318_1318.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
22
src/teams/migrations/0026_team_camp.py
Normal file
22
src/teams/migrations/0026_team_camp.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
60
src/teams/migrations/0027_fixup_teams.py
Normal file
60
src/teams/migrations/0027_fixup_teams.py
Normal 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),
|
||||||
|
]
|
||||||
|
|
21
src/teams/migrations/0028_auto_20180331_1416.py
Normal file
21
src/teams/migrations/0028_auto_20180331_1416.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
19
src/teams/migrations/0029_remove_team_area.py
Normal file
19
src/teams/migrations/0029_remove_team_area.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
30
src/teams/migrations/0030_auto_20180402_1514.py
Normal file
30
src/teams/migrations/0030_auto_20180402_1514.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
25
src/teams/migrations/0031_auto_20180402_2146.py
Normal file
25
src/teams/migrations/0031_auto_20180402_2146.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
21
src/teams/migrations/0032_auto_20180402_2148.py
Normal file
21
src/teams/migrations/0032_auto_20180402_2148.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
30
src/teams/migrations/0033_auto_20180402_2204.py
Normal file
30
src/teams/migrations/0033_auto_20180402_2204.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
19
src/teams/migrations/0034_auto_20180402_2334.py
Normal file
19
src/teams/migrations/0034_auto_20180402_2334.py
Normal 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']},
|
||||||
|
),
|
||||||
|
]
|
19
src/teams/migrations/0035_auto_20180402_2344.py
Normal file
19
src/teams/migrations/0035_auto_20180402_2344.py
Normal 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']},
|
||||||
|
),
|
||||||
|
]
|
20
src/teams/migrations/0036_auto_20180403_0201.py
Normal file
20
src/teams/migrations/0036_auto_20180403_0201.py
Normal 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')]),
|
||||||
|
),
|
||||||
|
]
|
42
src/teams/migrations/0037_auto_20180408_1416.py
Normal file
42
src/teams/migrations/0037_auto_20180408_1416.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers 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,
|
||||||
teammember__responsible=True
|
or use self.memberships.all() to get TeamMember objects for all members.
|
||||||
)
|
"""
|
||||||
else:
|
return TeamMember.objects.filter(
|
||||||
return self.area.responsible.all()
|
team=self
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def anoncount(self):
|
def approved_members(self):
|
||||||
return self.approvedmembers.filter(user__profile__public_credit_name_approved=False).count()
|
"""
|
||||||
|
Returns only approved members (returns User objects, not TeamMember objects)
|
||||||
|
"""
|
||||||
|
return self.members.filter(
|
||||||
|
teammember__approved=True
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def approvedmembers(self):
|
def unapproved_members(self):
|
||||||
return TeamMember.objects.filter(team=self, approved=True)
|
"""
|
||||||
|
Returns only unapproved members (returns User objects, not TeamMember objects)
|
||||||
|
"""
|
||||||
|
return self.members.filter(
|
||||||
|
teammember__approved=False
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def responsible_members(self):
|
||||||
|
"""
|
||||||
|
Return only approved and responsible members
|
||||||
|
Used to handle permissions for team management
|
||||||
|
"""
|
||||||
|
return self.members.filter(
|
||||||
|
teammember__approved=True,
|
||||||
|
teammember__responsible=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def regular_members(self):
|
||||||
|
"""
|
||||||
|
Return only approved and not responsible members with
|
||||||
|
an approved public_credit_name.
|
||||||
|
Used on the people pages.
|
||||||
|
"""
|
||||||
|
return self.members.filter(
|
||||||
|
teammember__approved=True,
|
||||||
|
teammember__responsible=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unnamed_members(self):
|
||||||
|
"""
|
||||||
|
Returns only approved and not responsible members,
|
||||||
|
without an approved public_credit_name.
|
||||||
|
"""
|
||||||
|
return self.members.filter(
|
||||||
|
teammember__approved=True,
|
||||||
|
teammember__responsible=False,
|
||||||
|
profile__public_credit_name_approved=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TeamMember(CampRelatedModel):
|
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()
|
|
||||||
|
|
||||||
|
|
31
src/teams/signal_handlers.py
Normal file
31
src/teams/signal_handlers.py
Normal 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))
|
||||||
|
|
|
@ -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,39 @@ 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>
|
<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 +99,17 @@ Team: {{ team.name }} | {{ block.super }}
|
||||||
<td><a href="{% url 'teams:task_detail' slug=task.slug camp_slug=camp.slug team_slug=team.slug %}">{{ task.name }}</a></td>
|
<td><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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
{% if request.user in team.members.all %}
|
<div class="btn-group-vertical">
|
||||||
<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 class="btn btn-primary" href="{% url 'teams:detail' camp_slug=camp.slug team_slug=team.slug %}"><i class="fa fa-search"></i> Details</a>
|
||||||
{% else %}
|
{% if request.user in team.responsible_members.all %}
|
||||||
{% if team.needs_members %}
|
|
||||||
<a href="{% url 'teams:join' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-success"><i class="fa fa-plus"></i> Join</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if request.user in team.responsible.all %}
|
|
||||||
<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>
|
<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 %}
|
||||||
|
{% if request.user in team.members.all %}
|
||||||
|
<a href="{% url 'teams:leave' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-danger"><i class="fa fa-remove"></i> Leave</a>
|
||||||
|
{% else %}
|
||||||
|
{% if team.needs_members %}
|
||||||
|
<a href="{% url 'teams:join' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-success"><i class="fa fa-plus"></i> Join</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -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,81 +7,92 @@ 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>
|
||||||
{% csrf_token %}
|
<div class="panel-body" style="margin-left: 1em; margin-right: 1em;">
|
||||||
|
<div class="form-group">
|
||||||
|
<form method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
{% bootstrap_form form %}
|
{% bootstrap_form form %}
|
||||||
|
|
||||||
|
{% buttons %}
|
||||||
|
<button class="btn btn-success pull-right" type="submit"><i class="fa fa-check"></i> Save Team</button>
|
||||||
|
<a class="btn btn-primary pull-right" href="{% url 'teams:detail' team_slug=team.slug camp_slug=camp.slug %}"><i class="fa fa-remove"></i> Cancel</a>
|
||||||
|
{% endbuttons %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% buttons %}
|
<div class="panel panel-default">
|
||||||
<button class="btn btn-primary pull-right" type="submit">Save</button>
|
<div class="panel-heading"><h4>Manage {{ team.name }} Team Members</h4></div>
|
||||||
{% endbuttons %}
|
<div class="panel-body" style="margin-left: 1em; margin-right: 1em;">
|
||||||
</form>
|
{% if team.teammember_set.exists %}
|
||||||
|
<table class="table table-hover">
|
||||||
<h3>{{ team.name }} Team Members</h3>
|
<thead>
|
||||||
{% if team.teammember_set.exists %}
|
<tr>
|
||||||
<table class="table table-bordered table-hover">
|
<th>
|
||||||
<thead>
|
Username
|
||||||
<tr>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
Profile
|
Name
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
Name
|
Email
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
Email
|
Description
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
Description
|
Public Credit Name
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
Public Credit Name
|
Membership
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
Membership
|
Action
|
||||||
</th>
|
</th>
|
||||||
<th>
|
</tr>
|
||||||
Action
|
</thead>
|
||||||
</th>
|
<tbody>
|
||||||
</tr>
|
{% for membership in team.teammember_set.all %}
|
||||||
</thead>
|
<tr>
|
||||||
<tbody>
|
<td>
|
||||||
{% for membership in team.teammember_set.all %}
|
{{ membership.user }}
|
||||||
<tr>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ membership.user }}
|
{{ membership.user.profile.name }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ membership.user.profile.name }}
|
{{ membership.user.profile.email }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ membership.user.profile.email }}
|
{{ membership.user.profile.description }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ membership.user.profile.description }}
|
{{ membership.user.profile.public_credit_name|default:"N/A" }}
|
||||||
</td>
|
{% 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>
|
||||||
{{ membership.user.profile.public_credit_name|default:"N/A" }}
|
<td>
|
||||||
</td>
|
{% if membership.approved %}member{% else %}pending{% endif %}
|
||||||
<td>
|
</td>
|
||||||
{% if membership.approved %}member{% else %}pending{% endif %}
|
<td>
|
||||||
</td>
|
<div class="btn-group-vertical">
|
||||||
<td>
|
<a class="btn btn-danger" href="{% url 'teams:teammember_remove' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-trash-o"></i> Remove Member</a>
|
||||||
{% if membership.approved %}
|
{% 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>
|
||||||
{% else %}
|
{% endif %}
|
||||||
<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>
|
</div>
|
||||||
<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>
|
</td>
|
||||||
{% endif %}
|
</tr>
|
||||||
</td>
|
{% endfor %}
|
||||||
</tr>
|
</tbody>
|
||||||
{% endfor %}
|
</table>
|
||||||
</tbody>
|
{% else %}
|
||||||
</table>
|
<p>No members found!</p>
|
||||||
{% else %}
|
{% endif %}
|
||||||
<p>No members found!</p>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -9,6 +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.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from profiles.models import Profile
|
from profiles.models import Profile
|
||||||
|
|
||||||
|
@ -17,9 +18,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 +32,22 @@ class EnsureTeamResponsibleMixin(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EnsureTeamMemberResponsibleMixin(SingleObjectMixin):
|
||||||
|
"""
|
||||||
|
Use to make sure request.user is responsible for the team which TeamMember belongs to
|
||||||
|
"""
|
||||||
|
model = TeamMember
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if request.user not in self.get_object().team.responsible_members.all():
|
||||||
|
messages.error(request, 'No thanks')
|
||||||
|
return redirect('teams:detail', camp_slug=self.get_object().team.camp.slug, team_slug=self.get_object().team.slug)
|
||||||
|
|
||||||
|
return super().dispatch(
|
||||||
|
request, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TeamListView(CampViewMixin, ListView):
|
class TeamListView(CampViewMixin, ListView):
|
||||||
template_name = "team_list.html"
|
template_name = "team_list.html"
|
||||||
model = Team
|
model = Team
|
||||||
|
@ -40,16 +60,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 +130,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"
|
||||||
|
|
|
@ -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)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,29 +1542,93 @@ Please note that sleeping in the parking lot is not permitted. If you want to sl
|
||||||
)
|
)
|
||||||
|
|
||||||
self.output("Setting team members for {}...".format(year))
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
self.output("marking 2016 as read_only...")
|
self.output("marking 2016 as read_only...")
|
||||||
|
|
Loading…
Reference in a new issue