jenkins-bot has submitted this change and it was merged. ( https://gerrit.wikimedia.org/r/364135 )
Change subject: Manage tool maintainers ...................................................................... Manage tool maintainers Add support of adding and removing tool maintainers. Also support adding and removing other tools accounts. This is needed to allow the legacy practice of using a tool account as a library source for one or more dependent tools. Bug: T149458 Bug: T128400 Change-Id: I84e8452b0c592283a6deb7c577f6ec3f037489ce --- M striker/openstack.py M striker/register/utils.py M striker/settings.py M striker/templates/tools/index.html A striker/templates/tools/maintainers.html M striker/templates/tools/tool.html A striker/tools/cache.py M striker/tools/forms.py M striker/tools/migrations/0001_squashed.py M striker/tools/models.py M striker/tools/urls.py M striker/tools/views.py 12 files changed, 429 insertions(+), 98 deletions(-) Approvals: BryanDavis: Looks good to me, approved jenkins-bot: Verified diff --git a/striker/openstack.py b/striker/openstack.py index 6a20d02..6c5c166 100644 --- a/striker/openstack.py +++ b/striker/openstack.py @@ -81,11 +81,14 @@ """Convenience method for getting a client with super user rights.""" return self._client(project='admin', interface='admin') - def role(self, name): + def _roles(self): if self.roles is None: keystone = self._client() self.roles = {r.name: r for r in keystone.roles.list()} - return self.roles[name] + return self.roles + + def role(self, name): + return self._roles()[name] def grant_role(self, role, user, project=None): project = project or self.project @@ -98,3 +101,18 @@ # We need global admin rights to change role assignments keystone = self._admin_client() keystone.roles.revoke(role, user=user, project=project) + + def users_by_role(self, project=None): + project = project or self.project + keystone = self._client() + # Ignore novaadmin & novaobserver in all user lists + seen = ['novaadmin', 'novaobserver'] + data = {} + for role_name, role_id in self._roles().items(): + data[role_name] = [ + r.user['id'] for r in keystone.role_assignments.list( + project=project, role=role_id) + if r.user['id'] not in seen + ] + seen += data[role_name] + return data diff --git a/striker/register/utils.py b/striker/register/utils.py index 2cacc45..c43541b 100644 --- a/striker/register/utils.py +++ b/striker/register/utils.py @@ -27,7 +27,7 @@ from striker import mediawiki from striker.labsauth.models import LabsUser -from striker.tools.models import Maintainer +from striker.labsauth.models import PosixAccount logger = logging.getLogger(__name__) @@ -35,6 +35,7 @@ def sul_available(name): try: + # TODO: change to LdapUser once T148048 is done LabsUser.objects.get(sulname=name) except LabsUser.DoesNotExist: return True @@ -82,8 +83,9 @@ def username_available(name): try: - Maintainer.objects.get(full_name=name) - except Maintainer.DoesNotExist: + # Check vs any posix account + PosixAccount.objects.get(cn=name) + except PosixAccount.DoesNotExist: return True else: return False @@ -91,8 +93,9 @@ def shellname_available(name): try: - Maintainer.objects.get(username=name) - except Maintainer.DoesNotExist: + # Check vs any posix account + PosixAccount.objects.get(uid=name) + except PosixAccount.DoesNotExist: return True else: return False diff --git a/striker/settings.py b/striker/settings.py index 0afdbad..584d957 100644 --- a/striker/settings.py +++ b/striker/settings.py @@ -21,6 +21,7 @@ import configparser import ldap import os +import sys import django_auth_ldap.config @@ -38,6 +39,10 @@ '/etc/striker/striker.ini', ]) +# Hack so that we can guard things that will probably fail miserably in test +# like contacting an external server +TEST_MODE = 'test' in sys.argv + # == Logging == LOGGING = { 'version': 1, diff --git a/striker/templates/tools/index.html b/striker/templates/tools/index.html index 7ee9bfb..f30c5d4 100644 --- a/striker/templates/tools/index.html +++ b/striker/templates/tools/index.html @@ -11,9 +11,6 @@ <div class="panel-heading">{% bootstrap_icon "star" %} {% trans "Your tools" %}</div> {% if not user.is_anonymous %} <div class="list-group"> - {% if member %} - <a class="list-group-item" href="{% url 'tools:tool_create' %}">{% bootstrap_icon "plus-sign" %} {% trans "Create Tool account" %}</a> - {% endif %} {% for tool in my_tools %} <a href="{% url 'tools:tool' tool=tool %}" class="list-group-item">{{ tool.name }}</a> {% empty %} @@ -22,21 +19,25 @@ {% else %} <div class="list-group-item"> <p>{% blocktrans %}Request membership in the Toolforge project so you can create your own tool and help maintain existing tools.{% endblocktrans %}</p> - <p><a href="{% url 'tools:membership_apply' %}" class="btn btn-default">{% trans "Join now" %}</a></p> + <a href="{% url 'tools:membership_apply' %}" class="btn btn-success">{% trans "Join now" %}</a> </div> {% endif %} {% endfor %} </div> - {% else %} + {% if member %} + <div class="list-group"> + <a class="list-group-item" href="{% url 'tools:tool_create' %}">{% bootstrap_icon "plus-sign" %} {% trans "Create new tool" %}</a> + </div> + {% endif %} + {% else %}{# user.is_anonymous #} <div class="panel-body"> {% url 'labsauth:login' as login_url %} {% url 'tools:index' as tools_url %} {% blocktrans with login_url=login_url tools_url=tools_url %} - <p><a href="{{ login_url }}?next={{ tools_url }}">Login</a> to see your tools.</p> + <a href="{{ login_url }}?next={{ tools_url }}">Login</a> to see your tools. {% endblocktrans %} </div> {% endif %} - <div class="panel-footer"></div> </div> <div class="panel panel-default"> <ul class="nav nav-pills nav-stacked"> diff --git a/striker/templates/tools/maintainers.html b/striker/templates/tools/maintainers.html new file mode 100644 index 0000000..f9a47af --- /dev/null +++ b/striker/templates/tools/maintainers.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% load bootstrap3 %} +{% load i18n %} +{% load staticfiles %} + +{% block title %}{% blocktrans with name=tool.name %}Maintainers: {{ name }}{% endblocktrans %}{% endblock %} + +{% block pre_content %} +<ol class="breadcrumb"> + <li><a href="{% url 'tools:index' %}">{% trans "Tools" %}</a></li> + <li><a href="{% url 'tools:tool' tool=tool.name %}">{{ tool.name }}</a></li> + <li class="active">{% trans "Maintainers" %}</li> +</ol> +{{ block.super }} +{% endblock %} + +{% block content %} +<div class="container-fluid"> + <div class="row"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h3 class="panel-title">{% bootstrap_icon "user" %} {% trans "Maintainers" %}</h3> + </div> + <div class="panel-body"> + <form method="post" action="{% url 'tools:maintainers' tool=tool.name %}" class="form parsley"> + {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + <input class="btn btn-success" type="submit" value="{% trans "Update" %}" /> + {% endbuttons %} + </form> + </div> + </div> + </div> +</div> +{% endblock %} + +{% block js %} +{{ block.super }} +<script lang="javascript" src="{% static 'js/parsley.min.js' %}"></script> +<script lang="javascript" src="{% static 'js/parsley-bootstrap.js' %}"></script> +{{ form.media }} +{% endblock %} diff --git a/striker/templates/tools/tool.html b/striker/templates/tools/tool.html index a97c1e4..b4b1d01 100644 --- a/striker/templates/tools/tool.html +++ b/striker/templates/tools/tool.html @@ -13,67 +13,72 @@ {% endblock %} {% block content %} -<div class="panel-group" role="tablist"> - {% for info in toolinfo %} - <div class="panel panel-info panel-pager"> - {% include "tools/info/revision/header.html" with toolinfo=info %} - {% include "tools/info/revision/body.html" with toolinfo=info %} - {% if forloop.last and can_edit %} - <div class="panel-body"> - <a href="{% url 'tools:info_create' tool=tool.name %}" class="btn btn-default btn-sm pull-right">{% bootstrap_icon "plus" %} {% trans "add toolinfo" %}</a> +<div class="container-fluid"> + <div class="row"> + <div class="col-sm-9 col-sm-push-3 panel-group"> + {% for info in toolinfo %} + <div class="panel panel-info panel-pager"> + {% include "tools/info/revision/header.html" with toolinfo=info %} + {% include "tools/info/revision/body.html" with toolinfo=info %} + {% if forloop.last and can_edit %} + <div class="panel-body"> + <a href="{% url 'tools:info_create' tool=tool.name %}" class="btn btn-default btn-sm pull-right">{% bootstrap_icon "plus" %} {% trans "add toolinfo" %}</a> + </div> + {% endif %} + </div> + {% empty %} + <div class="panel panel-warning"> + <div class="panel-heading"> + <h3 class="panel-title"> + {% bootstrap_icon "wrench" %} + {% trans "No toolinfo records found" %} + </h3> + </div> + <div class="panel-body"> + {% blocktrans %}Creating a toolinfo description is required for new tools and recommended for all tools.{% endblocktrans %} + </div> + {% if can_edit %} + <div class="panel-body"> + <a href="{% url 'tools:info_create' tool=tool.name %}" class="btn btn-default btn-sm pull-right">{% bootstrap_icon "plus" %} {% trans "add toolinfo" %}</a> + </div> + {% endif %} + </div> + {% endfor %} </div> - {% endif %} - </div> - {% empty %} - {% if can_edit %} - <div class="panel panel-warning"> - <div class="panel-heading"> - <h3 class="panel-title"> - {% bootstrap_icon "wrench" %} - {% trans "No toolinfo records found" %} - </h3> - </div> - <div class="panel-body"> - {% blocktrans %}Creating a toolinfo description is required for new tools and recommended for all tools.{% endblocktrans %} - </div> - <div class="panel-body"> - <a href="{% url 'tools:info_create' tool=tool.name %}" class="btn btn-default btn-sm pull-right">{% bootstrap_icon "plus" %} {% trans "add toolinfo" %}</a> - </div> - </div> - {% endif %} - {% endfor %} - - <div class="panel panel-default"> - <div class="panel-heading" role="tab" id="maintainers"> - <h3 class="panel-title">{% bootstrap_icon "user" %} {% trans "Maintainers" %}</h3> - </div> - <div class="panel-body"> - <ul class="tools-maintainers"> - {% for maintainer in tool.maintainers|dictsort:"full_name" %} - <li>{{ maintainer.full_name }}</li> - {% endfor %} - </ul> - </div> - </div> - <div class="panel panel-default {% if can_edit %}panel-pager{% endif %}"> - <div class="panel-heading" role="tab" id="diffusion"> - <div class="row"> - <div class="col-sm-9"> + <div class="col-sm-3 col-sm-pull-9 panel-group"> + <div class="panel panel-default"> + <div class="panel-heading" role="tab" id="maintainers"> + <h3 class="panel-title">{% bootstrap_icon "user" %} {% trans "Maintainers" %}</h3> + </div> + <ul class="list-group"> + {% for maintainer in tool.maintainers|dictsort:"cn" %} + <li class="list-group-item">{{ maintainer.cn }}</li> + {% endfor %} + {% for maintainer in tool.tool_members|dictsort:"cn" %} + <li class="list-group-item">{{ maintainer.cn }}</li> + {% endfor %} + </ul> + {% if can_edit %} + <div class="list-group"> + <a class="list-group-item" href="{% url 'tools:maintainers' tool=tool.name %}">{% bootstrap_icon "edit" %} {% trans "manage maintainers" %}</a> + </div> + {% endif %} + </div> + <div class="panel panel-default"> + <div class="panel-heading" role="tab" id="diffusion"> <h3 class="panel-title">{% bootstrap_icon "hdd" %} {% trans "Diffusion repositories" %}</h3> </div> - <div class="col-sm-3"> - {% if can_edit %} - <a href="{% url 'tools:repo_create' tool=tool.name %}" class="btn btn-default btn-sm pull-right">{% bootstrap_icon "plus" %} {% trans "create repository" %}</a> - {% endif %} + <div class="list-group"> + {% for repo in repos %} + <a class="list-group-item" href="{% url 'tools:repo_view' tool=tool.name repo=repo.name %}">{{ repo.name }}</a> + {% endfor %} </div> + {% if can_edit %} + <div class="list-group"> + <a class="list-group-item" href="{% url 'tools:repo_create' tool=tool.name %}">{% bootstrap_icon "plus-sign" %} {% trans "create repository" %}</a> + </div> + {% endif %} </div> - </div> - <div class="panel-body"> - <ul class="tools-diffusion-repos"> - {% for repo in repos %} - <li><a href="{% url 'tools:repo_view' tool=tool.name repo=repo.name %}">{{ repo.name }}</a></li> - {% endfor %} - </ul> </div> </div> </div> diff --git a/striker/tools/cache.py b/striker/tools/cache.py new file mode 100644 index 0000000..33c638b --- /dev/null +++ b/striker/tools/cache.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Wikimedia Foundation and contributors. +# All Rights Reserved. +# +# This file is part of Striker. +# +# Striker is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Striker is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Striker. If not, see <http://www.gnu.org/licenses/>. + +from django.core.cache import cache + +from striker import openstack + + +OPENSTACK_USERS_CACHE_KEY = 'openstack_users_by_role' + + +def get_openstack_users(): + users = cache.get(OPENSTACK_USERS_CACHE_KEY) + if users is None: + client = openstack.Client.default_client() + users = client.users_by_role() + cache.set(OPENSTACK_USERS_CACHE_KEY, users, 3600) + return users + + +def purge_openstack_users(): + cache.delete(OPENSTACK_USERS_CACHE_KEY) diff --git a/striker/tools/forms.py b/striker/tools/forms.py index e950eed..809e3e7 100644 --- a/striker/tools/forms.py +++ b/striker/tools/forms.py @@ -31,9 +31,11 @@ from striker import phabricator from striker.tools import utils from striker.tools.models import AccessRequest +from striker.tools.models import Maintainer from striker.tools.models import SoftwareLicense from striker.tools.models import ToolInfo from striker.tools.models import ToolInfoTag +from striker.tools.models import ToolUser phab = phabricator.Client.default_client() @@ -290,3 +292,40 @@ raise forms.ValidationError(check['error']) return name + + +class MaintainerChoiceField(forms.ModelMultipleChoiceField): + def label_from_instance(self, obj): + return obj.cn + + +@parsleyfy +class MantainersForm(forms.Form): + maintainers = MaintainerChoiceField( + queryset=Maintainer.objects.all(), + widget=autocomplete.ModelSelect2Multiple( + url='tools:api:maintainer', + ), + ) + tools = forms.ModelMultipleChoiceField( + queryset=ToolUser.objects.all(), + widget=autocomplete.ModelSelect2Multiple( + url='tools:api:tooluser', + ), + help_text=_( + 'Adding another tool as a maintainer will allow all ' + 'maintainers of that tool to access this tool. ' + 'It will also allow access to maintainers of any tool added to ' + 'that tool, and so on. Make sure you trust everyone and know what ' + 'you are doing before selecting anything in the "Tools" section.' + ), + required=False, + ) + + def __init__(self, *args, **kwargs): + tool = kwargs.pop('tool') + initial = kwargs.pop('initial', {}) + initial['maintainers'] = tool.maintainer_ids() + initial['tools'] = tool.tool_member_ids() + kwargs['initial'] = initial + super(MantainersForm, self).__init__(*args, **kwargs) diff --git a/striker/tools/migrations/0001_squashed.py b/striker/tools/migrations/0001_squashed.py index bcaba25..522797d 100644 --- a/striker/tools/migrations/0001_squashed.py +++ b/striker/tools/migrations/0001_squashed.py @@ -35,8 +35,8 @@ name='Maintainer', fields=[ ('dn', models.CharField(primary_key=True, serialize=False, max_length=200)), - ('username', ldapdb.models.fields.CharField(max_length=200, unique=True, db_column='uid')), - ('full_name', ldapdb.models.fields.CharField(max_length=200, db_column='cn')), + ('uid', ldapdb.models.fields.CharField(max_length=200, unique=True, db_column='uid')), + ('cn', ldapdb.models.fields.CharField(max_length=200, db_column='cn')), ], options={ 'abstract': False, diff --git a/striker/tools/models.py b/striker/tools/models.py index 0015b49..3feae27 100644 --- a/striker/tools/models.py +++ b/striker/tools/models.py @@ -30,14 +30,35 @@ import ldapdb.models import reversion +from striker.tools import cache + + +class MaintainerManager(models.Manager): + def _get_tool_users(self): + if settings.TEST_MODE: + # Hack to keep from trying to talk to openstack API from django + # test harness + return [] + users = cache.get_openstack_users() + return ( + users[settings.OPENSTACK_USER_ROLE] + + users[settings.OPENSTACK_ADMIN_ROLE] + ) + + def get_queryset(self): + return super(MaintainerManager, self).get_queryset().filter( + uid__in=self._get_tool_users()) + class Maintainer(ldapdb.models.Model): """A tool maintainer.""" base_dn = settings.TOOLS_MAINTAINER_BASE_DN object_classes = ['posixAccount'] - username = fields.CharField(db_column='uid', primary_key=True) - full_name = fields.CharField(db_column='cn') + uid = fields.CharField(db_column='uid', primary_key=True) + cn = fields.CharField(db_column='cn') + + objects = MaintainerManager() def __str__(self): return self.username @@ -68,11 +89,25 @@ def name(self, value): self.cn = 'tools.{0!s}'.format(value) + def maintainer_ids(self): + return [ + dn.split(',')[0].split('=')[1] + for dn in self.members + if not dn.startswith('uid=tools.') + ] + def maintainers(self): - # OMG, this is horrible. You can't search LDAP by dn. - return Maintainer.objects.filter( - username__in=( - dn.split(',')[0].split('=')[1] for dn in self.members)) + return Maintainer.objects.filter(uid__in=self.maintainer_ids()) + + def tool_member_ids(self): + return [ + dn.split(',')[0].split('=')[1] + for dn in self.members + if dn.startswith('uid=tools.') + ] + + def tool_members(self): + return ToolUser.objects.filter(uid__in=self.tool_member_ids()) def toolinfo(self): try: @@ -107,8 +142,16 @@ db_column='homeDirectory', max_length=200) login_shell = fields.CharField(db_column='loginShell', max_length=64) + @property + def name(self): + return self.cn[6:] + + @name.setter + def name(self, value): + self.cn = 'tools.{0!s}'.format(value) + def __str__(self): - return 'uid=%s,%s' % (self.uid, self.base_dn) + return self.name class SudoRole(ldapdb.models.Model): diff --git a/striker/tools/urls.py b/striker/tools/urls.py index f25b5e4..7a56500 100644 --- a/striker/tools/urls.py +++ b/striker/tools/urls.py @@ -93,6 +93,11 @@ name='repo_view' ), urls.url( + r'^id/{tool}/maintainers/$'.format(tool=TOOL), + 'striker.tools.views.maintainers', + name='maintainers' + ), + urls.url( r'^membership/$', 'striker.tools.views.membership', name='membership' @@ -126,6 +131,14 @@ urls.patterns( 'striker.tools.views', urls.url( + r'^autocomplete/maintainer$', + striker.tools.views.MaintainerAutocomplete.as_view(), + name='maintainer'), + urls.url( + r'^autocomplete/tooluser$', + striker.tools.views.ToolUserAutocomplete.as_view(), + name='tooluser'), + urls.url( r'^toolname/(?P<name>.+)$', 'toolname_available', name='toolname'), ), diff --git a/striker/tools/views.py b/striker/tools/views.py index c80d2d2..15154a0 100644 --- a/striker/tools/views.py +++ b/striker/tools/views.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU General Public License # along with Striker. If not, see <http://www.gnu.org/licenses/>. +import itertools import functools import logging @@ -49,18 +50,23 @@ from striker import mediawiki from striker import openstack from striker import phabricator +from striker.labsauth.models import LabsUser +from striker.tools import cache from striker.tools import utils from striker.tools.forms import AccessRequestAdminForm from striker.tools.forms import AccessRequestForm +from striker.tools.forms import MantainersForm from striker.tools.forms import RepoCreateForm from striker.tools.forms import ToolCreateForm from striker.tools.forms import ToolInfoForm from striker.tools.forms import ToolInfoPublicForm from striker.tools.models import AccessRequest from striker.tools.models import DiffusionRepo +from striker.tools.models import Maintainer from striker.tools.models import Tool from striker.tools.models import ToolInfo from striker.tools.models import ToolInfoTag +from striker.tools.models import ToolUser WELCOME_MSG = "== Welcome to Toolforge! ==\n{{subst:ToolsGranted}}" @@ -99,15 +105,12 @@ return decorated -def tool_member(tool, user): +def member_or_admin(tool, user): + """Is the given user a member of the given tool or a global admin?""" if user.is_anonymous(): return False - return user.ldap_dn in tool.members - - -def tools_admin(user): - if user.is_anonymous(): - return False + if user.ldap_dn in tool.members: + return True return user.ldap_dn in Tool.objects.get(cn='tools.admin').members @@ -162,7 +165,7 @@ 'tool': tool, 'toolinfo': tool.toolinfo(), 'repos': DiffusionRepo.objects.filter(tool=tool.name), - 'can_edit': tool_member(tool, req.user), + 'can_edit': member_or_admin(tool, req.user), }) @@ -171,10 +174,10 @@ @inject_tool def info_create(req, tool): """Create a ToolInfo record.""" - if not tool_member(tool, req.user): + if not member_or_admin(tool, req.user): messages.error( req, _('You are not a member of {tool}').format(tool=tool.name)) - return shortcuts.redirect(urlresolvers.reverse('tools:index')) + return shortcuts.redirect(tool.get_absolute_url()) initial_values = { 'name': tool.name, @@ -229,7 +232,7 @@ def info_edit(req, tool, info_id): """Create a ToolInfo record.""" toolinfo = shortcuts.get_object_or_404(ToolInfo, pk=info_id, tool=tool) - if tool_member(tool, req.user): + if member_or_admin(tool, req.user): form = ToolInfoForm( req.POST or None, req.FILES or None, instance=toolinfo) else: @@ -287,13 +290,11 @@ def get_context_data(self, **kwargs): user = self.request.user tool = Tool.objects.get(cn='tools.{0}'.format(kwargs['object'].tool)) - is_member = tool_member(tool, user) - is_admin = tools_admin(user) ctx = super(ToolInfoHistoryView, self).get_context_data(**kwargs) ctx['toolinfo'] = kwargs['object'] ctx['tool'] = tool - ctx['show_suppressed'] = is_member or is_admin + ctx['show_suppressed'] = member_or_admin(tool, user) return ctx @@ -305,10 +306,8 @@ version = shortcuts.get_object_or_404( reversion.models.Version, pk=version_id, object_id=info_id) - is_member = tool_member(tool, req.user) - is_admin = tools_admin(req.user) - can_revert = is_member or is_admin - can_suppress = is_member or is_admin + can_revert = member_or_admin(tool, req.user) + can_suppress = member_or_admin(tool, req.user) history_url = urlresolvers.reverse( 'tools:info_history', @@ -410,7 +409,7 @@ @inject_tool def repo_create(req, tool): """Create a new Diffusion repo.""" - if not tool_member(tool, req.user): + if not member_or_admin(tool, req.user): messages.error( req, _('You are not a member of {tool}').format(tool=tool.name)) return shortcuts.redirect(urlresolvers.reverse('tools:index')) @@ -630,6 +629,7 @@ msg = '{}\n{}'.format( talk.text(), WELCOME_MSG).strip() talk.save(msg, summary=WELCOME_SUMMARY, bot=False) + cache.purge_openstack_users() if request.status != AccessRequest.PENDING: request.resolved_by = req.user @@ -812,3 +812,125 @@ return JsonResponse({ 'available': available, }, status=status) + + +@login_required +@inject_tool +def maintainers(req, tool): + """Manage the maintainers list for a tool""" + if not member_or_admin(tool, req.user): + messages.error( + req, _('You are not a member of {tool}').format(tool=tool.name)) + return shortcuts.redirect(tool.get_absolute_url()) + form = MantainersForm(req.POST or None, req.FILES or None, tool=tool) + if req.method == 'POST': + if form.is_valid(): + old_members = set(tool.members) + new_members = set( + m.dn for m in itertools.chain( + form.cleaned_data['maintainers'], + form.cleaned_data['tools'] + ) + ) + tool.members = new_members + tool.save() + + maintainers, created = Group.objects.get_or_create(name=tool.cn) + added_maintainers = list(new_members - old_members) + removed_maintainers = list(old_members - new_members) + for dn in added_maintainers: + uid = dn.split(',')[0][4:] + logging.info('Added %s', uid) + try: + added = LabsUser.objects.get(shellname=uid) + except ObjectDoesNotExist: + # No local user for this account + logging.info('No LabsUser found for %s', uid) + pass + else: + # Add user to the mirrored group + added.groups.add(maintainers.id) + # Do not set tool as the notification target because the + # framework does not understand LDAP models. + notify.send( + recipient=added, + sender=req.user, + verb=_( + 'added you as a maintainer of {}' + ).format(tool.name), + public=True, + level='info', + actions=[ + { + 'title': _('View tool'), + 'href': tool.get_absolute_url(), + }, + ], + ) + + for dn in removed_maintainers: + uid = dn.split(',')[0][4:] + logging.info('Removed %s', uid) + try: + removed = LabsUser.objects.get(shellname=uid) + except ObjectDoesNotExist: + # No local user for this account + logging.info('No LabsUser found for %s', uid) + pass + else: + # Add user to the mirrored group + removed.groups.remove(maintainers.id) + # Do not set tool as the notification target because the + # framework does not understand LDAP models. + notify.send( + recipient=removed, + sender=req.user, + verb=_( + 'removed you from the maintainers of {}' + ).format(tool.name), + public=True, + level='info', + actions=[ + { + 'title': _('View tool'), + 'href': tool.get_absolute_url(), + }, + ], + ) + + messages.info(req, _('Maintainers updated')) + return shortcuts.redirect(tool.get_absolute_url()) + + ctx = { + 'tool': tool, + 'form': form, + } + return shortcuts.render(req, 'tools/maintainers.html', ctx) + + +class MaintainerAutocomplete(autocomplete.Select2QuerySetView): + def get_queryset(self): + if not self.request.user.is_authenticated(): + return Maintainer.objects.none() + qs = Maintainer.objects.all() + if self.q: + qs = qs.filter(cn__icontains=self.q) + qs.order_by('cn') + return qs + + def get_result_label(self, result): + return result.cn + + +class ToolUserAutocomplete(autocomplete.Select2QuerySetView): + def get_queryset(self): + if not self.request.user.is_authenticated(): + return ToolUser.objects.none() + qs = ToolUser.objects.all() + if self.q: + qs = qs.filter(uid__icontains=self.q) + qs.order_by('uid') + return qs + + def get_result_label(self, result): + return result.name -- To view, visit https://gerrit.wikimedia.org/r/364135 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I84e8452b0c592283a6deb7c577f6ec3f037489ce Gerrit-PatchSet: 1 Gerrit-Project: labs/striker Gerrit-Branch: master Gerrit-Owner: BryanDavis <bda...@wikimedia.org> Gerrit-Reviewer: Andrew Bogott <abog...@wikimedia.org> Gerrit-Reviewer: BryanDavis <bda...@wikimedia.org> Gerrit-Reviewer: Madhuvishy <mviswanat...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits