Mercurial > hg > savane-forge
changeset 186:6bc5e698e9c4
Interface to manage project members (TODO: search for members)
author | Sylvain Beucler <beuc@beuc.net> |
---|---|
date | Sun, 25 Jul 2010 12:47:51 +0200 |
parents | 3d1a07772d4d |
children | 95a750a73174 |
files | savane/django_utils.py savane/middleware/exception.py savane/my/urls.py savane/perms.py savane/svmain/models.py savane/svmain/urls.py savane/svmain/views.py settings_default.py static_media/savane/css/Savannah.css templates/error.html templates/svmain/group_admin_members.html |
diffstat | 11 files changed, 242 insertions(+), 23 deletions(-) [+] |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/savane/django_utils.py @@ -0,0 +1,47 @@ +# Batch-decorator for urlpatterns +# http://www.djangosnippets.org/snippets/532/ +# Author: miracle2k (Jan 1, 2008) +# +# I hate legal-speak as much as anybody, but on a site which is geared +# toward sharing code there has to be at least a little bit of it, so +# here goes: +# +# By creating an account here you agree to three things: +# +# 1. That you will only post code which you wrote yourself and that +# you have the legal right to release under these terms. +# +# 2. That you grant any third party who sees the code you post a +# royalty-free, non-exclusive license to copy and distribute that +# code and to make and distribute derivative works based on that +# code. You may include license terms in snippets you post, if you +# wish to use a particular license (such as the BSD license or GNU +# GPL), but that license must permit royalty-free copying, +# distribution and modification of the code to which it is applied. +# +# 3. That if you post code of which you are not the author or for +# which you do not have the legal right to distribute according to +# these terms, you will indemnify and hold harmless the operators of +# this site and any third parties who are exposed to liability as a +# result of your actions. +# +# If you can't legally agree to these terms, or don't want to, you +# cannot create an account here. + +from django.core.urlresolvers import RegexURLPattern +from django.conf.urls.defaults import patterns +class DecoratedURLPattern(RegexURLPattern): + def resolve(self, *args, **kwargs): + result = RegexURLPattern.resolve(self, *args, **kwargs) + if result: + result = list(result) + result[0] = self._decorate_with(result[0]) + return result +def decorated_patterns(prefix, func, *args): + result = patterns(prefix, *args) + if func: + for p in result: + if isinstance(p, RegexURLPattern): + p.__class__ = DecoratedURLPattern + p._decorate_with = func + return result
new file mode 100644 --- /dev/null +++ b/savane/middleware/exception.py @@ -0,0 +1,34 @@ +# Catch app-specific exception and display it to the user +# Copyright (C) 2009, 2010 Sylvain Beucler +# +# This file is part of Savane. +# +# Savane is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Savane 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 +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from django.shortcuts import render_to_response +from django.template import RequestContext + +class HttpAppException(Exception): + pass + +class HttpCatchAppExceptionMiddleware(object): + def process_exception(self, request, exception): + """ + Only catch our HttpAppException (and derivate classes) + """ + if isinstance(exception, HttpAppException): + return render_to_response('error.html', + { 'error' : exception.message }, + context_instance=RequestContext(request)) + return None
--- a/savane/my/urls.py +++ b/savane/my/urls.py @@ -25,27 +25,11 @@ import savane.svmain.models as svmain_models import django.contrib.auth.models as auth_models from savane.my.filters import * +from savane.django_utils import decorated_patterns -# Batch-decorator for urlpatterns -# http://www.djangosnippets.org/snippets/532/ -from django.core.urlresolvers import RegexURLPattern -class DecoratedURLPattern(RegexURLPattern): - def resolve(self, *args, **kwargs): - result = RegexURLPattern.resolve(self, *args, **kwargs) - if result: - result = list(result) - result[0] = self._decorate_with(result[0]) - return result -def decorated_patterns(prefix, func, *args): - result = patterns(prefix, *args) - if func: - for p in result: - if isinstance(p, RegexURLPattern): - p.__class__ = DecoratedURLPattern - p._decorate_with = func - return result +urlpatterns = patterns ('',) -urlpatterns = decorated_patterns ('', login_required, +urlpatterns += decorated_patterns ('', login_required, url(r'^$', direct_to_template, { 'template' : 'my/index.html', 'extra_context' : { 'title' : 'My account', }, },
new file mode 100644 --- /dev/null +++ b/savane/perms.py @@ -0,0 +1,43 @@ +# Permission restrictions to use as view decorators +# Copyright (C) 2010 Sylvain Beucler +# +# This file is part of Savane. +# +# Savane is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Savane 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import django.contrib.auth.models as auth_models +from django.shortcuts import get_object_or_404 +from savane.middleware.exception import HttpAppException +import savane.svmain.models as svmain_models + +def is_member(user, group): + return group.user_set.filter(pk=user.pk).count() > 0 + +def is_admin(user, group): + return (is_member(user, group) + and svmain_models.Membership.objects + .filter(user=user, group=group, admin_flags='A')) + +def only_project_admin(f, error_msg="Permission Denied"): + """ + Decorator to keep non-members out of project administration + screens. Identifies the current group using the 'slug' keyword + parameter. + """ + def _f(request, *args, **kwargs): + group = get_object_or_404(auth_models.Group, name=kwargs['slug']) + if request.user.is_anonymous() or not is_admin(request.user, group): + raise HttpAppException(error_msg) + return f(request, *args, **kwargs) + return _f
--- a/savane/svmain/models.py +++ b/savane/svmain/models.py @@ -533,6 +533,7 @@ blank=True, help_text="membership properties") onduty = models.BooleanField(default=True, help_text="Untick to hide emeritous members from the project page") + since = models.DateField(blank=True, null=True) # TODO: split news params #news_flags int(11) default NULL
--- a/savane/svmain/urls.py +++ b/savane/svmain/urls.py @@ -19,11 +19,14 @@ from django.conf.urls.defaults import * from django.views.generic.list_detail import object_list, object_detail +from django.contrib.auth.decorators import login_required import savane.svmain.models as svmain_models import django.contrib.auth.models as auth_models import views from savane.filters import search +from savane.perms import only_project_admin +from savane.django_utils import decorated_patterns urlpatterns = patterns ('',) @@ -82,13 +85,17 @@ url(r'^pr/(?P<slug>[-\w]+)/$', views.group_redir), url(r'^projects/(?P<slug>[-\w]+)/$', views.group_redir), url(r'^p/(?P<slug>[-\w]+)/join/$', views.group_join), +) +urlpatterns += decorated_patterns ('', only_project_admin, url(r'^p/(?P<slug>[-\w]+)/admin/$', views.group_admin, { 'extra_context' : { 'title' : 'Administration Summary' }, }, name='savane.svmain.group_admin'), url(r'^p/(?P<slug>[-\w]+)/admin/members/$', views.group_admin_members, { 'extra_context' : { 'title' : 'Administration Summary: Manage Members' }, }, name='savane.svmain.group_admin_members'), +) +urlpatterns += patterns ('', url(r'^license/$', 'django.views.generic.list_detail.object_list', { 'queryset' : svmain_models.License.objects.all(), 'extra_context' : { 'title' : 'License list' }, },
--- a/savane/svmain/views.py +++ b/savane/svmain/views.py @@ -55,11 +55,56 @@ @render_to('svmain/group_admin_members.html', mimetype=None) def group_admin_members(request, slug, extra_context={}): group = get_object_or_404(auth_models.Group, name=slug) - members = group.user_set.all() + + # If using a non-Savane groups base, prepare membership metadata + user_pks = svmain_models.Membership.objects.filter(group=group).values_list('user__pk', flat=True) + missing_members = group.user_set.exclude(pk__in=user_pks) + for member in missing_members: + svmain_models.Membership(user=member, group=group, admin_flags='A').save() + + # If a membership does not have a matching User<->Group relationship, remove it + user_pks = group.user_set.values_list('pk', flat=True) + invalid_memberships = svmain_models.Membership.objects.exclude(user__in=user_pks).exclude(admin_flags='P') + invalid_memberships.delete() + + + memberships = svmain_models.Membership.objects.filter(group=group).exclude(admin_flags='P') + pending_memberships = svmain_models.Membership.objects.filter(group=group, admin_flags='P') + + + if request.method == 'POST': + for membership in memberships: + if request.user != membership.user: # don't unadmin or remove myself + # admin / unadmin + if request.POST.get('admin_%d' % membership.pk, None): + if membership.admin_flags != 'A': + membership.admin_flags = 'A' + membership.save() + messages.success(request, "permissions of %s updated." % membership.user) + else: + if membership.admin_flags != '': + membership.admin_flags = '' + membership.save() + messages.success(request, "permissions of %s updated." % membership.user) + # remove members + if request.POST.get('remove_%d' % membership.pk, None): + group.user_set.remove(membership.user) + membership.delete() + messages.success(request, "User %s deleted from the project." % membership.user) + # approve pending membership + for membership in pending_memberships: + if request.POST.get('approve_%d' % membership.pk, None): + group.user_set.add(membership.user) + membership.admin_flags = '' + membership.save() + messages.success(request, "User %s added to the project." % membership.user) + return HttpResponseRedirect('') # reload + context = { 'group' : group, - 'members' : members, + 'memberships' : memberships, + 'pending_memberships' : pending_memberships, } context.update(extra_context) return context
--- a/settings_default.py +++ b/settings_default.py @@ -82,6 +82,7 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'savane.middleware.debug.DebugFooter', + 'savane.middleware.exception.HttpCatchAppExceptionMiddleware', ) ROOT_URLCONF = 'urls'
--- a/static_media/savane/css/Savannah.css +++ b/static_media/savane/css/Savannah.css @@ -2,7 +2,7 @@ * * Copyright (C) 2002-2006 Mathieu Roy * Copyright (C) 2005 Stéphane Urbanovski - * Copyright (C) 2005 Sylvain Beucler + * Copyright (C) 2005, 2010 Sylvain Beucler * * This file is part of Savane. *
--- a/templates/error.html +++ b/templates/error.html @@ -1,5 +1,19 @@ {% extends "base.html" %} +{% block title %} +Error +{% endblock %} + +{% block top %} +{% endblock %} + {% block content %} -<p style='color: red'>{{error}}</p> +<div class="main"><a name="top"></a> +<div id="feedback" class="feedbackerror"> + <span class="feedbackerrortitle"> + <img src="{{STATIC_MEDIA_URL}}savane/images/common/bool1/wrong.orig.png" class="feedbackimage" alt="" /> + Error: + </span><br/> + {{error}} +</div> {% endblock content %}
--- a/templates/svmain/group_admin_members.html +++ b/templates/svmain/group_admin_members.html @@ -14,6 +14,49 @@ {% block content %} +<p>Members:</p> +<form action="" method="POST">{% csrf_token %} +<table> +<tr><th>User</th><th>Admin</th><th>Remove?</th></tr> +{% for membership in memberships %} +<tr> + <td>{{ membership.user.get_full_name }} <{{ membership.user.username }}></td> + <td> + {% ifequal request.user membership.user %} + <em>{% trans "You are Admin" %}</em> + {% else %} + <input type="checkbox" name="admin_{{membership.pk}}" {% ifequal membership.admin_flags "A" %}checked="checked"{% endifequal %}/> + {% endifequal %} + </td> + <td> + {% ifequal request.user membership.user %} + - + {% else %} <input type="checkbox" name="remove_{{membership.pk}}" /> + {% endifequal %} + </td> +</tr> +{% endfor %} +</table> + +{% if pending_memberships %} + <p>Requests for inclusion:</p> + <table> + <tr><th>User</th><th>Approve</th></tr> + {% for membership in pending_memberships %} + <tr> + <td>{{ membership.user.get_full_name }} <{{ membership.user.username }}></td> + <td><input type="checkbox" name="approve_{{membership.pk}}" /></td> + </tr> + {% endfor %} + </table> +{% else %} + <p><em>{% trans "No requests for inclusion" %}</em></p> +{% endif %} + +<p><input type="submit" value="OK" /></p> +</form> + +<p><a href="add/">{% trans "Add users to group" %}</a></p> {% endblock %}