Commit 5c34a0a5 authored by Weblate's avatar Weblate

Merge remote-tracking branch 'origin/master'

parents 224bf758 a45e5e8e
...@@ -19,6 +19,7 @@ Released on ? 2015. ...@@ -19,6 +19,7 @@ Released on ? 2015.
* Add management command to optimize fulltext index. * Add management command to optimize fulltext index.
* Added support for error reporting to Rollbar. * Added support for error reporting to Rollbar.
* Projects now can have multiple owners. * Projects now can have multiple owners.
* Project owners can manage themselves.
weblate 2.3 weblate 2.3
----------- -----------
......
...@@ -164,6 +164,35 @@ ...@@ -164,6 +164,35 @@
</tr> </tr>
</thead> </thead>
<tbody> <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 %} {% for user in object.all_users %}
<tr> <tr>
<td>{{ user.username }}</td> <td>{{ user.username }}</td>
...@@ -177,6 +206,14 @@ ...@@ -177,6 +206,14 @@
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
{% trans "Remove" %} {% trans "Remove" %}
</button> </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> </form>
</td> </td>
</tr> </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 ...@@ -26,6 +26,8 @@ from django.utils.safestring import mark_safe
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode
from django.forms import ValidationError from django.forms import ValidationError
from django.core.urlresolvers import reverse 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 crispy_forms.helper import FormHelper
from weblate.lang.models import Language from weblate.lang.models import Language
from weblate.trans.models.unit import Unit, SEARCH_FILTERS from weblate.trans.models.unit import Unit, SEARCH_FILTERS
...@@ -763,7 +765,7 @@ class CheckFlagsForm(forms.Form): ...@@ -763,7 +765,7 @@ class CheckFlagsForm(forms.Form):
return flags return flags
class AddUserForm(forms.Form): class UserManageForm(forms.Form):
name = forms.CharField( name = forms.CharField(
label=_('User to add'), label=_('User to add'),
help_text=_( help_text=_(
...@@ -771,3 +773,14 @@ class AddUserForm(forms.Form): ...@@ -771,3 +773,14 @@ class AddUserForm(forms.Form):
'User needs to already have an active account in Weblate.' '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): ...@@ -192,7 +192,9 @@ class Project(models.Model, PercentMixin, URLMixin, PathMixin):
Returns all users having ACL on this project. Returns all users having ACL on this project.
""" """
group = Group.objects.get(name=self.name) 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): def add_user(self, user):
""" """
......
...@@ -32,6 +32,12 @@ class ACLViewTest(ViewTestCase): ...@@ -32,6 +32,12 @@ class ACLViewTest(ViewTestCase):
super(ACLViewTest, self).setUp() super(ACLViewTest, self).setUp()
self.project.enable_acl = True self.project.enable_acl = True
self.project.save() 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): def add_acl(self):
""" """
...@@ -42,18 +48,14 @@ class ACLViewTest(ViewTestCase): ...@@ -42,18 +48,14 @@ class ACLViewTest(ViewTestCase):
def test_acl_denied(self): def test_acl_denied(self):
"""No access to the project without ACL. """No access to the project without ACL.
""" """
response = self.client.get( response = self.client.get(self.project_url)
reverse('project', kwargs=self.kw_project)
)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_acl(self): def test_acl(self):
"""Regular user should not have access to user management. """Regular user should not have access to user management.
""" """
self.add_acl() self.add_acl()
response = self.client.get( response = self.client.get(self.project_url)
reverse('project', kwargs=self.kw_project)
)
self.assertNotContains(response, 'Manage users') self.assertNotContains(response, 'Manage users')
def test_edit_acl(self): def test_edit_acl(self):
...@@ -61,43 +63,109 @@ class ACLViewTest(ViewTestCase): ...@@ -61,43 +63,109 @@ class ACLViewTest(ViewTestCase):
""" """
self.add_acl() self.add_acl()
self.make_manager() self.make_manager()
response = self.client.get( response = self.client.get(self.project_url)
reverse('project', kwargs=self.kw_project)
)
self.assertContains(response, 'Manage users') self.assertContains(response, 'Manage users')
def test_add_acl(self): def test_edit_acl_owner(self):
"""Adding and removing user from the ACL project. """Owner should have access to user management.
""" """
self.add_acl() self.add_acl()
self.make_manager() self.project.owners.add(self.user)
project_url = reverse('project', kwargs=self.kw_project) response = self.client.get(self.project_url)
second_user = User.objects.create_user( self.assertContains(response, 'Manage users')
'seconduser',
'noreply@example.org', def add_user(self):
'testpassword' self.add_acl()
) self.project.owners.add(self.user)
# Add user # Add user
response = self.client.post( response = self.client.post(
reverse('add-user', kwargs=self.kw_project), 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 # Ensure user is now listed
response = self.client.get(project_url) response = self.client.get(self.project_url)
self.assertContains(response, second_user.username) self.assertContains(response, self.second_user.username)
self.assertContains(response, second_user.email) self.assertContains(response, self.second_user.email)
def remove_user(self):
# Remove user # Remove user
response = self.client.post( response = self.client.post(
reverse('delete-user', kwargs=self.kw_project), 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 # Ensure user is now not listed
response = self.client.get(project_url) response = self.client.get(self.project_url)
self.assertNotContains(response, second_user.username) self.assertNotContains(response, self.second_user.username)
self.assertNotContains(response, second_user.email) 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 ...@@ -27,37 +27,35 @@ from django.views.decorators.http import require_POST
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from weblate.trans.util import redirect_param 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.views.helper import get_project
from weblate.trans.permissions import can_manage_acl from weblate.trans.permissions import can_manage_acl
@require_POST def check_user_form(request, project):
@login_required
def add_user(request, project):
obj = get_project(request, project) obj = get_project(request, project)
if not can_manage_acl(request.user, obj): if not can_manage_acl(request.user, obj):
raise PermissionDenied() raise PermissionDenied()
form = AddUserForm(request.POST) form = UserManageForm(request.POST)
if form.is_valid(): if form.is_valid():
try: return obj, form
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!'))
else: 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( return redirect_param(
'project', 'project',
...@@ -68,29 +66,61 @@ def add_user(request, project): ...@@ -68,29 +66,61 @@ def add_user(request, project):
@require_POST @require_POST
@login_required @login_required
def delete_user(request, project): def revoke_owner(request, project):
obj = get_project(request, project) obj, form = check_user_form(request, project)
if not can_manage_acl(request.user, obj): if form is not None:
raise PermissionDenied() 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(): return redirect_param(
try: 'project',
user = User.objects.get( '#acl',
username=form.cleaned_data['name'] project=obj.slug,
) )
obj.remove_user(user)
@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( messages.success(
request, _('User has been removed from this project.') 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( return redirect_param(
'project', 'project',
......
...@@ -37,7 +37,7 @@ from weblate.lang.models import Language ...@@ -37,7 +37,7 @@ from weblate.lang.models import Language
from weblate.trans.forms import ( from weblate.trans.forms import (
get_upload_form, SearchForm, get_upload_form, SearchForm,
AutoForm, ReviewForm, NewLanguageForm, AutoForm, ReviewForm, NewLanguageForm,
AddUserForm, UserManageForm,
) )
from weblate.accounts.models import Profile, notify_new_language from weblate.accounts.models import Profile, notify_new_language
from weblate.trans.views.helper import ( from weblate.trans.views.helper import (
...@@ -246,7 +246,7 @@ def show_project(request, project): ...@@ -246,7 +246,7 @@ def show_project(request, project):
'last_changes_url': urlencode( 'last_changes_url': urlencode(
{'project': obj.slug} {'project': obj.slug}
), ),
'add_user_form': AddUserForm(), 'add_user_form': UserManageForm(),
} }
) )
......
...@@ -201,6 +201,16 @@ urlpatterns = patterns( ...@@ -201,6 +201,16 @@ urlpatterns = patterns(
'weblate.trans.views.acl.delete_user', 'weblate.trans.views.acl.delete_user',
name='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 # Monthly activity
url( 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