BryanDavis has uploaded a new change for review. (
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(-)
git pull ssh://gerrit.wikimedia.org:29418/labs/striker
refs/changes/35/364135/1
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: newchange
Gerrit-Change-Id: I84e8452b0c592283a6deb7c577f6ec3f037489ce
Gerrit-PatchSet: 1
Gerrit-Project: labs/striker
Gerrit-Branch: master
Gerrit-Owner: BryanDavis <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits