Commit 5c34a0a5 authored by Weblate's avatar Weblate

Merge remote-tracking branch 'origin/master'

parents 224bf758 a45e5e8e
......@@ -19,6 +19,7 @@ Released on ? 2015.
* Add management command to optimize fulltext index.
* Added support for error reporting to Rollbar.
* Projects now can have multiple owners.
* Project owners can manage themselves.
weblate 2.3
-----------
......
......@@ -164,6 +164,35 @@
</tr>
</thead>
<tbody>
{% with object.owners.count as owner_count %}
{% for user in object.owners.all %}
<tr>
<td>{{ user.username }} <span class="badge">{% trans "Owner" %}</span></td>
<td>{{ user.first_name }}</td>
<td>{{ user.email }}</td>
<td>
{% if owner_count > 1 %}
<form action="{% url "delete-user" project=object.slug %}" method="post">
{% csrf_token %}
<input type="hidden" name="name" value="{{ user.username }}" />
<button type="submit" class="btn btn-danger btn-xs">
<i class="fa fa-trash"></i>
{% trans "Remove" %}
</button>
</form>
<form action="{% url "revoke-owner" project=object.slug %}" method="post">
{% csrf_token %}
<input type="hidden" name="name" value="{{ user.username }}" />
<button type="submit" class="btn btn-warning btn-xs">
<i class="fa fa-user-times"></i>
{% trans "Revoke ownership" %}
</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
{% endwith %}
{% for user in object.all_users %}
<tr>
<td>{{ user.username }}</td>
......@@ -177,6 +206,14 @@
<i class="fa fa-trash"></i>
{% trans "Remove" %}
</button>
</form>
<form action="{% url "make-owner" project=object.slug %}" method="post">
{% csrf_token %}
<input type="hidden" name="name" value="{{ user.username }}" />
<button type="submit" class="btn btn-success btn-xs">
<i class="fa fa-user-plus"></i>
{% trans "Make owner" %}
</button>
</form>
</td>
</tr>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -26,6 +26,8 @@ from django.utils.safestring import mark_safe
from django.utils.encoding import smart_unicode
from django.forms import ValidationError
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.contrib.auth.models import User
from crispy_forms.helper import FormHelper
from weblate.lang.models import Language
from weblate.trans.models.unit import Unit, SEARCH_FILTERS
......@@ -763,7 +765,7 @@ class CheckFlagsForm(forms.Form):
return flags
class AddUserForm(forms.Form):
class UserManageForm(forms.Form):
name = forms.CharField(
label=_('User to add'),
help_text=_(
......@@ -771,3 +773,14 @@ class AddUserForm(forms.Form):
'User needs to already have an active account in Weblate.'
),
)
def clean(self):
try:
self.cleaned_data['user'] = User.objects.get(
Q(username=self.cleaned_data['name']) |
Q(email=self.cleaned_data['name'])
)
except User.DoesNotExist:
raise ValidationError(_('No matching user found!'))
except User.MultipleObjectsReturned:
raise ValidationError(_('More users matched!'))
......@@ -192,7 +192,9 @@ class Project(models.Model, PercentMixin, URLMixin, PathMixin):
Returns all users having ACL on this project.
"""
group = Group.objects.get(name=self.name)
return group.user_set.all()
return group.user_set.exclude(
id__in=self.owners.values_list('id', flat=True)
)
def add_user(self, user):
"""
......
......@@ -32,6 +32,12 @@ class ACLViewTest(ViewTestCase):
super(ACLViewTest, self).setUp()
self.project.enable_acl = True
self.project.save()
self.project_url = reverse('project', kwargs=self.kw_project)
self.second_user = User.objects.create_user(
'seconduser',
'noreply@example.org',
'testpassword'
)
def add_acl(self):
"""
......@@ -42,18 +48,14 @@ class ACLViewTest(ViewTestCase):
def test_acl_denied(self):
"""No access to the project without ACL.
"""
response = self.client.get(
reverse('project', kwargs=self.kw_project)
)
response = self.client.get(self.project_url)
self.assertEqual(response.status_code, 403)
def test_acl(self):
"""Regular user should not have access to user management.
"""
self.add_acl()
response = self.client.get(
reverse('project', kwargs=self.kw_project)
)
response = self.client.get(self.project_url)
self.assertNotContains(response, 'Manage users')
def test_edit_acl(self):
......@@ -61,43 +63,109 @@ class ACLViewTest(ViewTestCase):
"""
self.add_acl()
self.make_manager()
response = self.client.get(
reverse('project', kwargs=self.kw_project)
)
response = self.client.get(self.project_url)
self.assertContains(response, 'Manage users')
def test_add_acl(self):
"""Adding and removing user from the ACL project.
def test_edit_acl_owner(self):
"""Owner should have access to user management.
"""
self.add_acl()
self.make_manager()
project_url = reverse('project', kwargs=self.kw_project)
second_user = User.objects.create_user(
'seconduser',
'noreply@example.org',
'testpassword'
)
self.project.owners.add(self.user)
response = self.client.get(self.project_url)
self.assertContains(response, 'Manage users')
def add_user(self):
self.add_acl()
self.project.owners.add(self.user)
# Add user
response = self.client.post(
reverse('add-user', kwargs=self.kw_project),
{'name': second_user.username}
{'name': self.second_user.username}
)
self.assertRedirects(response, '{0}#acl'.format(project_url))
self.assertRedirects(response, '{0}#acl'.format(self.project_url))
# Ensure user is now listed
response = self.client.get(project_url)
self.assertContains(response, second_user.username)
self.assertContains(response, second_user.email)
response = self.client.get(self.project_url)
self.assertContains(response, self.second_user.username)
self.assertContains(response, self.second_user.email)
def remove_user(self):
# Remove user
response = self.client.post(
reverse('delete-user', kwargs=self.kw_project),
{'name': second_user.username}
{'name': self.second_user.username}
)
self.assertRedirects(response, '{0}#acl'.format(project_url))
self.assertRedirects(response, '{0}#acl'.format(self.project_url))
# Ensure user is now not listed
response = self.client.get(project_url)
self.assertNotContains(response, second_user.username)
self.assertNotContains(response, second_user.email)
response = self.client.get(self.project_url)
self.assertNotContains(response, self.second_user.username)
self.assertNotContains(response, self.second_user.email)
def test_add_acl(self):
"""Adding and removing user from the ACL project.
"""
self.add_user()
self.remove_user()
def test_add_owner(self):
"""Adding and removing owner from the ACL project.
"""
self.add_user()
response = self.client.post(
reverse('make-owner', kwargs=self.kw_project),
{'name': self.second_user.username}
)
self.assertTrue(
self.project.owners.filter(
username=self.second_user.username
).exists()
)
response = self.client.post(
reverse('revoke-owner', kwargs=self.kw_project),
{'name': self.second_user.username}
)
self.assertFalse(
self.project.owners.filter(
username=self.second_user.username
).exists()
)
self.remove_user()
def test_delete_owner(self):
"""Adding and deleting owner from the ACL project.
"""
self.add_user()
response = self.client.post(
reverse('make-owner', kwargs=self.kw_project),
{'name': self.second_user.username}
)
self.remove_user()
self.assertFalse(
self.project.owners.filter(
username=self.second_user.username
).exists()
)
def test_denied_owner_delete(self):
"""Test that deleting last owner does not work."""
self.project.owners.add(self.user)
self.client.post(
reverse('revoke-owner', kwargs=self.kw_project),
{'name': self.second_user.username}
)
self.assertTrue(
self.project.owners.filter(
username=self.user.username
).exists()
)
self.client.post(
reverse('delete-user', kwargs=self.kw_project),
{'name': self.second_user.username}
)
self.assertTrue(
self.project.owners.filter(
username=self.user.username
).exists()
)
......@@ -27,37 +27,35 @@ from django.views.decorators.http import require_POST
from django.core.exceptions import PermissionDenied
from weblate.trans.util import redirect_param
from weblate.trans.forms import AddUserForm
from weblate.trans.forms import UserManageForm
from weblate.trans.views.helper import get_project
from weblate.trans.permissions import can_manage_acl
@require_POST
@login_required
def add_user(request, project):
def check_user_form(request, project):
obj = get_project(request, project)
if not can_manage_acl(request.user, obj):
raise PermissionDenied()
form = AddUserForm(request.POST)
form = UserManageForm(request.POST)
if form.is_valid():
try:
user = User.objects.get(
Q(username=form.cleaned_data['name']) |
Q(email=form.cleaned_data['name'])
)
obj.add_user(user)
messages.success(
request, _('User has been added to this project.')
)
except User.DoesNotExist:
messages.error(request, _('No matching user found!'))
except User.MultipleObjectsReturned:
messages.error(request, _('More users matched!'))
return obj, form
else:
messages.error(request, _('Invalid user specified!'))
for error in form.errors:
for message in form.errors[error]:
messages.error(request, message)
return obj, None
@require_POST
@login_required
def make_owner(request, project):
obj, form = check_user_form(request, project)
if form is not None:
obj.owners.add(form.cleaned_data['user'])
return redirect_param(
'project',
......@@ -68,29 +66,61 @@ def add_user(request, project):
@require_POST
@login_required
def delete_user(request, project):
obj = get_project(request, project)
def revoke_owner(request, project):
obj, form = check_user_form(request, project)
if not can_manage_acl(request.user, obj):
raise PermissionDenied()
if form is not None:
if obj.owners.count() <= 1:
messages.error(request, _('You can not remove last owner!'))
else:
# Ensure owner stays within project
obj.add_user(form.cleaned_data['user'])
form = AddUserForm(request.POST)
obj.owners.remove(form.cleaned_data['user'])
if form.is_valid():
try:
user = User.objects.get(
username=form.cleaned_data['name']
)
obj.remove_user(user)
return redirect_param(
'project',
'#acl',
project=obj.slug,
)
@require_POST
@login_required
def add_user(request, project):
obj, form = check_user_form(request, project)
if form is not None:
obj.add_user(form.cleaned_data['user'])
messages.success(
request, _('User has been added to this project.')
)
return redirect_param(
'project',
'#acl',
project=obj.slug,
)
@require_POST
@login_required
def delete_user(request, project):
obj, form = check_user_form(request, project)
if form is not None:
is_owner = obj.owners.filter(
id=form.cleaned_data['user'].id
).exists()
if is_owner and obj.owners.count() <= 1:
messages.error(request, _('You can not remove last owner!'))
else:
if is_owner:
obj.owners.remove(form.cleaned_data['user'])
obj.remove_user(form.cleaned_data['user'])
messages.success(
request, _('User has been removed from this project.')
)
except User.DoesNotExist:
messages.error(request, _('No matching user found!'))
except User.MultipleObjectsReturned:
messages.error(request, _('More users matched!'))
else:
messages.error(request, _('Invalid user specified!'))
return redirect_param(
'project',
......
......@@ -37,7 +37,7 @@ from weblate.lang.models import Language
from weblate.trans.forms import (
get_upload_form, SearchForm,
AutoForm, ReviewForm, NewLanguageForm,
AddUserForm,
UserManageForm,
)
from weblate.accounts.models import Profile, notify_new_language
from weblate.trans.views.helper import (
......@@ -246,7 +246,7 @@ def show_project(request, project):
'last_changes_url': urlencode(
{'project': obj.slug}
),
'add_user_form': AddUserForm(),
'add_user_form': UserManageForm(),
}
)
......
......@@ -201,6 +201,16 @@ urlpatterns = patterns(
'weblate.trans.views.acl.delete_user',
name='delete-user',
),
url(
r'^make-owner/' + PROJECT + '$',
'weblate.trans.views.acl.make_owner',
name='make-owner',
),
url(
r'^revoke-owner/' + PROJECT + '$',
'weblate.trans.views.acl.revoke_owner',
name='revoke-owner',
),
# Monthly activity
url(
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment