Merge branch 'master' into django-and-channels-upgrade
This commit is contained in:
commit
4d6caf6947
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
|
||||
|
|
@ -37,6 +37,9 @@ CAMP_REDIRECT_PERCENT=25
|
|||
### changes below here are only needed for production
|
||||
|
||||
# email settings
|
||||
{% if not django_email_realworld | default(False) %}
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
{% endif %}
|
||||
EMAIL_HOST='{{ django_email_host }}'
|
||||
EMAIL_PORT={{ django_email_port }}
|
||||
EMAIL_HOST_USER='{{ django_email_user }}'
|
||||
|
@ -77,13 +80,13 @@ SCHEDULE_EVENT_NOTIFICATION_MINUTES=10
|
|||
# irc bot settings
|
||||
IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS=10
|
||||
IRCBOT_NICK='{{ django_ircbot_nickname }}'
|
||||
IRCBOT_CHANSERV_MASK='{{ django_ircbot_chanserv_mask }}'
|
||||
IRCBOT_NICKSERV_MASK='{{ django_ircbot_nickserv_mask }}'
|
||||
IRCBOT_NICKSERV_PASSWORD='{{ django_ircbot_nickserv_password }}'
|
||||
IRCBOT_NICKSERV_EMAIL='{{ django_ircbot_nickserv_email }}'
|
||||
IRCBOT_NICKSERV_IDENTIFY_STRING="This nickname is registered. Please choose a different nickname, or identify via \x02/msg NickServ identify <password>\x02."
|
||||
IRCBOT_SERVER_HOSTNAME='{{ django_ircbot_server }}'
|
||||
IRCBOT_SERVER_PORT=6697
|
||||
IRCBOT_SERVER_USETLS=True
|
||||
IRCBOT_CHANNELS={
|
||||
'default': '{{ django_ircbot_default_channel }}',
|
||||
'orga': '{{ django_ircbot_orga_channel }}',
|
||||
'public': '{{ django_ircbot_public_channel }}'
|
||||
}
|
||||
IRCBOT_PUBLIC_CHANNEL='{{ django_ircbot_public_channel }}'
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ INSTALLED_APPS = [
|
|||
'tickets',
|
||||
'bar',
|
||||
'backoffice',
|
||||
'events',
|
||||
|
||||
'allauth',
|
||||
'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'
|
||||
)
|
||||
|
||||
shortslug = models.SlugField(
|
||||
verbose_name='Short Slug',
|
||||
help_text='Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.',
|
||||
)
|
||||
|
||||
buildup = DateTimeRangeField(
|
||||
verbose_name='Buildup Period',
|
||||
help_text='The camp buildup period.',
|
||||
|
@ -189,9 +194,3 @@ class Camp(CreatedUpdatedModel, UUIDModel):
|
|||
else:
|
||||
return True
|
||||
|
||||
@property
|
||||
def teams(self):
|
||||
""" Return a queryset with all teams under all TeamAreas under this Camp """
|
||||
from teams.models import Team
|
||||
return Team.objects.filter(area__in=self.teamareas.all())
|
||||
|
||||
|
|
0
src/events/__init__.py
Normal file
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 teams.models import Team, TeamMember
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from events.models import Routing
|
||||
import logging
|
||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||
|
||||
|
@ -26,6 +28,12 @@ class Plugin(object):
|
|||
"""triggered after the server sent the MOTD (require core plugin)"""
|
||||
logger.debug("inside server_ready(), kwargs: %s" % kwargs)
|
||||
|
||||
logger.info("Identifying with %s" % settings.IRCBOT_NICKSERV_MASK)
|
||||
self.bot.privmsg(settings.IRCBOT_NICKSERV_MASK, "identify %s %s" % (settings.IRCBOT_NICK, settings.IRCBOT_NICKSERV_PASSWORD))
|
||||
|
||||
logger.info("Calling self.bot.do_stuff() in %s seconds.." % settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS)
|
||||
self.bot.loop.call_later(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS, self.bot.do_stuff)
|
||||
|
||||
|
||||
def connection_lost(self, **kwargs):
|
||||
"""triggered when connection is lost"""
|
||||
|
@ -36,9 +44,6 @@ class Plugin(object):
|
|||
"""triggered when connection is up"""
|
||||
logger.debug("inside connection_made(), kwargs: %s" % kwargs)
|
||||
|
||||
# wait 5 secs before starting the loop to check for outgoing messages
|
||||
self.bot.loop.call_later(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS, self.bot.get_outgoing_messages)
|
||||
|
||||
|
||||
###############################################################################################
|
||||
### decorated irc3 event methods
|
||||
|
@ -48,16 +53,38 @@ class Plugin(object):
|
|||
"""triggered when there is a join part or quit on a channel the bot is in"""
|
||||
logger.debug("inside on_join_part_quit(), kwargs: %s" % kwargs)
|
||||
|
||||
# TODO: on part or quit check if the bot is the only remaining member of a channel,
|
||||
# if so, check if the channel should be managed, and if so, part and join the channel
|
||||
# to gain @ and register with ChanServ
|
||||
|
||||
|
||||
@irc3.event(irc3.rfc.JOIN)
|
||||
def on_join(self, mask, channel, **kwargs):
|
||||
"""Triggered when a channel is joined by someone, including the bot itself"""
|
||||
if mask.nick == self.bot.nick:
|
||||
# the bot just joined a channel
|
||||
if channel in self.get_managed_team_channels():
|
||||
logger.debug("Just joined a channel I am supposed to be managing, asking ChanServ for info about %s" % channel)
|
||||
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "info %s" % channel)
|
||||
return
|
||||
|
||||
|
||||
@irc3.event(irc3.rfc.PRIVMSG)
|
||||
def on_privmsg(self, **kwargs):
|
||||
"""triggered when a privmsg is sent to the bot or to a channel the bot is in"""
|
||||
logger.debug("inside on_privmsg(), kwargs: %s" % kwargs)
|
||||
|
||||
# nickserv
|
||||
if kwargs['mask'] == "NickServ!NickServ@services.baconsvin.org" and kwargs['event'] == "NOTICE" and kwargs['data'] == "This nickname is registered. Please choose a different nickname, or identify via \x02/msg NickServ identify <password>\x02.":
|
||||
logger.info("Nickserv identify needed, fixing...")
|
||||
self.bot.privmsg("NickServ@services.baconsvin.org", "identify %s %s" % (settings.IRCBOT_NICK, settings.IRCBOT_NICKSERV_PASSWORD))
|
||||
# we only handle NOTICEs for now
|
||||
if kwargs['event'] != "NOTICE":
|
||||
return
|
||||
|
||||
# check if this is a message from nickserv
|
||||
if kwargs['mask'] == "NickServ!%s" % settings.IRCBOT_NICKSERV_MASK:
|
||||
self.bot.handle_nickserv_privmsg(**kwargs)
|
||||
|
||||
# check if this is a message from chanserv
|
||||
if kwargs['mask'] == "ChanServ!%s" % settings.IRCBOT_CHANSERV_MASK:
|
||||
self.bot.handle_chanserv_privmsg(**kwargs)
|
||||
|
||||
|
||||
@irc3.event(irc3.rfc.KICK)
|
||||
|
@ -68,13 +95,28 @@ class Plugin(object):
|
|||
###############################################################################################
|
||||
### custom irc3 methods
|
||||
|
||||
@irc3.extend
|
||||
def do_stuff(self):
|
||||
"""
|
||||
Main periodic method called every N seconds.
|
||||
"""
|
||||
#logger.debug("inside do_stuff()")
|
||||
|
||||
# call the methods we need to
|
||||
self.bot.check_irc_channels()
|
||||
self.bot.fix_missing_acls()
|
||||
self.bot.get_outgoing_messages()
|
||||
|
||||
# schedule a call of this function again in N seconds
|
||||
self.bot.loop.call_later(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS, self.bot.do_stuff)
|
||||
|
||||
|
||||
@irc3.extend
|
||||
def get_outgoing_messages(self):
|
||||
"""
|
||||
This method gets unprocessed OutgoingIrcMessage objects and attempts to send them to
|
||||
the target channel. Messages are skipped if the bot is not in the channel.
|
||||
"""
|
||||
#logger.debug("inside get_outgoing_messages()")
|
||||
for msg in OutgoingIrcMessage.objects.filter(processed=False).order_by('created'):
|
||||
logger.info("processing irc message to %s: %s" % (msg.target, msg.message))
|
||||
# if this message expired mark it as expired and processed without doing anything
|
||||
|
@ -99,7 +141,209 @@ class Plugin(object):
|
|||
else:
|
||||
logger.warning("skipping message to %s" % msg.target)
|
||||
|
||||
# call this function again in X seconds
|
||||
self.bot.loop.call_later(settings.IRCBOT_CHECK_MESSAGE_INTERVAL_SECONDS, self.bot.get_outgoing_messages)
|
||||
|
||||
###############################################################################################
|
||||
### irc channel methods
|
||||
|
||||
@irc3.extend
|
||||
def check_irc_channels(self):
|
||||
"""
|
||||
Compare the list of IRC channels the bot is currently in with the list of IRC channels the bot is supposed to be in.
|
||||
Join or part channels as needed.
|
||||
"""
|
||||
desired_channel_list = list(set(list(self.get_managed_team_channels()) + list(self.get_unmanaged_team_channels()) + [settings.IRCBOT_PUBLIC_CHANNEL]))
|
||||
#logger.debug("Inside check_irc_channels(), desired_channel_list is: %s and self.bot.channels is: %s" % (desired_channel_list, self.bot.channels.keys()))
|
||||
|
||||
# loop over desired_channel_list, join as needed
|
||||
for channel in desired_channel_list:
|
||||
if channel not in self.bot.channels:
|
||||
logger.debug("I should be in %s but I am not, attempting to join..." % channel)
|
||||
self.bot.join(channel)
|
||||
|
||||
# loop over self.bot.channels, part as needed
|
||||
for channel in self.bot.channels:
|
||||
if channel not in desired_channel_list:
|
||||
logger.debug("I am in %s but I shouldn't be, parting..." % channel)
|
||||
self.bot.part(channel, "I am no longer needed here")
|
||||
|
||||
|
||||
@irc3.extend
|
||||
def get_managed_team_channels(self):
|
||||
"""
|
||||
Return a unique list of team IRC channels which the bot is supposed to be managing.
|
||||
"""
|
||||
return Team.objects.filter(
|
||||
irc_channel=True,
|
||||
irc_channel_managed=True
|
||||
).values_list("irc_channel_name", flat=True)
|
||||
|
||||
|
||||
@irc3.extend
|
||||
def get_unmanaged_team_channels(self):
|
||||
"""
|
||||
Return a unique list of team IRC channels which the bot is not supposed to be managing.
|
||||
"""
|
||||
return Team.objects.filter(
|
||||
irc_channel=True,
|
||||
irc_channel_managed=False
|
||||
).values_list("irc_channel_name", flat=True)
|
||||
|
||||
|
||||
@irc3.extend
|
||||
def setup_private_channel(self, team):
|
||||
"""
|
||||
Configures a private team IRC channel by setting modes and adding all members to ACL
|
||||
"""
|
||||
# basic private channel modes
|
||||
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s mlock +inpst" % team.irc_channel_name)
|
||||
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE on" % team.irc_channel_name)
|
||||
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED on" % team.irc_channel_name)
|
||||
|
||||
# add the bot to the ACL
|
||||
self.bot.add_user_to_team_channel_acl(
|
||||
username=settings.IRCBOT_NICK,
|
||||
channel=team.irc_channel_name
|
||||
)
|
||||
|
||||
# add all members to the acl
|
||||
for membership in team.memberships.all():
|
||||
if membership.approved and membership.user.profile.nickserv_username:
|
||||
self.bot.add_user_to_team_channel_acl(
|
||||
username=membership.user.profile.nickserv_username,
|
||||
channel=membership.team.irc_channel_name,
|
||||
)
|
||||
|
||||
# mark membership as irc_channel_acl_ok=True and save
|
||||
membership.irc_channel_acl_ok=True
|
||||
membership.save()
|
||||
|
||||
|
||||
@irc3.extend
|
||||
def setup_public_channel(self, team):
|
||||
"""
|
||||
Configures a public team IRC channel (by unsetting SECURE and RESTRICTED modes used by private channels and setting mlock back to the default +nt-lk)
|
||||
"""
|
||||
# basic private channel modes
|
||||
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s mlock +nt-lk" % team.irc_channel_name)
|
||||
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE off" % team.irc_channel_name)
|
||||
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED off" % team.irc_channel_name)
|
||||
|
||||
|
||||
@irc3.extend
|
||||
def add_user_to_team_channel_acl(self, username, channel):
|
||||
"""
|
||||
Add user to team IRC channel ACL
|
||||
"""
|
||||
# set autoop for this username
|
||||
self.bot.privmsg(
|
||||
settings.IRCBOT_CHANSERV_MASK,
|
||||
"flags %(channel)s %(user)s +oO" % {
|
||||
'channel': channel,
|
||||
'user': username,
|
||||
},
|
||||
)
|
||||
|
||||
# also add autoinvite for this username
|
||||
self.bot.mode(channel, '+I', '$a:%s' % username)
|
||||
|
||||
|
||||
@irc3.extend
|
||||
def fix_missing_acls(self):
|
||||
"""
|
||||
Called periodically by do_stuff()
|
||||
Loops over TeamMember objects and adds and removes ACL entries as needed
|
||||
"""
|
||||
missing_acls = TeamMember.objects.filter(
|
||||
team__irc_channel=True,
|
||||
team__irc_channel_managed=True,
|
||||
team__irc_channel_private=True,
|
||||
irc_channel_acl_ok=False
|
||||
).exclude(
|
||||
user__profile__nickserv_username=''
|
||||
)
|
||||
|
||||
if not missing_acls:
|
||||
return
|
||||
|
||||
logger.debug("Found %s memberships which need IRC ACL fixing.." % missing_acls.count())
|
||||
for membership in missing_acls:
|
||||
self.bot.add_user_to_team_channel_acl(
|
||||
username=membership.user.profile.nickserv_username,
|
||||
channel=membership.team.irc_channel_name,
|
||||
)
|
||||
# mark membership as irc_channel_acl_ok=True and save
|
||||
membership.irc_channel_acl_ok=True
|
||||
membership.save()
|
||||
|
||||
|
||||
###############################################################################################
|
||||
### services (ChanServ & NickServ) methods
|
||||
|
||||
@irc3.extend
|
||||
def handle_chanserv_privmsg(self, **kwargs):
|
||||
"""
|
||||
Handle messages from ChanServ on networks with Services.
|
||||
"""
|
||||
logger.debug("Got a message from ChanServ")
|
||||
|
||||
###############################################
|
||||
# handle "Channel \x02#example\x02 is not registered." message
|
||||
###############################################
|
||||
match = re.compile("Channel (#[a-zA-Z0-9-]+) is not registered.").match(kwargs['data'].replace("\x02", ""))
|
||||
if match:
|
||||
# the irc channel is not registered
|
||||
channel = match.group(1)
|
||||
# get a list of the channels we are supposed to be managing
|
||||
if channel in self.bot.get_managed_team_channels():
|
||||
# we want to register this channel! but we can only do so if we have a @ in the channel
|
||||
if self.bot.nick in self.bot.channels[channel].modes['@']:
|
||||
logger.debug("ChanServ says channel %s is not registered, bot is supposed to be managing this channel, registering it with chanserv" % channel)
|
||||
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "register %s" % channel)
|
||||
else:
|
||||
logger.debug("ChanServ says channel %s is not registered, bot is supposed to be managing this channel, but the bot cannot register without @ in the channel" % channel)
|
||||
self.bot.privmsg(channel, "I need @ before I can register this channel with ChanServ")
|
||||
return
|
||||
|
||||
###############################################
|
||||
# handle "\x02#example\x02 is now registered to \x02tykbhdev\x02" message
|
||||
###############################################
|
||||
match = re.compile("(#[a-zA-Z0-9-]+) is now registered to ([a-zA-Z0-9-]+)\\.").match(kwargs['data'].replace("\x02", ""))
|
||||
if match:
|
||||
# the irc channel is now registered
|
||||
channel = match.group(1)
|
||||
botnick = match.group(2)
|
||||
logger.debug("Channel %s was registered with ChanServ, looking up Team..." % channel)
|
||||
|
||||
# if this channel is a private team IRC channel set modes and add initial ACL
|
||||
try:
|
||||
team = Team.objects.get(irc_channel_name=channel)
|
||||
except Team.DoesNotExist:
|
||||
logger.debug("Unable to find Team matching IRC channel %s" % channel)
|
||||
return
|
||||
|
||||
if not team.irc_channel_private:
|
||||
# this channel is not private, no mode change and ACL needed
|
||||
return
|
||||
|
||||
# set channel modes and ACL
|
||||
self.bot.setup_private_channel(team)
|
||||
return
|
||||
|
||||
logger.debug("Unhandled ChanServ message: %s" % kwargs['data'])
|
||||
|
||||
|
||||
@irc3.extend
|
||||
def handle_nickserv_privmsg(self, **kwargs):
|
||||
"""th
|
||||
Handles messages from NickServ on networks with Services.
|
||||
"""
|
||||
logger.debug("Got a message from NickServ")
|
||||
|
||||
# handle "\x02botnick\x02 is not a registered nickname." message
|
||||
if kwargs['data'] == '\x02%s\x02 is not a registered nickname.' % self.bot.nick:
|
||||
# the bots nickname is not registered, register new account with nickserv
|
||||
self.bot.privmsg(settings.IRCBOT_NICKSERV_MASK, "register %s %s" % (settings.IRCBOT_NICKSERV_PASSWORD, settings.IRCBOT_NICKSERV_EMAIL))
|
||||
return
|
||||
|
||||
logger.debug("Unhandled NickServ message: %s" % kwargs['data'])
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.conf import settings
|
||||
import logging
|
||||
import irc3
|
||||
from events.models import Routing
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger('bornhack.%s' % __name__)
|
||||
|
||||
|
@ -9,9 +10,13 @@ def do_work():
|
|||
"""
|
||||
Run irc3 module code, wait for events on IRC and wait for messages in OutgoingIrcMessage
|
||||
"""
|
||||
if hasattr(settings, 'IRCBOT_CHANNELS'):
|
||||
logger.error("settings.IRCBOT_CHANNELS is deprecated. Please define settings.IRCBOT_PUBLIC_CHANNEL and use team channels for the rest.")
|
||||
return False
|
||||
|
||||
config = {
|
||||
'nick': settings.IRCBOT_NICK,
|
||||
'autojoins': list(set(settings.IRCBOT_CHANNELS.values())),
|
||||
'autojoins': [],
|
||||
'host': settings.IRCBOT_SERVER_HOSTNAME,
|
||||
'port': settings.IRCBOT_SERVER_PORT,
|
||||
'ssl': settings.IRCBOT_SERVER_USETLS,
|
||||
|
|
21
src/ircbot/utils.py
Normal file
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>
|
||||
<tr>
|
||||
<th>Team Name</th>
|
||||
<th>Team Responsible</th>
|
||||
<th>Team Members</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -31,19 +32,20 @@ People | {{ block.super }}
|
|||
{{ team.name }} Team
|
||||
</td>
|
||||
<td>
|
||||
{% if team.anoncount == 0 and team.approvedmembers.count == 0 %}
|
||||
<b>No team member(s)
|
||||
{% elif team.approvedmembers.count == team.anoncount %}
|
||||
<b>{{ team.anoncount }}</b> anonymous member(s)
|
||||
{% endif %}
|
||||
|
||||
{% for member in team.approvedmembers.all %}
|
||||
{% if member.user.profile.approved_public_credit_name %}
|
||||
{{ member.user.profile.approved_public_credit_name }}{% if member in team.responsible.all %} (responsible){% endif %}<br>
|
||||
{% endif %}
|
||||
{% for resp in team.responsible_members.all %}
|
||||
{{ resp.profile.get_public_credit_name }}<br>
|
||||
{% endfor %}
|
||||
{% if team.anoncount and team.anoncount != team.approvedmembers.count %}
|
||||
plus <b>{{ team.anoncount }}</b> anonymous member(s).
|
||||
</td>
|
||||
<td>
|
||||
{% for member in team.regular_members.all %}
|
||||
{% if member.profile.get_public_credit_name != "Unnamed" %}
|
||||
{{ member.profile.get_public_credit_name }}<br>
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
No team members
|
||||
{% endfor %}
|
||||
{% if team.unnamed_members %}
|
||||
{% if team.unnamed_members.count < team.regular_members.count %}Plus {% endif %}<b>{{ team.unnamed_members.count }}</b> anonymous member(s).
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
default_app_config = 'profiles.apps.ProfilesConfig'
|
||||
|
|
@ -14,6 +14,10 @@ class OrderAdmin(admin.ModelAdmin):
|
|||
'public_credit_name_approved',
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'public_credit_name_approved',
|
||||
]
|
||||
|
||||
def approve_public_credit_names(self, request, queryset):
|
||||
for profile in queryset.filter(public_credit_name_approved=False):
|
||||
profile.approve_public_credit_name()
|
||||
|
|
16
src/profiles/apps.py
Normal file
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),
|
||||
),
|
||||
]
|
20
src/profiles/migrations/0010_auto_20180411_2305.py
Normal file
20
src/profiles/migrations/0010_auto_20180411_2305.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2018-04-11 21:05
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('profiles', '0009_profile_nickserv_username'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='nickserv_username',
|
||||
field=models.CharField(blank=True, help_text='Your NickServ username is used to manage team IRC channel access lists. Make sure you register with NickServ _before_ you enter the username here!', max_length=50),
|
||||
),
|
||||
]
|
|
@ -1,9 +1,5 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models.signals import (
|
||||
post_save,
|
||||
pre_save
|
||||
)
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.dispatch import receiver
|
||||
|
@ -11,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from datetime import timedelta
|
||||
|
||||
from ircbot.models import OutgoingIrcMessage
|
||||
from utils.models import UUIDModel, CreatedUpdatedModel
|
||||
|
||||
|
||||
|
@ -43,7 +38,7 @@ class Profile(CreatedUpdatedModel, UUIDModel):
|
|||
public_credit_name = models.CharField(
|
||||
blank=True,
|
||||
max_length=100,
|
||||
help_text='The name you want to appear on in the credits section of the public website (the People pages). Leave empty if you want no public credit.'
|
||||
help_text='The name you want to appear on in the credits section of the public website (on Team and People pages). Leave this empty if you want your name hidden on the public webpages.'
|
||||
)
|
||||
|
||||
public_credit_name_approved = models.BooleanField(
|
||||
|
@ -51,6 +46,12 @@ class Profile(CreatedUpdatedModel, UUIDModel):
|
|||
help_text='Check this box to approve this users public_credit_name. This will be unchecked automatically when the user edits public_credit_name'
|
||||
)
|
||||
|
||||
nickserv_username = models.CharField(
|
||||
blank=True,
|
||||
max_length=50,
|
||||
help_text='Your NickServ username is used to manage team IRC channel access lists. Make sure you register with NickServ _before_ you enter the username here!',
|
||||
)
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
return self.user.email
|
||||
|
@ -59,37 +60,21 @@ class Profile(CreatedUpdatedModel, UUIDModel):
|
|||
return self.user.username
|
||||
|
||||
def approve_public_credit_name(self):
|
||||
"""
|
||||
This method just sets profile.public_credit_name_approved=True and calls save()
|
||||
It is used in an admin action
|
||||
"""
|
||||
self.public_credit_name_approved = True
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def approved_public_credit_name(self):
|
||||
def get_public_credit_name(self):
|
||||
"""
|
||||
Convenience method to return profile.public_credit_name if it is approved,
|
||||
and the string "Unnamed" otherwise
|
||||
"""
|
||||
if self.public_credit_name_approved:
|
||||
return self.public_credit_name
|
||||
else:
|
||||
return False
|
||||
return "Unnamed"
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_profile(sender, created, instance, **kwargs):
|
||||
if created:
|
||||
Profile.objects.create(user=instance)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Profile)
|
||||
def changed_public_credit_name(sender, instance, **kwargs):
|
||||
try:
|
||||
original = sender.objects.get(pk=instance.pk)
|
||||
except sender.DoesNotExist:
|
||||
# newly created object, just pass
|
||||
pass
|
||||
else:
|
||||
if not original.public_credit_name == instance.public_credit_name:
|
||||
OutgoingIrcMessage.objects.create(
|
||||
target=settings.IRCBOT_CHANNELS['orga'] if 'orga' in settings.IRCBOT_CHANNELS else settings.IRCBOT_CHANNELS['default'],
|
||||
message='User {username} changed public credit name. please review and act accordingly: https://bornhack.dk/admin/profiles/profile/{uuid}/change/'.format(
|
||||
username=instance.name,
|
||||
uuid=instance.uuid
|
||||
),
|
||||
timeout=timezone.now()+timedelta(minutes=60)
|
||||
)
|
||||
|
|
80
src/profiles/signal_handlers.py
Normal file
80
src/profiles/signal_handlers.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
from django.db.models.signals import (
|
||||
post_save,
|
||||
pre_save
|
||||
)
|
||||
from events.handler import handle_team_event
|
||||
import logging
|
||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||
|
||||
|
||||
def create_profile(sender, created, instance, **kwargs):
|
||||
"""
|
||||
Signal handler called after a User object is saved.
|
||||
Creates a Profile object when the User object was just created.
|
||||
"""
|
||||
from .models import Profile
|
||||
if created:
|
||||
Profile.objects.create(user=instance)
|
||||
|
||||
|
||||
def profile_pre_save(sender, instance, **kwargs):
|
||||
"""
|
||||
Signal handler called before a Profile object is saved.
|
||||
"""
|
||||
try:
|
||||
original = sender.objects.get(pk=instance.pk)
|
||||
except sender.DoesNotExist:
|
||||
original = None
|
||||
|
||||
public_credit_name_changed(instance, original)
|
||||
nickserv_username_changed(instance, original)
|
||||
|
||||
|
||||
def public_credit_name_changed(instance, original):
|
||||
"""
|
||||
Checks if a users public_credit_name has been changed, and triggers a public_credit_name_changed event if so
|
||||
"""
|
||||
if original and original.public_credit_name == instance.public_credit_name:
|
||||
# public_credit_name has not been changed
|
||||
return
|
||||
|
||||
if original and original.public_credit_name and not original.public_credit_name_approved:
|
||||
# the original.public_credit_name was not approved, no need to notify again
|
||||
return
|
||||
|
||||
# put the message together
|
||||
message='User {username} changed public credit name. please review and act accordingly: https://bornhack.dk/admin/profiles/profile/{uuid}/change/'.format(
|
||||
username=instance.name,
|
||||
uuid=instance.uuid
|
||||
)
|
||||
|
||||
# trigger the event
|
||||
handle_team_event(
|
||||
eventtype='public_credit_name_changed',
|
||||
irc_message=message,
|
||||
)
|
||||
|
||||
|
||||
def nickserv_username_changed(instance, original):
|
||||
"""
|
||||
Check if profile.nickserv_username was changed, and uncheck irc_channel_acl_ok if so
|
||||
This will be picked up by the IRC bot and fixed as needed
|
||||
"""
|
||||
if instance.nickserv_username and original and instance.nickserv_username != original.nickserv_username:
|
||||
logger.debug("profile.nickserv_username changed for user %s, setting irc_channel_acl_ok=False" % instance.user.username)
|
||||
|
||||
# find team memberships for this user
|
||||
from teams.models import TeamMember
|
||||
memberships = TeamMember.objects.filter(
|
||||
user=instance.user,
|
||||
approved=True,
|
||||
team__irc_channel=True,
|
||||
team__irc_channel_managed=True,
|
||||
team__irc_channel_private=True,
|
||||
)
|
||||
|
||||
# loop over memberships
|
||||
for membership in memberships:
|
||||
membership.irc_channel_acl_ok = False
|
||||
membership.save()
|
||||
|
|
@ -14,9 +14,13 @@
|
|||
<td>{{ profile.description|default:"N/A" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Public Credit Name (visible to the public, leave empty if you want no credits)</b></td>
|
||||
<td><b>Public Credit Name (visible to the public, leave empty if you want no credits on this website)</b></td>
|
||||
<td>{{ profile.public_credit_name|default:"N/A" }} {% if profile.public_credit_name %}({% if profile.public_credit_name_approved %}<span class="text-success">approved</span>{% else %}<span class="text-danger">pending approval</span>{% endif %}){% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>NickServ username (visible to the public on IRC, used to handle team channel ACLs)</b></td>
|
||||
<td>{{ profile.nickserv_username|default:"N/A" }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a href="{% url 'profiles:update' %}" class="btn btn-black"><i class="fa fa-edit"></i> Edit Profile</a>
|
||||
{% endblock profile_content %}
|
||||
|
|
|
@ -16,7 +16,7 @@ class ProfileDetail(LoginRequiredMixin, DetailView):
|
|||
|
||||
class ProfileUpdate(LoginRequiredMixin, UpdateView):
|
||||
model = models.Profile
|
||||
fields = ['name', 'description', 'public_credit_name']
|
||||
fields = ['name', 'description', 'public_credit_name', 'nickserv_username']
|
||||
success_url = reverse_lazy('profiles:detail')
|
||||
template_name = 'profile_form.html'
|
||||
|
||||
|
@ -28,6 +28,6 @@ class ProfileUpdate(LoginRequiredMixin, UpdateView):
|
|||
# user changed the name (to something non blank)
|
||||
form.instance.public_credit_name_approved = False
|
||||
form.instance.save()
|
||||
messages.info(self.request, 'Your profile has been updated.')
|
||||
messages.success(self.request, 'Your profile has been updated.')
|
||||
return super().form_valid(form, **kwargs)
|
||||
|
||||
|
|
|
@ -14,12 +14,10 @@ class ProgramConfig(AppConfig):
|
|||
from .signal_handlers import (
|
||||
check_speaker_event_camp_consistency,
|
||||
check_speaker_camp_change,
|
||||
notify_proposal_submitted
|
||||
)
|
||||
m2m_changed.connect(
|
||||
check_speaker_event_camp_consistency,
|
||||
sender=Speaker.events.through
|
||||
)
|
||||
pre_save.connect(check_speaker_camp_change, sender=Speaker)
|
||||
pre_save.connect(notify_proposal_submitted, sender=SpeakerProposal)
|
||||
pre_save.connect(notify_proposal_submitted, sender=EventProposal)
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ from django.conf import settings
|
|||
|
||||
from .email import add_new_speakerproposal_email, add_new_eventproposal_email
|
||||
from .models import EventProposal, SpeakerProposal
|
||||
from ircbot.models import OutgoingIrcMessage
|
||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||
|
||||
|
||||
|
@ -37,42 +36,3 @@ def check_speaker_camp_change(sender, instance, **kwargs):
|
|||
if event.camp != instance.camp:
|
||||
raise ValidationError({'camp': 'You cannot change the camp a speaker belongs to if the speaker is associated with one or more events.'})
|
||||
|
||||
|
||||
# pre_save signal that notifies if a proposal changes status from draft to
|
||||
# pending i.e. is submitted.
|
||||
def notify_proposal_submitted(sender, instance, **kwargs):
|
||||
try:
|
||||
original = sender.objects.get(pk=instance.pk)
|
||||
except sender.DoesNotExist:
|
||||
return False
|
||||
|
||||
target = settings.IRCBOT_CHANNELS['orga'] if 'orga' in settings.IRCBOT_CHANNELS else settings.IRCBOT_CHANNELS['default']
|
||||
|
||||
if original.proposal_status == 'draft' and instance.proposal_status == 'pending':
|
||||
if isinstance(instance, EventProposal):
|
||||
if not add_new_eventproposal_email(instance):
|
||||
logger.error(
|
||||
'Error adding event proposal email to outgoing queue for {}'.format(instance)
|
||||
)
|
||||
OutgoingIrcMessage.objects.create(
|
||||
target=target,
|
||||
message="New event proposal: {} - https://bornhack.dk/admin/program/eventproposal/{}/change/".format(
|
||||
instance.title,
|
||||
instance.uuid
|
||||
),
|
||||
timeout=timezone.now()+timedelta(minutes=10)
|
||||
)
|
||||
|
||||
if isinstance(instance, SpeakerProposal):
|
||||
if not add_new_speakerproposal_email(instance):
|
||||
logger.error(
|
||||
'Error adding speaker proposal email to outgoing queue for {}'.format(instance)
|
||||
)
|
||||
OutgoingIrcMessage.objects.create(
|
||||
target=target,
|
||||
message="New speaker proposal: {} - https://bornhack.dk/admin/program/speakerproposal/{}/change/".format(
|
||||
instance.name,
|
||||
instance.uuid
|
||||
),
|
||||
timeout=timezone.now()+timedelta(minutes=10)
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.contrib import admin
|
||||
from .models import Team, TeamArea, TeamMember, TeamTask
|
||||
from .models import Team, TeamMember, TeamTask
|
||||
from .email import add_added_membership_email, add_removed_membership_email
|
||||
from camps.utils import CampPropertyListFilter
|
||||
|
||||
|
@ -17,12 +17,12 @@ class TeamTaskAdmin(admin.ModelAdmin):
|
|||
@admin.register(Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
def get_responsible(self, obj):
|
||||
return ", ".join([resp.get_full_name() for resp in obj.responsible])
|
||||
return ", ".join([resp.profile.public_credit_name for resp in obj.responsible_members.all()])
|
||||
get_responsible.short_description = 'Responsible'
|
||||
|
||||
list_display = [
|
||||
'name',
|
||||
'area',
|
||||
'camp',
|
||||
'get_responsible',
|
||||
'needs_members',
|
||||
]
|
||||
|
@ -35,6 +35,7 @@ class TeamAdmin(admin.ModelAdmin):
|
|||
@admin.register(TeamMember)
|
||||
class TeamMemberAdmin(admin.ModelAdmin):
|
||||
list_filter = [
|
||||
CampPropertyListFilter,
|
||||
'team',
|
||||
'approved',
|
||||
]
|
||||
|
@ -78,9 +79,3 @@ class TeamMemberAdmin(admin.ModelAdmin):
|
|||
)
|
||||
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.db.models.signals import post_save, post_delete
|
||||
from .signal_handlers import teammember_saved, teammember_deleted
|
||||
|
||||
|
||||
class TeamsConfig(AppConfig):
|
||||
name = 'teams'
|
||||
|
||||
def ready(self):
|
||||
# connect the post_save signal, always including a dispatch_uid to prevent it being called multiple times in corner cases
|
||||
post_save.connect(teammember_saved, sender='teams.TeamMember', dispatch_uid='teammember_save_signal')
|
||||
post_delete.connect(teammember_deleted, sender='teams.TeamMember', dispatch_uid='teammember_save_signal')
|
||||
|
||||
|
|
|
@ -49,10 +49,11 @@ def add_new_membership_email(membership):
|
|||
return add_outgoing_email(
|
||||
text_template='emails/new_membership_email.txt',
|
||||
html_template='emails/new_membership_email.html',
|
||||
to_recipients=[resp.email for resp in membership.team.responsible],
|
||||
to_recipients=[resp.email for resp in membership.team.responsible_members.all()],
|
||||
formatdict=formatdict,
|
||||
subject='New membership request for {} at {}'.format(
|
||||
membership.team.name,
|
||||
membership.team.camp.title
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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.utils.text import slugify
|
||||
from utils.models import CampRelatedModel
|
||||
from .email import add_new_membership_email
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse_lazy
|
||||
|
@ -11,123 +10,219 @@ import logging
|
|||
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||
|
||||
|
||||
class TeamArea(CampRelatedModel):
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = ('name', 'camp')
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(default='')
|
||||
camp = models.ForeignKey('camps.Camp', related_name="teamareas", on_delete=models.PROTECT)
|
||||
responsible = models.ManyToManyField(
|
||||
'auth.User',
|
||||
related_name='responsible_team_areas'
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '{} ({})'.format(self.name, self.camp)
|
||||
|
||||
|
||||
class Team(CampRelatedModel):
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(max_length=255, blank=True)
|
||||
area = models.ForeignKey(
|
||||
'teams.TeamArea',
|
||||
related_name='teams',
|
||||
on_delete=models.PROTECT
|
||||
camp = models.ForeignKey(
|
||||
'camps.Camp',
|
||||
related_name="teams",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
help_text='The team name',
|
||||
)
|
||||
|
||||
slug = models.SlugField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text='Url slug for this team. Leave blank to generate based on team name',
|
||||
)
|
||||
|
||||
shortslug = models.SlugField(
|
||||
help_text='Abbreviated version of the slug. Used in places like IRC channel names where space is limited',
|
||||
)
|
||||
|
||||
description = models.TextField()
|
||||
needs_members = models.BooleanField(default=True)
|
||||
|
||||
needs_members = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Check to indicate that this team needs more members',
|
||||
)
|
||||
|
||||
members = models.ManyToManyField(
|
||||
'auth.User',
|
||||
related_name='teams',
|
||||
through='teams.TeamMember'
|
||||
)
|
||||
mailing_list = models.EmailField(blank=True)
|
||||
|
||||
# mailing list related fields
|
||||
mailing_list = models.EmailField(
|
||||
blank=True
|
||||
)
|
||||
|
||||
mailing_list_archive_public = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Check if the mailing list archive is public'
|
||||
)
|
||||
|
||||
mailing_list_nonmember_posts = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Check if the mailinglist allows non-list-members to post'
|
||||
)
|
||||
|
||||
# IRC related fields
|
||||
irc_channel = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Check to make the IRC bot join the team IRC channel. Leave unchecked to disable IRC bot functionality for this team entirely.',
|
||||
)
|
||||
|
||||
irc_channel_name = models.TextField(
|
||||
default='',
|
||||
blank=True,
|
||||
help_text='Team IRC channel. Leave blank to generate channel name automatically, based on camp shortslug and team shortslug.',
|
||||
)
|
||||
|
||||
irc_channel_managed = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Check to make the bot manage the team IRC channel. The bot will register the channel with ChanServ if possible, and manage ACLs as needed.',
|
||||
)
|
||||
|
||||
irc_channel_private = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Check to make the IRC channel secret and +i (private for team members only using an ACL). Leave unchecked to make the IRC channel public and open for everyone.'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = (('name', 'camp'), ('slug', 'camp'))
|
||||
|
||||
def __str__(self):
|
||||
return '{} ({})'.format(self.name, self.camp)
|
||||
|
||||
def validate_unique(self, exclude):
|
||||
"""
|
||||
We cannot use unique_together with the camp field because it is a property,
|
||||
so check uniqueness of team name and slug here instead
|
||||
"""
|
||||
# check if this team name is in use under this camp
|
||||
if self.camp.teams.filter(name=self.name).exists():
|
||||
raise ValidationError("This Team name already exists for this Camp")
|
||||
if self.camp.teams.filter(slug=self.slug).exists():
|
||||
raise ValidationError("This Team slug already exists for this Camp")
|
||||
return True
|
||||
|
||||
@property
|
||||
def camp(self):
|
||||
return self.area.camp
|
||||
|
||||
def save(self, **kwargs):
|
||||
if (
|
||||
not self.pk or
|
||||
not self.slug
|
||||
):
|
||||
# generate slug if needed
|
||||
if not self.pk or not self.slug:
|
||||
slug = slugify(self.name)
|
||||
self.slug = slug
|
||||
|
||||
if not self.shortslug:
|
||||
self.shortslug = self.slug
|
||||
|
||||
# generate IRC channel name if needed
|
||||
if self.irc_channel and not self.irc_channel_name:
|
||||
self.irc_channel_name = "#%s-%s" % (self.camp.shortslug, self.shortslug)
|
||||
|
||||
super().save(**kwargs)
|
||||
|
||||
def memberstatus(self, member):
|
||||
if member not in self.members.all():
|
||||
return "Not member"
|
||||
else:
|
||||
if TeamMember.objects.get(team=self, user=member).approved:
|
||||
return "Member"
|
||||
else:
|
||||
return "Membership Pending"
|
||||
def clean(self):
|
||||
# make sure the irc channel name is prefixed with a # if it is set
|
||||
if self.irc_channel_name and self.irc_channel_name[0] != "#":
|
||||
self.irc_channel_name = "#%s" % self.irc_channel_name
|
||||
|
||||
if self.irc_channel_name:
|
||||
if Team.objects.filter(irc_channel_name=self.irc_channel_name).exclude(pk=self.pk).exists():
|
||||
raise ValidationError("This IRC channel name is already in use")
|
||||
|
||||
@property
|
||||
def responsible(self):
|
||||
if TeamMember.objects.filter(team=self, responsible=True).exists():
|
||||
return User.objects.filter(
|
||||
teammember__team=self,
|
||||
teammember__responsible=True
|
||||
)
|
||||
else:
|
||||
return self.area.responsible.all()
|
||||
def memberships(self):
|
||||
"""
|
||||
Returns all TeamMember objects for this team.
|
||||
Use self.members.all() to get User objects for all members,
|
||||
or use self.memberships.all() to get TeamMember objects for all members.
|
||||
"""
|
||||
return TeamMember.objects.filter(
|
||||
team=self
|
||||
)
|
||||
|
||||
@property
|
||||
def anoncount(self):
|
||||
return self.approvedmembers.filter(user__profile__public_credit_name_approved=False).count()
|
||||
def approved_members(self):
|
||||
"""
|
||||
Returns only approved members (returns User objects, not TeamMember objects)
|
||||
"""
|
||||
return self.members.filter(
|
||||
teammember__approved=True
|
||||
)
|
||||
|
||||
@property
|
||||
def approvedmembers(self):
|
||||
return TeamMember.objects.filter(team=self, approved=True)
|
||||
def unapproved_members(self):
|
||||
"""
|
||||
Returns only unapproved members (returns User objects, not TeamMember objects)
|
||||
"""
|
||||
return self.members.filter(
|
||||
teammember__approved=False
|
||||
)
|
||||
|
||||
@property
|
||||
def responsible_members(self):
|
||||
"""
|
||||
Return only approved and responsible members
|
||||
Used to handle permissions for team management
|
||||
"""
|
||||
return self.members.filter(
|
||||
teammember__approved=True,
|
||||
teammember__responsible=True
|
||||
)
|
||||
|
||||
@property
|
||||
def regular_members(self):
|
||||
"""
|
||||
Return only approved and not responsible members with
|
||||
an approved public_credit_name.
|
||||
Used on the people pages.
|
||||
"""
|
||||
return self.members.filter(
|
||||
teammember__approved=True,
|
||||
teammember__responsible=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def unnamed_members(self):
|
||||
"""
|
||||
Returns only approved and not responsible members,
|
||||
without an approved public_credit_name.
|
||||
"""
|
||||
return self.members.filter(
|
||||
teammember__approved=True,
|
||||
teammember__responsible=False,
|
||||
profile__public_credit_name_approved=False
|
||||
)
|
||||
|
||||
|
||||
class TeamMember(CampRelatedModel):
|
||||
user = models.ForeignKey('auth.User', on_delete=models.PROTECT)
|
||||
team = models.ForeignKey('teams.Team', on_delete=models.PROTECT)
|
||||
approved = models.BooleanField(default=False)
|
||||
responsible = models.BooleanField(default=False)
|
||||
user = models.ForeignKey(
|
||||
'auth.User',
|
||||
on_delete=models.PROTECT,
|
||||
help_text="The User object this team membership relates to",
|
||||
)
|
||||
|
||||
team = models.ForeignKey(
|
||||
'teams.Team',
|
||||
on_delete=models.PROTECT,
|
||||
help_text="The Team this membership relates to"
|
||||
)
|
||||
|
||||
approved = models.BooleanField(
|
||||
default=False,
|
||||
help_text="True if this membership is approved. False if not."
|
||||
)
|
||||
|
||||
responsible = models.BooleanField(
|
||||
default=False,
|
||||
help_text="True if this teammember is responsible for this Team. False if not."
|
||||
)
|
||||
|
||||
irc_channel_acl_ok = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Maintained by the IRC bot, do not edit manually. True if the teammembers NickServ username has been added to the Team IRC channels ACL.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-responsible', '-approved']
|
||||
|
||||
def __str__(self):
|
||||
return '{} is {} member of team {}'.format(
|
||||
self.user, '' if self.approved else 'an unapproved', self.team
|
||||
return '{} is {} {} member of team {}'.format(
|
||||
self.user,
|
||||
'' if self.approved else 'an unapproved',
|
||||
'' if not self.responsible else 'a responsible',
|
||||
self.team
|
||||
)
|
||||
|
||||
@property
|
||||
def camp(self):
|
||||
""" All CampRelatedModels must have a camp FK or a camp property """
|
||||
return self.team.camp
|
||||
|
||||
|
||||
@receiver(post_save, sender=TeamMember)
|
||||
def add_responsible_email(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
if not add_new_membership_email(instance):
|
||||
logger.error('Error adding email to outgoing queue')
|
||||
|
||||
|
||||
class TeamTask(CampRelatedModel):
|
||||
team = models.ForeignKey(
|
||||
'teams.Team',
|
||||
|
@ -157,14 +252,12 @@ class TeamTask(CampRelatedModel):
|
|||
|
||||
@property
|
||||
def camp(self):
|
||||
""" All CampRelatedModels must have a camp FK or a camp property """
|
||||
return self.team.camp
|
||||
|
||||
def save(self, **kwargs):
|
||||
# generate slug if needed
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(**kwargs)
|
||||
|
||||
@property
|
||||
def responsible(self):
|
||||
return self.team.responsible.all()
|
||||
|
||||
|
|
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))
|
||||
|
18
src/teams/templates/fix_irc_acl.html
Normal file
18
src/teams/templates/fix_irc_acl.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load commonmark %}
|
||||
|
||||
{% block title %}
|
||||
Fix IRC permissions for NickServ user {{ request.user.profile.nickserv_username }} for IRC channel {{ team.irc_channel_name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>Fix IRC permissions</h3>
|
||||
<p class="lead">This will make the bot re-add IRC ACL for your NickServ user <b>{{ request.user.profile.nickserv_username }}</b> for IRC channel <b>{{ team.irc_channel_name }}</b>. Use this in cases where you are unable to join the team IRC channel after entering your NickServ username in your profile.</p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<button class="btn btn-success" type="submit"><i class="fa fa-check"></i> Yes Please</button>
|
||||
<a href="{% url 'teams:detail' camp_slug=team.camp.slug team_slug=team.slug %}" class="btn btn-default" type="submit"><i class="fa fa-remove"></i> Cancel</a>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,27 +1,47 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load commonmark %}
|
||||
{% load teams_tags %}
|
||||
{% load bootstrap3 %}
|
||||
{% load teams_tags %}
|
||||
|
||||
{% block title %}
|
||||
Team: {{ team.name }} | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h4>{{ team.name }} Team</h4></div>
|
||||
<div class="panel-heading"><h4>{{ team.name }} Team Details</h4></div>
|
||||
<div class="panel-body">
|
||||
{{ team.description|unsafecommonmark }}
|
||||
{% if request.user in team.responsible.all %}
|
||||
<a href="{% url 'teams:manage' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary">Manage Team</a>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>{{ team.name }} Team Communications</h4>
|
||||
{{ team.camp.title }} teams primarily use mailing lists and IRC to communicate. The <b>{{ team.name }} team</b> can be contacted in the following ways:</p>
|
||||
|
||||
<h5>Mailing List</h5>
|
||||
{% if team.mailing_list and request.user in team.approved_members.all %}
|
||||
<p>The {{ team.name }} Team mailinglist is <b>{{ team.mailing_list }}</b>{% if team.mailing_list_archive_public %}, and the archives are publicly available{% endif %}. You should sign up for the list if you haven't already.</p>
|
||||
{% elif team.mailing_list and team.mailinglist_nonmember_posts %}
|
||||
<p>The {{ team.name }} Team mailinglist is <b>{{ team.mailing_list }}</b>{% if team.mailing_list_archive_public %}, and the archives are publicly available{% endif %}. You do not need to be a member of the list to post to it.</p>
|
||||
{% else %}
|
||||
<p>The {{ team.name }} Team does not have a public mailing list, but it can be contacted through our main email <a href="mailto:info@bornhack.dk">info@bornhack.dk</a>.
|
||||
{% endif %}
|
||||
|
||||
<h5>IRC Channel</h5>
|
||||
{% if team.irc_channel and request.user in team.approved_members.all %}
|
||||
<p>The {{ team.name }} Team IRC channel is <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ team.irc_channel_name }}">{{ team.irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}</a>.
|
||||
{% if team.irc_channel_private %}The channel is not open to the public. Enter your nickserv username in your <a href="{% url 'profiles:detail' %}">Profile</a> to get access.{% else %}The IRC channel is open for everyone to join.{% endif %}</p>
|
||||
{% elif team.irc_channel and not team.irc_channel_private %}
|
||||
<p>The {{ team.name }} Team IRC channel is <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ team.irc_channel_name }}">{{ team.irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}</a> and it is open for everyone to join.</p>
|
||||
{% else %}
|
||||
<p>The {{ team.name }} Team does not have a public IRC channel, but it can be reached through our main IRC channel <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ IRCBOT_PUBLIC_CHANNEL }}">{{ IRCBOT_PUBLIC_CHANNEL }} on {{ IRCBOT_SERVER_HOSTNAME }}</a>.</p>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Members</h3>
|
||||
<p>The following <b>{{ team.approvedmembers.count }}</b> people are members of the <b>{{ team.name }} team</b>:</p>
|
||||
<table class="table">
|
||||
<h4>{{ team.name }} Team Members</h4>
|
||||
<p>The following <b>{{ team.approved_members.count }}</b> people {% if team.unapproved_members.count %}(and {{ team.unapproved_members.count }} pending){% endif %} are members of the <b>{{ team.name }} Team</b>:</p>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
|
@ -33,40 +53,42 @@ Team: {{ team.name }} | {{ block.super }}
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for teammember in team.approvedmembers.all %}
|
||||
{% for teammember in team.memberships.all %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if teammember.user.profile.approved_public_credit_name %}
|
||||
{{ teammember.user.profile.approved_public_credit_name }}
|
||||
{% else %}
|
||||
anonymous
|
||||
{% endif %}
|
||||
{{ teammember.user.profile.get_public_credit_name }} {% if teammember.user == request.user %}(this is you!){% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if teammember.responsible %}Team Responsible{% else %}Team Member{% endif %}
|
||||
Team {% if teammember.responsible %}Responsible{% else %}Member{% endif %}
|
||||
{% if not teammember.approved %}(pending approval){% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if request.user in team.members.all %}
|
||||
<p>Your membership status: <b>{% membershipstatus request.user team %}</b></p>
|
||||
{% endif %}
|
||||
<p>Your membership status: <b>{% membershipstatus user team %}</b></p>
|
||||
|
||||
{% if request.user in team.members.all %}
|
||||
<a href="{% url 'teams:leave' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-danger">Leave Team</a>
|
||||
{% if team.irc_channel and team.irc_channel_managed and request.user.profile.nickserv_username %}
|
||||
<a href="{% url 'teams:fix_irc_acl' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fa fa-wrench"></i> Fix IRC ACL</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'teams:leave' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-danger"><i class="fa fa-remove"></i> Leave Team</a>
|
||||
{% else %}
|
||||
{% if team.needs_members %}
|
||||
<b>This team is looking for members!</b> <a href="{% url 'teams:join' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-xs btn-success">Join Team</a>
|
||||
<b>This team is looking for members!</b> <a href="{% url 'teams:join' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-xs btn-success"><i class="fa fa-plus"></i> Join Team</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if request.user in team.responsible_members.all %}
|
||||
<a href="{% url 'teams:manage' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fa fa-cog"></i> Manage Team</a>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Tasks</h3>
|
||||
<p>This team is responsible for the following tasks</p>
|
||||
<table class="table">
|
||||
<h4>{{ team.name }} Team Tasks</h4>
|
||||
<p>The {{ team.name }} Team is responsible for the following tasks</p>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
@ -80,17 +102,17 @@ Team: {{ team.name }} | {{ block.super }}
|
|||
<td><a href="{% url 'teams:task_detail' slug=task.slug camp_slug=camp.slug team_slug=team.slug %}">{{ task.name }}</a></td>
|
||||
<td>{{ task.description }}</td>
|
||||
<td>
|
||||
<a href="{% url 'teams:task_detail' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm">Details</a>
|
||||
{% if request.user in team.responsible.all %}
|
||||
<a href="{% url 'teams:task_update' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm">Edit Task</a>
|
||||
<a href="{% url 'teams:task_detail' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm"><i class="fa fa-search"></i> Details</a>
|
||||
{% if request.user in team.responsible_members.all %}
|
||||
<a href="{% url 'teams:task_update' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm"><i class="fa fa-edit"></i> Edit Task</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if request.user in team.responsible.all %}
|
||||
<a href="{% url 'teams:task_create' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary">Create Task</a>
|
||||
{% if request.user in team.responsible_members.all %}
|
||||
<a href="{% url 'teams:task_create' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fa fa-plus"></i> Create Task</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,12 +7,17 @@ Join Team: {{ team.name }} | {{ block.super }}
|
|||
|
||||
{% block content %}
|
||||
|
||||
<h3>{{ team.name }} Team</h3>
|
||||
<p class="lead">Really join the <b>{{ team.name }}</b> team? You will receive a message when your membership has been approved.<p>
|
||||
<p class="lead">Really join the <b>{{ team.name }}</b> Team for <b>{{ team.camp.title }}</b>?</p>
|
||||
|
||||
<p>Your membership will need to be approved by a team responsible. You will receive an email when your membership request has been processed.<p>
|
||||
|
||||
<p>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<button class="btn btn-success" type="submit"><i class="fa fa-check"></i> Join {{ team.name }} Team</button>
|
||||
<a href="{% url 'teams:list' camp_slug=camp.slug %}" class="btn btn-default" type="submit"><i class="fa fa-remove"></i> Cancel</a>
|
||||
</form>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -12,7 +12,7 @@ Teams | {{ block.super }}
|
|||
<p>This is a list of the teams for {{ camp.title }}. To join a team just press the Join button, but please put some info in your <a href="{% url 'profiles:detail' %}">profile</a> first, so the team responsible has some idea who you are.</p>
|
||||
<p>You can also leave a team of course, but please let the team responsible know why :)</p>
|
||||
<p>Team memberships need to be approved by a team responsible. You will receive a message when your membership has been approved.</p>
|
||||
<p>At {{ camp.title }} all organisers and volunteers buy full tickets like everyone else. In the future our budget may allow for discounts or free tickets for volunteers, but not this year. However: Please let us know if you can't afford a ticket - we will figure something out!</p>
|
||||
<p>At {{ camp.title }} all organisers and volunteers buy full tickets like everyone else. At future events our budget may allow for discounts or free tickets for volunteers, but currently it does not.</p>
|
||||
<p>We currently have {{ teams.count }} teams for {{ camp.title }}:</p>
|
||||
{% if teams %}
|
||||
<table class="table table-hover">
|
||||
|
@ -42,13 +42,13 @@ Teams | {{ block.super }}
|
|||
</td>
|
||||
|
||||
<td>
|
||||
{% for resp in team.responsible.all %}
|
||||
{{ resp.profile.approved_public_credit_name|default:"Unnamed" }}{% if not forloop.last %},{% endif %}<br>
|
||||
{% for resp in team.responsible_members.all %}
|
||||
{{ resp.profile.get_public_credit_name }}{% if not forloop.last %},{% endif %}<br>
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
||||
<td class="text-center">
|
||||
<span class="badge">{{ team.approvedmembers.count }}</span><br>
|
||||
<span class="badge">{{ team.members.count }}</span><br>
|
||||
{% if team.needs_members %}(more needed){% endif %}
|
||||
</td>
|
||||
|
||||
|
@ -58,30 +58,23 @@ Teams | {{ block.super }}
|
|||
|
||||
{% if request.user.is_authenticated %}
|
||||
<td class="text-center">
|
||||
{% membershipstatus request.user team as membership_status %}
|
||||
{% if membership_status == 'Membership Pending' %}
|
||||
<i class='fa fa-clock-o' title='Pending'></i>
|
||||
{% else %}
|
||||
{% if membership_status == 'Member' %}
|
||||
<i class='fa fa-thumbs-o-up' title='Member'></i>
|
||||
{% else %}
|
||||
<i class='fa fa-times' title='Not a member'></i>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% membershipstatus request.user team True %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if request.user in team.members.all %}
|
||||
<a href="{% url 'teams:leave' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-danger"><i class="fa fa-minus"></i> Leave</a>
|
||||
{% else %}
|
||||
{% if team.needs_members %}
|
||||
<a href="{% url 'teams:join' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-success"><i class="fa fa-plus"></i> Join</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if request.user in team.responsible.all %}
|
||||
<div class="btn-group-vertical">
|
||||
<a class="btn btn-primary" href="{% url 'teams:detail' camp_slug=camp.slug team_slug=team.slug %}"><i class="fa fa-search"></i> Details</a>
|
||||
{% if request.user in team.responsible_members.all %}
|
||||
<a href="{% url 'teams:manage' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fa fa-cog"></i> Manage</a>
|
||||
{% endif %}
|
||||
{% if request.user in team.members.all %}
|
||||
<a href="{% url 'teams:leave' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-danger"><i class="fa fa-remove"></i> Leave</a>
|
||||
{% else %}
|
||||
{% if team.needs_members %}
|
||||
<a href="{% url 'teams:join' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-success"><i class="fa fa-plus"></i> Join</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load commonmark %}
|
||||
{% load teams_tags %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}
|
||||
|
@ -8,81 +7,92 @@ Manage Team: {{ team.name }} | {{ block.super }}
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3>Manage {{ team.name }} Team</h3>
|
||||
<form method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h4>Manage {{ team.name }} Team</h4></div>
|
||||
<div class="panel-body" style="margin-left: 1em; margin-right: 1em;">
|
||||
<div class="form-group">
|
||||
<form method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
|
||||
{% bootstrap_form form %}
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{% buttons %}
|
||||
<button class="btn btn-success pull-right" type="submit"><i class="fa fa-check"></i> Save Team</button>
|
||||
<a class="btn btn-primary pull-right" href="{% url 'teams:detail' team_slug=team.slug camp_slug=camp.slug %}"><i class="fa fa-remove"></i> Cancel</a>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% buttons %}
|
||||
<button class="btn btn-primary pull-right" type="submit">Save</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
|
||||
<h3>{{ team.name }} Team Members</h3>
|
||||
{% if team.teammember_set.exists %}
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Profile
|
||||
</th>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
<th>
|
||||
Public Credit Name
|
||||
</th>
|
||||
<th>
|
||||
Membership
|
||||
</th>
|
||||
<th>
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for membership in team.teammember_set.all %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ membership.user }}
|
||||
</td>
|
||||
<td>
|
||||
{{ membership.user.profile.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ membership.user.profile.email }}
|
||||
</td>
|
||||
<td>
|
||||
{{ membership.user.profile.description }}
|
||||
</td>
|
||||
<td>
|
||||
{{ membership.user.profile.public_credit_name|default:"N/A" }}
|
||||
</td>
|
||||
<td>
|
||||
{% if membership.approved %}member{% else %}pending{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if membership.approved %}
|
||||
<a class="btn btn-danger" href="{% url 'teams:teammember_remove' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-trash-o"></i> Remove</a>
|
||||
{% else %}
|
||||
<a class="btn btn-danger" href="{% url 'teams:teammember_remove' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-trash-o"></i> Remove</a>
|
||||
<a class="btn btn-success" href="{% url 'teams:teammember_approve' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-check"></i> Approve</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No members found!</p>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h4>Manage {{ team.name }} Team Members</h4></div>
|
||||
<div class="panel-body" style="margin-left: 1em; margin-right: 1em;">
|
||||
{% if team.teammember_set.exists %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Username
|
||||
</th>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Email
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
<th>
|
||||
Public Credit Name
|
||||
</th>
|
||||
<th>
|
||||
Membership
|
||||
</th>
|
||||
<th>
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for membership in team.teammember_set.all %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ membership.user }}
|
||||
</td>
|
||||
<td>
|
||||
{{ membership.user.profile.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ membership.user.profile.email }}
|
||||
</td>
|
||||
<td>
|
||||
{{ membership.user.profile.description }}
|
||||
</td>
|
||||
<td>
|
||||
{{ membership.user.profile.public_credit_name|default:"N/A" }}
|
||||
{% if membership.user.profile.public_credit_name and not membership.user.profile.public_credit_name_approved %}<span class="text-warning">(name not approved)</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if membership.approved %}member{% else %}pending{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group-vertical">
|
||||
<a class="btn btn-danger" href="{% url 'teams:teammember_remove' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-trash-o"></i> Remove Member</a>
|
||||
{% if not membership.approved %}
|
||||
<a class="btn btn-success" href="{% url 'teams:teammember_approve' camp_slug=camp.slug pk=membership.id %}"><i class="fa fa-check"></i> Approve Member</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No members found!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,8 +1,24 @@
|
|||
from django import template
|
||||
|
||||
from django.utils.safestring import mark_safe
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def membershipstatus(user, team):
|
||||
return team.memberstatus(user)
|
||||
def membershipstatus(user, team, showicon=False):
|
||||
if user in team.responsible_members.all():
|
||||
text = "Responsible"
|
||||
icon = "fa-star"
|
||||
elif user in team.approved_members.all():
|
||||
text = "Member"
|
||||
icon = "fa-thumbs-o-up"
|
||||
elif user in team.unapproved_members.all():
|
||||
text = "Membership pending approval"
|
||||
icon = "fa-clock-o"
|
||||
else:
|
||||
text = "Not member"
|
||||
icon = "fa-times"
|
||||
|
||||
if showicon:
|
||||
return mark_safe("<i class='fa %s' title='%s'></i>" % (icon, text))
|
||||
else:
|
||||
return text
|
||||
|
||||
|
|
|
@ -46,6 +46,11 @@ urlpatterns = [
|
|||
TeamManageView.as_view(),
|
||||
name='manage'
|
||||
),
|
||||
url(
|
||||
r'^fix_irc_acl/$',
|
||||
FixIrcAclView.as_view(),
|
||||
name='fix_irc_acl',
|
||||
),
|
||||
url(
|
||||
r'^tasks/', include([
|
||||
url(
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.contrib import messages
|
|||
from django.http import HttpResponseRedirect
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from django.conf import settings
|
||||
from profiles.models import Profile
|
||||
|
||||
import logging
|
||||
|
@ -17,9 +17,12 @@ logger = logging.getLogger("bornhack.%s" % __name__)
|
|||
|
||||
|
||||
class EnsureTeamResponsibleMixin(object):
|
||||
"""
|
||||
Use to make sure request.user is responsible for the team specified by kwargs['team_slug']
|
||||
"""
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.team = Team.objects.get(slug=kwargs['team_slug'], camp=self.camp)
|
||||
if request.user not in self.team.responsible.all():
|
||||
if request.user not in self.team.responsible_members.all():
|
||||
messages.error(request, 'No thanks')
|
||||
return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=self.team.slug)
|
||||
|
||||
|
@ -28,6 +31,22 @@ class EnsureTeamResponsibleMixin(object):
|
|||
)
|
||||
|
||||
|
||||
class EnsureTeamMemberResponsibleMixin(SingleObjectMixin):
|
||||
"""
|
||||
Use to make sure request.user is responsible for the team which TeamMember belongs to
|
||||
"""
|
||||
model = TeamMember
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.user not in self.get_object().team.responsible_members.all():
|
||||
messages.error(request, 'No thanks')
|
||||
return redirect('teams:detail', camp_slug=self.get_object().team.camp.slug, team_slug=self.get_object().team.slug)
|
||||
|
||||
return super().dispatch(
|
||||
request, *args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class TeamListView(CampViewMixin, ListView):
|
||||
template_name = "team_list.html"
|
||||
model = Team
|
||||
|
@ -40,16 +59,26 @@ class TeamDetailView(CampViewMixin, DetailView):
|
|||
model = Team
|
||||
slug_url_kwarg = 'team_slug'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(TeamDetailView, self).get_context_data(**kwargs)
|
||||
context['IRCBOT_SERVER_HOSTNAME'] = settings.IRCBOT_SERVER_HOSTNAME
|
||||
context['IRCBOT_PUBLIC_CHANNEL'] = settings.IRCBOT_PUBLIC_CHANNEL
|
||||
return context
|
||||
|
||||
|
||||
class TeamManageView(CampViewMixin, EnsureTeamResponsibleMixin, UpdateView):
|
||||
model = Team
|
||||
template_name = "team_manage.html"
|
||||
fields = ['description', 'needs_members']
|
||||
fields = ['description', 'needs_members', 'irc_channel', 'irc_channel_name', 'irc_channel_managed', 'irc_channel_private']
|
||||
slug_url_kwarg = 'team_slug'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('teams:detail', kwargs={'camp_slug': self.camp.slug, 'team_slug': self.get_object().slug})
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, "Team has been saved")
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class TeamJoinView(LoginRequiredMixin, CampViewMixin, UpdateView):
|
||||
template_name = "team_join.html"
|
||||
|
@ -100,18 +129,6 @@ class TeamLeaveView(LoginRequiredMixin, CampViewMixin, UpdateView):
|
|||
return redirect('teams:list', camp_slug=self.get_object().camp.slug)
|
||||
|
||||
|
||||
class EnsureTeamMemberResponsibleMixin(SingleObjectMixin):
|
||||
model = TeamMember
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.user not in self.get_object().team.responsible.all():
|
||||
messages.error(request, 'No thanks')
|
||||
return redirect('teams:detail', camp_slug=self.get_object().team.camp.slug, team_slug=self.get_object().team.slug)
|
||||
|
||||
return super().dispatch(
|
||||
request, *args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class TeamMemberRemoveView(LoginRequiredMixin, CampViewMixin, EnsureTeamMemberResponsibleMixin, UpdateView):
|
||||
template_name = "teammember_remove.html"
|
||||
|
@ -197,3 +214,66 @@ class TaskUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMix
|
|||
def get_success_url(self):
|
||||
return self.get_object().get_absolute_url()
|
||||
|
||||
|
||||
class FixIrcAclView(LoginRequiredMixin, CampViewMixin, UpdateView):
|
||||
template_name = "fix_irc_acl.html"
|
||||
model = Team
|
||||
fields = []
|
||||
slug_url_kwarg = 'team_slug'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# we need to call the super().dispatch() method early so self.camp gets populated by CampViewMixin,
|
||||
# because the lookups below depend on self.camp being set :)
|
||||
response = super().dispatch(
|
||||
request, *args, **kwargs
|
||||
)
|
||||
|
||||
# check if the logged in user has an approved membership of this team
|
||||
if request.user not in self.get_object().approved_members.all():
|
||||
messages.error(request, 'No thanks')
|
||||
return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug)
|
||||
|
||||
# check if we manage the channel for this team
|
||||
if not self.get_object().irc_channel or not self.get_object().irc_channel_managed:
|
||||
messages.error(request, 'IRC functionality is disabled for this team, or the team channel is not managed by the bot')
|
||||
return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug)
|
||||
|
||||
# check if user has a nickserv username
|
||||
if not request.user.profile.nickserv_username:
|
||||
messages.error(request, 'Please go to your profile and set your NickServ username first. Make sure the account is registered with NickServ first!')
|
||||
return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug)
|
||||
|
||||
return response
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# get membership
|
||||
try:
|
||||
TeamMember.objects.get(
|
||||
user=request.user,
|
||||
team=self.get_object(),
|
||||
approved=True,
|
||||
irc_channel_acl_ok=True
|
||||
)
|
||||
except TeamMember.DoesNotExist:
|
||||
# this membership is already marked as membership.irc_channel_acl_ok=False, no need to do anything
|
||||
messages.error(request, 'No need, this membership is already marked as irc_channel_acl_ok=False, so the bot will fix the ACL soon')
|
||||
return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug)
|
||||
|
||||
return super().get(
|
||||
request, *args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
def form_valid(self, form):
|
||||
membership = TeamMember.objects.get(
|
||||
user=self.request.user,
|
||||
team=self.get_object(),
|
||||
approved=True,
|
||||
irc_channel_acl_ok=True
|
||||
)
|
||||
|
||||
membership.irc_channel_acl_ok = False
|
||||
membership.save()
|
||||
messages.success(self.request, "OK, hang on while we fix the permissions for your NickServ user '%s' for IRC channel '%s'" % (self.request.user.profile.nickserv_username, form.instance.irc_channel_name))
|
||||
return redirect('teams:detail', camp_slug=form.instance.camp.slug, team_slug=form.instance.slug)
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
<li><a href="{% url 'people' %}">People</a></li>
|
||||
{% if user.is_authenticated and user.is_staff %}
|
||||
<li><a href="{% url 'backoffice:index' %}">Backoffice</a></li>
|
||||
<li><a href="{% url 'admin:index' %}">Django Admin</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
|
|
|
@ -5,21 +5,20 @@ from datetime import (
|
|||
)
|
||||
from django.db.models import Count
|
||||
from django.utils import timezone
|
||||
from events.handler import handle_team_event
|
||||
|
||||
def ticket_changed(sender, instance, created, **kwargs):
|
||||
"""
|
||||
This signal is called every time a ShopTicket is saved
|
||||
"""
|
||||
# only queue an IRC message when a new ticket is created
|
||||
# only trigger an event when a new ticket is created
|
||||
if not created:
|
||||
return
|
||||
|
||||
# queue an IRC message to the orga channel if defined,
|
||||
# otherwise for the default channel
|
||||
target = settings.IRCBOT_CHANNELS['orga'] if 'orga' in settings.IRCBOT_CHANNELS else settings.IRCBOT_CHANNELS['default']
|
||||
|
||||
# get ticket stats
|
||||
from .models import ShopTicket
|
||||
|
||||
# TODO: this is nasty, get the prefix some other way
|
||||
ticket_prefix = "BornHack {}".format(datetime.now().year)
|
||||
|
||||
stats = ", ".join(
|
||||
|
@ -50,19 +49,17 @@ def ticket_changed(sender, instance, created, **kwargs):
|
|||
).count()
|
||||
|
||||
# queue the messages
|
||||
from ircbot.models import OutgoingIrcMessage
|
||||
OutgoingIrcMessage.objects.create(
|
||||
target=target,
|
||||
message="%s sold!" % instance.product.name,
|
||||
timeout=timezone.now()+timedelta(minutes=10)
|
||||
handle_team_event(
|
||||
eventtype='ticket_created',
|
||||
irc_message="%s sold!" % instance.product.name
|
||||
)
|
||||
OutgoingIrcMessage.objects.create(
|
||||
target=target,
|
||||
message="Totals: {}, 1day: {}, 1day child: {}".format(
|
||||
# limit this one to a length of 200 because IRC is nice
|
||||
handle_team_event(
|
||||
eventtype='ticket_created',
|
||||
irc_message="Totals: {}, 1day: {}, 1day child: {}".format(
|
||||
stats,
|
||||
onedaystats,
|
||||
onedaychildstats
|
||||
)[:200],
|
||||
timeout=timezone.now()+timedelta(minutes=10)
|
||||
)[:200]
|
||||
)
|
||||
|
||||
|
|
|
@ -23,9 +23,12 @@ from tickets.models import (
|
|||
from teams.models import (
|
||||
Team,
|
||||
TeamTask,
|
||||
TeamArea,
|
||||
TeamMember
|
||||
)
|
||||
from events.models import (
|
||||
Type,
|
||||
Routing
|
||||
)
|
||||
from django.contrib.auth.models import User
|
||||
from allauth.account.models import EmailAddress
|
||||
from django.utils.text import slugify
|
||||
|
@ -44,6 +47,7 @@ class Command(BaseCommand):
|
|||
title='BornHack 2016',
|
||||
tagline='Initial Commit',
|
||||
slug='bornhack-2016',
|
||||
shortslug='bh2016',
|
||||
buildup=(
|
||||
timezone.datetime(2016, 8, 25, 12, 0, tzinfo=timezone.utc),
|
||||
timezone.datetime(2016, 8, 27, 11, 59, tzinfo=timezone.utc),
|
||||
|
@ -63,6 +67,7 @@ class Command(BaseCommand):
|
|||
title='BornHack 2017',
|
||||
tagline='Make Tradition',
|
||||
slug='bornhack-2017',
|
||||
shortslug='bh2017',
|
||||
buildup=(
|
||||
timezone.datetime(2017, 8, 25, 12, 0, tzinfo=timezone.utc),
|
||||
timezone.datetime(2017, 8, 27, 11, 59, tzinfo=timezone.utc),
|
||||
|
@ -82,6 +87,7 @@ class Command(BaseCommand):
|
|||
title='BornHack 2018',
|
||||
tagline='Undecided',
|
||||
slug='bornhack-2018',
|
||||
shortslug='bh2018',
|
||||
buildup=(
|
||||
timezone.datetime(2018, 8, 25, 12, 0, tzinfo=timezone.utc),
|
||||
timezone.datetime(2018, 8, 27, 11, 59, tzinfo=timezone.utc),
|
||||
|
@ -104,6 +110,8 @@ class Command(BaseCommand):
|
|||
)
|
||||
user1.profile.name = 'John Doe'
|
||||
user1.profile.description = 'one that once was'
|
||||
user1.profile.public_credit_name = 'PublicDoe'
|
||||
user1.profile.public_credit_name_approved = True
|
||||
user1.profile.save()
|
||||
email = EmailAddress.objects.create(
|
||||
user=user1,
|
||||
|
@ -112,6 +120,7 @@ class Command(BaseCommand):
|
|||
verified=True
|
||||
)
|
||||
email.set_as_primary()
|
||||
|
||||
user2 = User.objects.create_user(
|
||||
username='user2',
|
||||
password='user2',
|
||||
|
@ -127,6 +136,7 @@ class Command(BaseCommand):
|
|||
verified=True
|
||||
)
|
||||
email.set_as_primary()
|
||||
|
||||
user3 = User.objects.create_user(
|
||||
username='user3',
|
||||
password='user3',
|
||||
|
@ -134,6 +144,7 @@ class Command(BaseCommand):
|
|||
)
|
||||
user3.profile.name = 'Lorem Ipsum'
|
||||
user3.profile.description = 'just a user'
|
||||
user3.profile.public_credit_name = 'Lorem Ipsum'
|
||||
user3.profile.save()
|
||||
email = EmailAddress.objects.create(
|
||||
user=user3,
|
||||
|
@ -142,6 +153,7 @@ class Command(BaseCommand):
|
|||
verified=True
|
||||
)
|
||||
email.set_as_primary()
|
||||
|
||||
user4 = User.objects.create_user(
|
||||
username='user4',
|
||||
password='user4',
|
||||
|
@ -149,6 +161,8 @@ class Command(BaseCommand):
|
|||
)
|
||||
user4.profile.name = 'Ethe Reum'
|
||||
user4.profile.description = 'I prefer doge'
|
||||
user4.profile.public_credit_name = 'Dogefan'
|
||||
user4.profile.public_credit_name_approved = True
|
||||
user4.profile.save()
|
||||
email = EmailAddress.objects.create(
|
||||
user=user4,
|
||||
|
@ -157,6 +171,96 @@ class Command(BaseCommand):
|
|||
verified=True
|
||||
)
|
||||
email.set_as_primary()
|
||||
|
||||
user5 = User.objects.create_user(
|
||||
username='user5',
|
||||
password='user5',
|
||||
is_staff=True
|
||||
)
|
||||
user5.profile.name = 'Pyra Mid'
|
||||
user5.profile.description = 'This is not a scam'
|
||||
user5.profile.public_credit_name = 'Ponziarne'
|
||||
user5.profile.public_credit_name_approved = True
|
||||
user5.profile.save()
|
||||
email = EmailAddress.objects.create(
|
||||
user=user5,
|
||||
email='user5@example.com',
|
||||
primary=False,
|
||||
verified=True
|
||||
)
|
||||
email.set_as_primary()
|
||||
|
||||
user6 = User.objects.create_user(
|
||||
username='user6',
|
||||
password='user6',
|
||||
is_staff=True
|
||||
)
|
||||
user6.profile.name = 'User Number 6'
|
||||
user6.profile.description = 'some description'
|
||||
user6.profile.public_credit_name = 'bruger 6'
|
||||
user6.profile.public_credit_name_approved = True
|
||||
user6.profile.save()
|
||||
email = EmailAddress.objects.create(
|
||||
user=user6,
|
||||
email='user6@example.com',
|
||||
primary=False,
|
||||
verified=True
|
||||
)
|
||||
email.set_as_primary()
|
||||
|
||||
user7 = User.objects.create_user(
|
||||
username='user7',
|
||||
password='user7',
|
||||
is_staff=True
|
||||
)
|
||||
user7.profile.name = 'Assembly Hacker'
|
||||
user7.profile.description = 'Low level is best level'
|
||||
user7.profile.public_credit_name = 'asm'
|
||||
user7.profile.public_credit_name_approved = True
|
||||
user7.profile.save()
|
||||
email = EmailAddress.objects.create(
|
||||
user=user7,
|
||||
email='user7@example.com',
|
||||
primary=False,
|
||||
verified=True
|
||||
)
|
||||
email.set_as_primary()
|
||||
|
||||
user8 = User.objects.create_user(
|
||||
username='user8',
|
||||
password='user8',
|
||||
is_staff=True
|
||||
)
|
||||
user8.profile.name = 'TCL'
|
||||
user8.profile.description = 'Expect me'
|
||||
user8.profile.public_credit_name = 'TCL lover'
|
||||
user8.profile.public_credit_name_approved = True
|
||||
user8.profile.save()
|
||||
email = EmailAddress.objects.create(
|
||||
user=user8,
|
||||
email='user8@example.com',
|
||||
primary=False,
|
||||
verified=True
|
||||
)
|
||||
email.set_as_primary()
|
||||
|
||||
user9 = User.objects.create_user(
|
||||
username='user9',
|
||||
password='user9',
|
||||
is_staff=True
|
||||
)
|
||||
user9.profile.name = 'John Windows'
|
||||
user9.profile.description = 'Microsoft is best soft'
|
||||
user9.profile.public_credit_name = 'msboy'
|
||||
user9.profile.save()
|
||||
email = EmailAddress.objects.create(
|
||||
user=user9,
|
||||
email='user9@example.com',
|
||||
primary=False,
|
||||
verified=True
|
||||
)
|
||||
email.set_as_primary()
|
||||
|
||||
admin = User.objects.create_superuser(
|
||||
username='admin',
|
||||
email='admin@example.com',
|
||||
|
@ -1373,44 +1477,21 @@ Please note that sleeping in the parking lot is not permitted. If you want to sl
|
|||
description='This village is representing TheCamp.dk, an annual danish tech camp held in July. The official subjects for this event is open source software, network and security. In reality we are interested in anything from computers to illumination soap bubbles and irish coffee'
|
||||
)
|
||||
|
||||
self.output("Creating team areas for {}...".format(year))
|
||||
pr_area = TeamArea.objects.create(
|
||||
name='PR',
|
||||
description="The Public Relations area covers website, social media and marketing related tasks.",
|
||||
camp=camp
|
||||
)
|
||||
content_area = TeamArea.objects.create(
|
||||
name='Content',
|
||||
description="The Content area handles talks, A/V and photos.",
|
||||
camp=camp
|
||||
)
|
||||
infrastructure_area = TeamArea.objects.create(
|
||||
name='Infrastructure',
|
||||
description="The Infrastructure area covers network/NOC, power, villages, CERT, logistics.",
|
||||
camp=camp
|
||||
)
|
||||
bar_area = TeamArea.objects.create(
|
||||
name='Bar',
|
||||
description="The Bar area covers building and running the IRL bar, DJ booth and related tasks.",
|
||||
camp=camp
|
||||
)
|
||||
|
||||
self.output("Setting teamarea responsibles for {}...".format(year))
|
||||
pr_area.responsible.add(user2)
|
||||
content_area.responsible.add(user2, user3)
|
||||
infrastructure_area.responsible.add(user3, user4)
|
||||
bar_area.responsible.add(user4)
|
||||
|
||||
self.output("Creating teams for {}...".format(year))
|
||||
orga_team = Team.objects.create(
|
||||
name="Orga",
|
||||
description="The Orga team are the main organisers. All tasks are Orga responsibility until they are delegated to another team",
|
||||
camp=camp
|
||||
)
|
||||
noc_team = Team.objects.create(
|
||||
name="NOC",
|
||||
description="The NOC team is in charge of establishing and running a network onsite.".format(year),
|
||||
area=infrastructure_area,
|
||||
description="The NOC team is in charge of establishing and running a network onsite.",
|
||||
camp=camp
|
||||
)
|
||||
bar_team = Team.objects.create(
|
||||
name="Bar",
|
||||
description="The Bar team plans, builds and run the IRL bar!",
|
||||
area=bar_area
|
||||
camp=camp
|
||||
)
|
||||
|
||||
self.output("Creating TeamTasks for {}...".format(year))
|
||||
|
@ -1461,29 +1542,93 @@ Please note that sleeping in the parking lot is not permitted. If you want to sl
|
|||
)
|
||||
|
||||
self.output("Setting team members for {}...".format(year))
|
||||
# noc team
|
||||
TeamMember.objects.create(
|
||||
team=noc_team,
|
||||
user=user4,
|
||||
approved=True
|
||||
approved=True,
|
||||
responsible=True
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=noc_team,
|
||||
user=user1
|
||||
user=user1,
|
||||
approved=True,
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=noc_team,
|
||||
user=user5,
|
||||
approved=True,
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=noc_team,
|
||||
user=user6,
|
||||
)
|
||||
|
||||
# bar team
|
||||
TeamMember.objects.create(
|
||||
team=bar_team,
|
||||
user=user1,
|
||||
approved=True
|
||||
approved=True,
|
||||
responsible=True
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=bar_team,
|
||||
user=user3,
|
||||
approved=True
|
||||
approved=True,
|
||||
responsible=True
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=bar_team,
|
||||
user=user2
|
||||
user=user2,
|
||||
approved=True,
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=bar_team,
|
||||
user=user7,
|
||||
approved=True,
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=bar_team,
|
||||
user=user8,
|
||||
)
|
||||
|
||||
# orga team
|
||||
TeamMember.objects.create(
|
||||
team=orga_team,
|
||||
user=user1,
|
||||
approved=True,
|
||||
responsible=True
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=orga_team,
|
||||
user=user3,
|
||||
approved=True,
|
||||
responsible=True
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=orga_team,
|
||||
user=user8,
|
||||
approved=True,
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=orga_team,
|
||||
user=user9,
|
||||
approved=True,
|
||||
)
|
||||
TeamMember.objects.create(
|
||||
team=orga_team,
|
||||
user=user4,
|
||||
)
|
||||
|
||||
self.output("Adding event routing...")
|
||||
Routing.objects.create(
|
||||
team=orga_team,
|
||||
eventtype=Type.objects.get(name="public_credit_name_changed")
|
||||
)
|
||||
Routing.objects.create(
|
||||
team=orga_team,
|
||||
eventtype=Type.objects.get(name="ticket_created")
|
||||
)
|
||||
|
||||
|
||||
self.output("marking 2016 as read_only...")
|
||||
|
|
Loading…
Reference in a new issue