change irc channel stuff so each team can have both a private and a public irc channel, introduce the concept of a volunteer channel which all teammembers of all teams get access to
This commit is contained in:
parent
cefdaaea97
commit
1c4a4dd259
|
@ -88,4 +88,5 @@ IRCBOT_SERVER_HOSTNAME='{{ django_ircbot_server }}'
|
||||||
IRCBOT_SERVER_PORT=6697
|
IRCBOT_SERVER_PORT=6697
|
||||||
IRCBOT_SERVER_USETLS=True
|
IRCBOT_SERVER_USETLS=True
|
||||||
IRCBOT_PUBLIC_CHANNEL='{{ django_ircbot_public_channel }}'
|
IRCBOT_PUBLIC_CHANNEL='{{ django_ircbot_public_channel }}'
|
||||||
|
IRCBOT_VOLUNTEER_CHANNEL='{{ django_ircbot_volunteer_channel }}'
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ def handle_team_event(eventtype, irc_message=None, irc_timeout=60, email_templat
|
||||||
|
|
||||||
if not eventtype.teams:
|
if not eventtype.teams:
|
||||||
# no routes found for this eventtype, do nothing
|
# no routes found for this eventtype, do nothing
|
||||||
logger.error("No routes round for eventtype %s" % eventtype)
|
#logger.error("No routes round for eventtype %s" % eventtype)
|
||||||
return
|
return
|
||||||
|
|
||||||
# loop over routes (teams) for this eventtype
|
# loop over routes (teams) for this eventtype
|
||||||
|
@ -48,13 +48,13 @@ def team_irc_notification(team, eventtype, irc_message=None, irc_timeout=60):
|
||||||
logger.error("IRC notifications not enabled for eventtype %s" % eventtype)
|
logger.error("IRC notifications not enabled for eventtype %s" % eventtype)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not team.irc_channel or not team.irc_channel_name:
|
if not team.private_irc_channel_name or not team.private_irc_channel_bot:
|
||||||
logger.error("team %s is not IRC enabled" % team)
|
logger.error("team %s does not have a private IRC channel" % team)
|
||||||
return
|
return
|
||||||
|
|
||||||
# send an IRC message to the the channel for this team
|
# send an IRC message to the the channel for this team
|
||||||
add_irc_message(
|
add_irc_message(
|
||||||
target=team.irc_channel_name,
|
target=team.private_irc_channel_name,
|
||||||
message=irc_message,
|
message=irc_message,
|
||||||
timeout=60
|
timeout=60
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,8 @@ from teams.models import Team, TeamMember
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from events.models import Routing
|
from events.models import Routing
|
||||||
|
from teams.utils import get_team_from_irc_channel
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
@ -63,7 +65,7 @@ class Plugin(object):
|
||||||
"""Triggered when a channel is joined by someone, including the bot itself"""
|
"""Triggered when a channel is joined by someone, including the bot itself"""
|
||||||
if mask.nick == self.bot.nick:
|
if mask.nick == self.bot.nick:
|
||||||
# the bot just joined a channel
|
# the bot just joined a channel
|
||||||
if channel in self.get_managed_team_channels():
|
if channel in self.get_managed_team_channels() or channel == settings.IRCBOT_PUBLIC_CHANNEL or channel == settings.IRCBOT_VOLUNTEER_CHANNEL:
|
||||||
logger.debug("Just joined a channel I am supposed to be managing, asking ChanServ for info about %s" % channel)
|
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)
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "info %s" % channel)
|
||||||
return
|
return
|
||||||
|
@ -93,7 +95,7 @@ class Plugin(object):
|
||||||
|
|
||||||
|
|
||||||
###############################################################################################
|
###############################################################################################
|
||||||
### custom irc3 methods
|
### custom irc3 methods below here
|
||||||
|
|
||||||
@irc3.extend
|
@irc3.extend
|
||||||
def do_stuff(self):
|
def do_stuff(self):
|
||||||
|
@ -151,7 +153,7 @@ class Plugin(object):
|
||||||
Compare the list of IRC channels the bot is currently in with the list of IRC channels the bot is supposed to be in.
|
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.
|
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]))
|
desired_channel_list = self.bot.get_desired_channel_list()
|
||||||
#logger.debug("Inside check_irc_channels(), desired_channel_list is: %s and self.bot.channels is: %s" % (desired_channel_list, self.bot.channels.keys()))
|
#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
|
# loop over desired_channel_list, join as needed
|
||||||
|
@ -167,70 +169,122 @@ class Plugin(object):
|
||||||
self.bot.part(channel, "I am no longer needed here")
|
self.bot.part(channel, "I am no longer needed here")
|
||||||
|
|
||||||
|
|
||||||
|
@irc3.extend
|
||||||
|
def get_desired_channel_list(self):
|
||||||
|
"""
|
||||||
|
Return a list of strings of all the IRC channels the bot is supposed to be in
|
||||||
|
"""
|
||||||
|
desired_channel_list = self.get_managed_team_channels()
|
||||||
|
desired_channel_list += self.get_unmanaged_team_channels()
|
||||||
|
desired_channel_list.append(settings.IRCBOT_PUBLIC_CHANNEL)
|
||||||
|
desired_channel_list.append(settings.IRCBOT_VOLUNTEER_CHANNEL)
|
||||||
|
return desired_channel_list
|
||||||
|
|
||||||
|
|
||||||
@irc3.extend
|
@irc3.extend
|
||||||
def get_managed_team_channels(self):
|
def get_managed_team_channels(self):
|
||||||
"""
|
"""
|
||||||
Return a unique list of team IRC channels which the bot is supposed to be managing.
|
Return a list of team IRC channels which the bot is supposed to be managing.
|
||||||
"""
|
"""
|
||||||
return Team.objects.filter(
|
pubchans = Team.objects.filter(
|
||||||
irc_channel=True,
|
public_irc_channel_name__isnull=False,
|
||||||
irc_channel_managed=True
|
public_irc_channel_bot=True,
|
||||||
).values_list("irc_channel_name", flat=True)
|
public_irc_channel_managed=True
|
||||||
|
).values_list("public_irc_channel_name", flat=True)
|
||||||
|
|
||||||
|
privchans = Team.objects.filter(
|
||||||
|
private_irc_channel_name__isnull=False,
|
||||||
|
private_irc_channel_bot=True,
|
||||||
|
private_irc_channel_managed=True
|
||||||
|
).values_list("private_irc_channel_name", flat=True)
|
||||||
|
|
||||||
|
return list(pubchans) + list(privchans)
|
||||||
|
|
||||||
|
|
||||||
@irc3.extend
|
@irc3.extend
|
||||||
def get_unmanaged_team_channels(self):
|
def get_unmanaged_team_channels(self):
|
||||||
"""
|
"""
|
||||||
Return a unique list of team IRC channels which the bot is not supposed to be managing.
|
Return a list of team IRC channels which the bot is supposed to be in, but not managing.
|
||||||
"""
|
"""
|
||||||
return Team.objects.filter(
|
pubchans = Team.objects.filter(
|
||||||
irc_channel=True,
|
public_irc_channel_name__isnull=False,
|
||||||
irc_channel_managed=False
|
public_irc_channel_bot=True,
|
||||||
).values_list("irc_channel_name", flat=True)
|
public_irc_channel_managed=False
|
||||||
|
).values_list("public_irc_channel_name", flat=True)
|
||||||
|
|
||||||
|
privchans = Team.objects.filter(
|
||||||
|
private_irc_channel_name__isnull=False,
|
||||||
|
private_irc_channel_bot=True,
|
||||||
|
private_irc_channel_managed=False
|
||||||
|
).values_list("private_irc_channel_name", flat=True)
|
||||||
|
|
||||||
|
return list(pubchans) + list(privchans)
|
||||||
|
|
||||||
|
|
||||||
@irc3.extend
|
@irc3.extend
|
||||||
def setup_private_channel(self, team):
|
def setup_private_channel(self, channel):
|
||||||
"""
|
"""
|
||||||
Configures a private team IRC channel by setting modes and adding all members to ACL
|
Configures a private IRC channel by setting modes and adding all members to ACL if it is a team channel
|
||||||
"""
|
"""
|
||||||
|
logger.debug("Inside setup_private_channel() for %s" % channel)
|
||||||
|
|
||||||
# basic private channel modes
|
# 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 mlock +inpst" % channel)
|
||||||
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE on" % team.irc_channel_name)
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE on" % channel)
|
||||||
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED on" % team.irc_channel_name)
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED on" % channel)
|
||||||
|
|
||||||
# add the bot to the ACL
|
# add the bot to the ACL
|
||||||
self.bot.add_user_to_team_channel_acl(
|
self.bot.add_user_to_channel_acl(
|
||||||
username=settings.IRCBOT_NICK,
|
username=settings.IRCBOT_NICK,
|
||||||
channel=team.irc_channel_name
|
channel=channel,
|
||||||
|
invite=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
team = get_team_from_irc_channel(channel)
|
||||||
|
if team:
|
||||||
|
# this is a team channel, add team members to channel ACL
|
||||||
|
self.bot.add_team_members_to_channel_acl(team)
|
||||||
|
# make sure private_irc_channel_fix_needed is set to False and save
|
||||||
|
team.private_irc_channel_fix_needed=False
|
||||||
|
team.save()
|
||||||
|
|
||||||
|
|
||||||
|
@irc3.extend
|
||||||
|
def setup_public_channel(self, channel):
|
||||||
|
"""
|
||||||
|
Configures a public IRC channel by setting modes and giving all team members +oO if it is a team channel
|
||||||
|
"""
|
||||||
|
logger.debug("Inside setup_public_channel() for %s" % channel)
|
||||||
|
|
||||||
|
# basic private channel modes
|
||||||
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s mlock +nt-lk" % channel)
|
||||||
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE off" % channel)
|
||||||
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED off" % channel)
|
||||||
|
|
||||||
|
team = get_team_from_irc_channel(channel)
|
||||||
|
if team:
|
||||||
|
# add members to ACL
|
||||||
|
self.bot.add_team_members_to_channel_acl(team)
|
||||||
|
# make sure public_irc_channel_fix_needed is set to False and save
|
||||||
|
team.public_irc_channel_fix_needed=False
|
||||||
|
team.save()
|
||||||
|
|
||||||
|
|
||||||
|
@irc3.extend
|
||||||
|
def add_team_members_to_channel_acl(self, team):
|
||||||
|
"""
|
||||||
|
Handles initial ACL for team channels.
|
||||||
|
Sets membership.irc_acl_fix_needed=True for each approved teammember with a NickServ username
|
||||||
|
"""
|
||||||
# add all members to the acl
|
# add all members to the acl
|
||||||
for membership in team.memberships.all():
|
for membership in team.memberships.all():
|
||||||
if membership.approved and membership.user.profile.nickserv_username:
|
if membership.approved and membership.user.profile.nickserv_username:
|
||||||
self.bot.add_user_to_team_channel_acl(
|
membership.irc_acl_fix_needed=True
|
||||||
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()
|
membership.save()
|
||||||
|
|
||||||
|
|
||||||
@irc3.extend
|
@irc3.extend
|
||||||
def setup_public_channel(self, team):
|
def add_user_to_channel_acl(self, username, channel, invite):
|
||||||
"""
|
|
||||||
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
|
Add user to team IRC channel ACL
|
||||||
"""
|
"""
|
||||||
|
@ -243,6 +297,7 @@ class Plugin(object):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if invite:
|
||||||
# also add autoinvite for this username
|
# also add autoinvite for this username
|
||||||
self.bot.mode(channel, '+I', '$a:%s' % username)
|
self.bot.mode(channel, '+I', '$a:%s' % username)
|
||||||
|
|
||||||
|
@ -251,13 +306,11 @@ class Plugin(object):
|
||||||
def fix_missing_acls(self):
|
def fix_missing_acls(self):
|
||||||
"""
|
"""
|
||||||
Called periodically by do_stuff()
|
Called periodically by do_stuff()
|
||||||
Loops over TeamMember objects and adds and removes ACL entries as needed
|
Loops over TeamMember objects and adds ACL entries as needed
|
||||||
|
Loops over Team objects and fixes permissions and ACLS as needed
|
||||||
"""
|
"""
|
||||||
missing_acls = TeamMember.objects.filter(
|
missing_acls = TeamMember.objects.filter(
|
||||||
team__irc_channel=True,
|
irc_acl_fix_needed=True
|
||||||
team__irc_channel_managed=True,
|
|
||||||
team__irc_channel_private=True,
|
|
||||||
irc_channel_acl_ok=False
|
|
||||||
).exclude(
|
).exclude(
|
||||||
user__profile__nickserv_username=''
|
user__profile__nickserv_username=''
|
||||||
)
|
)
|
||||||
|
@ -267,14 +320,41 @@ class Plugin(object):
|
||||||
|
|
||||||
logger.debug("Found %s memberships which need IRC ACL fixing.." % missing_acls.count())
|
logger.debug("Found %s memberships which need IRC ACL fixing.." % missing_acls.count())
|
||||||
for membership in missing_acls:
|
for membership in missing_acls:
|
||||||
self.bot.add_user_to_team_channel_acl(
|
# add to team public channel?
|
||||||
|
if membership.team.public_channel_name and membership.publíc_channel_managed:
|
||||||
|
self.bot.add_user_to_channel_acl(
|
||||||
username=membership.user.profile.nickserv_username,
|
username=membership.user.profile.nickserv_username,
|
||||||
channel=membership.team.irc_channel_name,
|
channel=membership.team.public_irc_channel_name,
|
||||||
|
invite=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# add to team private channel?
|
||||||
|
if membership.team.private_channel_name and membership.private_channel_managed:
|
||||||
|
self.bot.add_user_to_channel_acl(
|
||||||
|
username=membership.user.profile.nickserv_username,
|
||||||
|
channel=membership.team.private_irc_channel_name,
|
||||||
|
invite=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# add to volunteer channel
|
||||||
|
self.bot.add_user_to_channel_acl(
|
||||||
|
username=membership.user.profile.nickserv_username,
|
||||||
|
chanel=settings.IRCBOT_VOLUNTEER_CHANNEL,
|
||||||
|
invite=True
|
||||||
|
)
|
||||||
|
|
||||||
# mark membership as irc_channel_acl_ok=True and save
|
# mark membership as irc_channel_acl_ok=True and save
|
||||||
membership.irc_channel_acl_ok=True
|
membership.irc_acl_fix_neede=False
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
|
for team in Team.objects.filter(private_irc_channel_fix_needed=True):
|
||||||
|
logger.debug("Team %s private IRC channel %s needs ACL fixing" % (team, team.private_irc_channel_name))
|
||||||
|
self.bot.setup_private_channel(team.private_irc_channel_name)
|
||||||
|
|
||||||
|
for team in Team.objects.filter(public_irc_channel_fix_needed=True):
|
||||||
|
logger.debug("Team %s public IRC channel %s needs ACL fixing" % (team, team.public_irc_channel_name))
|
||||||
|
self.bot.setup_public_channel(team.public_irc_channel_name)
|
||||||
|
|
||||||
|
|
||||||
###############################################################################################
|
###############################################################################################
|
||||||
### services (ChanServ & NickServ) methods
|
### services (ChanServ & NickServ) methods
|
||||||
|
@ -294,8 +374,8 @@ class Plugin(object):
|
||||||
# the irc channel is not registered
|
# the irc channel is not registered
|
||||||
channel = match.group(1)
|
channel = match.group(1)
|
||||||
# get a list of the channels we are supposed to be managing
|
# get a list of the channels we are supposed to be managing
|
||||||
if channel in self.bot.get_managed_team_channels():
|
if channel in self.bot.get_managed_team_channels() or channel == settings.IRCBOT_VOLUNTEER_CHANNEL:
|
||||||
# we want to register this channel! but we can only do so if we have a @ in the channel
|
# we want to register this channel! though we can only do so if we have a @ in the channel
|
||||||
if self.bot.nick in self.bot.channels[channel].modes['@']:
|
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)
|
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)
|
self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "register %s" % channel)
|
||||||
|
@ -314,19 +394,22 @@ class Plugin(object):
|
||||||
botnick = match.group(2)
|
botnick = match.group(2)
|
||||||
logger.debug("Channel %s was registered with ChanServ, looking up Team..." % channel)
|
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
|
team = get_team_from_irc_channel(channel)
|
||||||
try:
|
if team:
|
||||||
team = Team.objects.get(irc_channel_name=channel)
|
if team.private_irc_channel_name == channel:
|
||||||
except Team.DoesNotExist:
|
# set private channel modes, +I and ACL
|
||||||
|
self.bot.setup_private_channel(channel)
|
||||||
|
else:
|
||||||
|
# set public channel modes and +oO for all members
|
||||||
|
self.bot.setup_public_channel(channel)
|
||||||
|
return
|
||||||
logger.debug("Unable to find Team matching IRC channel %s" % channel)
|
logger.debug("Unable to find Team matching IRC channel %s" % channel)
|
||||||
return
|
|
||||||
|
|
||||||
if not team.irc_channel_private:
|
# check if this channel is the volunteer channel
|
||||||
# this channel is not private, no mode change and ACL needed
|
if channel == settings.IRCBOT_VOLUNTEER_CHANNEL:
|
||||||
return
|
logger.debug("%s is the volunteer channel, setting up" channel)
|
||||||
|
self.bot.setup_private_channel(channel)
|
||||||
# set channel modes and ACL
|
# lets handle the volunteer channels initial ACL manually..
|
||||||
self.bot.setup_private_channel(team)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug("Unhandled ChanServ message: %s" % kwargs['data'])
|
logger.debug("Unhandled ChanServ message: %s" % kwargs['data'])
|
||||||
|
@ -334,7 +417,7 @@ class Plugin(object):
|
||||||
|
|
||||||
@irc3.extend
|
@irc3.extend
|
||||||
def handle_nickserv_privmsg(self, **kwargs):
|
def handle_nickserv_privmsg(self, **kwargs):
|
||||||
"""th
|
"""
|
||||||
Handles messages from NickServ on networks with Services.
|
Handles messages from NickServ on networks with Services.
|
||||||
"""
|
"""
|
||||||
logger.debug("Got a message from NickServ")
|
logger.debug("Got a message from NickServ")
|
||||||
|
|
|
@ -57,24 +57,29 @@ def public_credit_name_changed(instance, original):
|
||||||
|
|
||||||
def nickserv_username_changed(instance, original):
|
def nickserv_username_changed(instance, original):
|
||||||
"""
|
"""
|
||||||
Check if profile.nickserv_username was changed, and uncheck irc_channel_acl_ok if so
|
Check if profile.nickserv_username was changed, and check irc_acl_fix_needed if so
|
||||||
This will be picked up by the IRC bot and fixed as needed
|
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:
|
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)
|
logger.debug("profile.nickserv_username changed for user %s, setting membership.irc_acl_fix_needed=True" % instance.user.username)
|
||||||
|
|
||||||
# find team memberships for this user
|
# find team memberships for this user
|
||||||
from teams.models import TeamMember
|
from teams.models import TeamMember
|
||||||
memberships = TeamMember.objects.filter(
|
memberships = TeamMember.objects.filter(
|
||||||
user=instance.user,
|
user=instance.user,
|
||||||
approved=True,
|
approved=True,
|
||||||
team__irc_channel=True,
|
|
||||||
team__irc_channel_managed=True,
|
|
||||||
team__irc_channel_private=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# loop over memberships
|
# loop over memberships
|
||||||
for membership in memberships:
|
for membership in memberships:
|
||||||
membership.irc_channel_acl_ok = False
|
if not membership.team.public_irc_channel_name and not membership.team.private_irc_channel_name:
|
||||||
|
# no irc channels for this team
|
||||||
|
continue
|
||||||
|
if not membership.team.public_irc_channel_managed and not membership.team.private_irc_channel_managed:
|
||||||
|
# irc channel(s) are not managed for this team
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ok, mark this membership as in need of fixing
|
||||||
|
membership.irc_acl_fix_needed = False
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
|
|
45
src/teams/migrations/0038_auto_20180412_1844.py
Normal file
45
src/teams/migrations/0038_auto_20180412_1844.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2018-04-12 16:44
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('teams', '0037_auto_20180408_1416'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='team',
|
||||||
|
name='private_irc_channel_bot',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check to make the bot join the teams private IRC channel. Leave unchecked to disable the IRC bot for this channel.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='team',
|
||||||
|
name='private_irc_channel_managed',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check to make the bot manage the private IRC channel by registering it with NickServ, setting +I and maintaining the ACL.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='team',
|
||||||
|
name='private_irc_channel_name',
|
||||||
|
field=models.CharField(blank=True, help_text='The private IRC channel for this team. Will be shown to team members on the team page. Leave empty if the team has no private IRC channel.', max_length=50, null=True, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='team',
|
||||||
|
name='public_irc_channel_bot',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check to make the bot join the teams public IRC channel. Leave unchecked to disable the IRC bot for this channel.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='team',
|
||||||
|
name='public_irc_channel_managed',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check to make the bot manage the teams public IRC channel by registering it with NickServ.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='team',
|
||||||
|
name='public_irc_channel_name',
|
||||||
|
field=models.CharField(blank=True, help_text='The public IRC channel for this team. Will be shown on the team page so people know how to reach the team. Leave empty if the team has no public IRC channel.', max_length=50, null=True, unique=True),
|
||||||
|
),
|
||||||
|
]
|
33
src/teams/migrations/0039_fix_irc_channels.py
Normal file
33
src/teams/migrations/0039_fix_irc_channels.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2018-04-12 15:46
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
def fix_irc_channels(apps, schema_editor):
|
||||||
|
Team = apps.get_model('teams', 'Team')
|
||||||
|
for team in Team.objects.filter(irc_channel=True):
|
||||||
|
print("fixing irc channel for team %s" % team.name)
|
||||||
|
if team.irc_channel_private:
|
||||||
|
team.private_irc_channel_name=team.irc_channel_name
|
||||||
|
if team.irc_channel_managed:
|
||||||
|
team.private_irc_channel_managed=True
|
||||||
|
team.private_irc_channel_bot=True
|
||||||
|
else:
|
||||||
|
team.public_irc_channel_name=team.irc_channel_name
|
||||||
|
if team.irc_channel_managed:
|
||||||
|
team.public_irc_channel_managed=True
|
||||||
|
team.public_irc_channel_bot=True
|
||||||
|
team.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('teams', '0038_auto_20180412_1844'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(fix_irc_channels),
|
||||||
|
]
|
||||||
|
|
31
src/teams/migrations/0040_auto_20180412_2109.py
Normal file
31
src/teams/migrations/0040_auto_20180412_2109.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2018-04-12 19:09
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('teams', '0039_fix_irc_channels'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='team',
|
||||||
|
name='irc_channel',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='team',
|
||||||
|
name='irc_channel_managed',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='team',
|
||||||
|
name='irc_channel_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='team',
|
||||||
|
name='irc_channel_private',
|
||||||
|
),
|
||||||
|
]
|
24
src/teams/migrations/0041_auto_20180412_2231.py
Normal file
24
src/teams/migrations/0041_auto_20180412_2231.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2018-04-12 20:31
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('teams', '0040_auto_20180412_2109'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='teammember',
|
||||||
|
name='irc_channel_acl_ok',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='teammember',
|
||||||
|
name='irc_acl_fix_needed',
|
||||||
|
field=models.BooleanField(default=False, help_text='Maintained by the IRC bot, manual editing should not be needed. Will be set to true when a teammember sets or changes NickServ username, and back to false after the ACL has been fixed by the bot.'),
|
||||||
|
),
|
||||||
|
]
|
30
src/teams/migrations/0042_auto_20180413_1933.py
Normal file
30
src/teams/migrations/0042_auto_20180413_1933.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2018-04-13 17:33
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('teams', '0041_auto_20180412_2231'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='team',
|
||||||
|
name='private_irc_channel_fix_needed',
|
||||||
|
field=models.BooleanField(default=False, help_text='Used to indicate to the IRC bot that this teams private IRC channel is in need of a permissions and ACL fix.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='team',
|
||||||
|
name='public_irc_channel_fix_needed',
|
||||||
|
field=models.BooleanField(default=False, help_text='Used to indicate to the IRC bot that this teams public IRC channel is in need of a permissions and ACL fix.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='team',
|
||||||
|
name='public_irc_channel_managed',
|
||||||
|
field=models.BooleanField(default=False, help_text='Check to make the bot manage the teams public IRC channel by registering it with NickServ and setting +Oo for all teammembers.'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -6,6 +6,7 @@ from utils.models import CampRelatedModel
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
|
from django.conf import settings
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
@ -61,25 +62,44 @@ class Team(CampRelatedModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
# IRC related fields
|
# IRC related fields
|
||||||
irc_channel = models.BooleanField(
|
public_irc_channel_name = models.CharField(
|
||||||
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,
|
blank=True,
|
||||||
help_text='Team IRC channel. Leave blank to generate channel name automatically, based on camp shortslug and team shortslug.',
|
null=True,
|
||||||
|
unique=True,
|
||||||
|
max_length=50,
|
||||||
|
help_text='The public IRC channel for this team. Will be shown on the team page so people know how to reach the team. Leave empty if the team has no public IRC channel.'
|
||||||
|
)
|
||||||
|
public_irc_channel_bot = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='Check to make the bot join the teams public IRC channel. Leave unchecked to disable the IRC bot for this channel.'
|
||||||
|
)
|
||||||
|
public_irc_channel_managed = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='Check to make the bot manage the teams public IRC channel by registering it with NickServ and setting +Oo for all teammembers.'
|
||||||
|
)
|
||||||
|
public_irc_channel_fix_needed = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='Used to indicate to the IRC bot that this teams public IRC channel is in need of a permissions and ACL fix.'
|
||||||
)
|
)
|
||||||
|
|
||||||
irc_channel_managed = models.BooleanField(
|
private_irc_channel_name = models.CharField(
|
||||||
default=True,
|
blank=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.',
|
null=True,
|
||||||
|
unique=True,
|
||||||
|
max_length=50,
|
||||||
|
help_text='The private IRC channel for this team. Will be shown to team members on the team page. Leave empty if the team has no private IRC channel.'
|
||||||
)
|
)
|
||||||
|
private_irc_channel_bot = models.BooleanField(
|
||||||
irc_channel_private = models.BooleanField(
|
default=False,
|
||||||
default=True,
|
help_text='Check to make the bot join the teams private IRC channel. Leave unchecked to disable the IRC bot for this channel.'
|
||||||
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.'
|
)
|
||||||
|
private_irc_channel_managed = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='Check to make the bot manage the private IRC channel by registering it with NickServ, setting +I and maintaining the ACL.'
|
||||||
|
)
|
||||||
|
private_irc_channel_fix_needed = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text='Used to indicate to the IRC bot that this teams private IRC channel is in need of a permissions and ACL fix.'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -95,23 +115,36 @@ class Team(CampRelatedModel):
|
||||||
slug = slugify(self.name)
|
slug = slugify(self.name)
|
||||||
self.slug = slug
|
self.slug = slug
|
||||||
|
|
||||||
|
# set shortslug if needed
|
||||||
if not self.shortslug:
|
if not self.shortslug:
|
||||||
self.shortslug = self.slug
|
self.shortslug = self.slug
|
||||||
|
|
||||||
# generate IRC channel name if needed
|
|
||||||
if self.irc_channel and not self.irc_channel_name:
|
|
||||||
self.irc_channel_name = "#%s-%s" % (self.camp.shortslug, self.shortslug)
|
|
||||||
|
|
||||||
super().save(**kwargs)
|
super().save(**kwargs)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# make sure the irc channel name is prefixed with a # if it is set
|
# make sure the public irc channel name is prefixed with a # if it is set
|
||||||
if self.irc_channel_name and self.irc_channel_name[0] != "#":
|
if self.public_irc_channel_name and self.public_irc_channel_name[0] != "#":
|
||||||
self.irc_channel_name = "#%s" % self.irc_channel_name
|
self.public_irc_channel_name = "#%s" % self.public_irc_channel_name
|
||||||
|
|
||||||
if self.irc_channel_name:
|
# make sure the private irc channel name is prefixed with a # if it is set
|
||||||
if Team.objects.filter(irc_channel_name=self.irc_channel_name).exclude(pk=self.pk).exists():
|
if self.private_irc_channel_name and self.private_irc_channel_name[0] != "#":
|
||||||
raise ValidationError("This IRC channel name is already in use")
|
self.private_irc_channel_name = "#%s" % self.private_irc_channel_name
|
||||||
|
|
||||||
|
# make sure the channel names are not reserved
|
||||||
|
if self.public_irc_channel_name == settings.IRCBOT_PUBLIC_CHANNEL or self.public_irc_channel_name == settings.IRCBOT_VOLUNTEER_CHANNEL:
|
||||||
|
raise ValidationError('The public IRC channel name is reserved')
|
||||||
|
if self.private_irc_channel_name == settings.IRCBOT_PUBLIC_CHANNEL or self.private_irc_channel_name == settings.IRCBOT_VOLUNTEER_CHANNEL:
|
||||||
|
raise ValidationError('The private IRC channel name is reserved')
|
||||||
|
|
||||||
|
# make sure public_irc_channel_name is unique
|
||||||
|
if self.public_irc_channel_name:
|
||||||
|
if Team.objects.filter(private_irc_channel_name=self.public_irc_channel_name).exclude(pk=self.pk).exists():
|
||||||
|
raise ValidationError('The public IRC channel name is already in use!')
|
||||||
|
|
||||||
|
# make sure private_irc_channel_name is unique
|
||||||
|
if self.private_irc_channel_name:
|
||||||
|
if Team.objects.filter(public_irc_channel_name=self.private_irc_channel_name).exclude(pk=self.pk).exists():
|
||||||
|
raise ValidationError('The private IRC channel name is already in use!')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def memberships(self):
|
def memberships(self):
|
||||||
|
@ -201,9 +234,9 @@ class TeamMember(CampRelatedModel):
|
||||||
help_text="True if this teammember is responsible for this Team. False if not."
|
help_text="True if this teammember is responsible for this Team. False if not."
|
||||||
)
|
)
|
||||||
|
|
||||||
irc_channel_acl_ok = models.BooleanField(
|
irc_acl_fix_needed = models.BooleanField(
|
||||||
default=False,
|
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.",
|
help_text='Maintained by the IRC bot, manual editing should not be needed. Will be set to true when a teammember sets or changes NickServ username, and back to false after the ACL has been fixed by the bot.',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -15,17 +15,16 @@ def teammember_saved(sender, instance, created, **kwargs):
|
||||||
if not add_new_membership_email(instance):
|
if not add_new_membership_email(instance):
|
||||||
logger.error('Error adding email to outgoing queue')
|
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):
|
def teammember_deleted(sender, instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
This signal handler is called whenever a TeamMember instance is deleted
|
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:
|
if instance.team.private_irc_channel_name and instance.team.private_irc_channel_managed:
|
||||||
# TODO: we have an ACL entry that needs to be deleted but the bot does not handle it automatically
|
# TODO: remove user from private channel ACL
|
||||||
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))
|
pass
|
||||||
|
|
||||||
|
if instance.team.public_irc_channel_name and instance.team.public_irc_channel_managed:
|
||||||
|
# TODO: remove user from public channel ACL
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
|
@ -28,15 +28,16 @@ Team: {{ team.name }} | {{ block.super }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h5>IRC Channel</h5>
|
<h5>IRC Channel</h5>
|
||||||
{% if team.irc_channel and request.user in team.approved_members.all %}
|
{% if team.public_irc_channel_name %}
|
||||||
<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>.
|
<p>The {{ team.name }} Team public IRC channel is <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ team.public_irc_channel_name }}">{{ team.public_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 %}
|
{% 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>
|
<p>The {{ team.name }} Team does not have a public IRC channel, but it can be reached through our main IRC channel <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ IRCBOT_PUBLIC_CHANNEL }}">{{ IRCBOT_PUBLIC_CHANNEL }} on {{ IRCBOT_SERVER_HOSTNAME }}</a>.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if request.user in team.approved_members.all and team.private_irc_channel_name %}
|
||||||
|
<p>The {{ team.name }} Team private IRC channel is <a href="irc://{{ IRCBOT_SERVER_HOSTNAME }}/{{ team.private_irc_channel_name }}">{{ team.private_irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}</a>.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<h4>{{ team.name }} Team Members</h4>
|
<h4>{{ team.name }} Team Members</h4>
|
||||||
|
|
18
src/teams/utils.py
Normal file
18
src/teams/utils.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from .models import Team
|
||||||
|
|
||||||
|
def get_team_from_irc_channel(channel):
|
||||||
|
"""
|
||||||
|
Returns a Team object given an IRC channel name, if possible
|
||||||
|
"""
|
||||||
|
# check if this channel is a private_irc_channel for a team
|
||||||
|
try:
|
||||||
|
return Team.objects.get(private_irc_channel_name=channel)
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# check if this channel is a public_irc_channel for a team
|
||||||
|
try:
|
||||||
|
return Team.objects.get(public_irc_channel_name=channel)
|
||||||
|
except Team.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
|
@ -70,7 +70,7 @@ class TeamDetailView(CampViewMixin, DetailView):
|
||||||
class TeamManageView(CampViewMixin, EnsureTeamResponsibleMixin, UpdateView):
|
class TeamManageView(CampViewMixin, EnsureTeamResponsibleMixin, UpdateView):
|
||||||
model = Team
|
model = Team
|
||||||
template_name = "team_manage.html"
|
template_name = "team_manage.html"
|
||||||
fields = ['description', 'needs_members', 'irc_channel', 'irc_channel_name', 'irc_channel_managed', 'irc_channel_private']
|
fields = ['description', 'needs_members', 'public_irc_channel_name', 'public_irc_channel_bot', 'public_irc_channel_managed', 'private_irc_channel_name', 'private_irc_channel_bot', 'private_irc_channel_managed']
|
||||||
slug_url_kwarg = 'team_slug'
|
slug_url_kwarg = 'team_slug'
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
|
|
@ -1631,8 +1631,10 @@ Please note that sleeping in the parking lot is not permitted. If you want to sl
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
self.output("marking 2016 as read_only...")
|
self.output("marking 2016 and 2017 as read_only...")
|
||||||
camp2016.read_only = True
|
camp2016.read_only = True
|
||||||
camp2016.save()
|
camp2016.save()
|
||||||
|
camp2017.read_only = True
|
||||||
|
camp2017.save()
|
||||||
self.output("done!")
|
self.output("done!")
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue