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

Reply via email to