Mercurial > hg > savane-forge
changeset 94:5c90eef9f2c1
- Import user->group membership
- Clean-up duplicates and dangling membership
- Document the User/Group inheritence (compared to using Profiles)
- Introduce get_obsolute_url(...) functions in models
- Add templates for users, groups and licenses, with links from one
another
- Filter users by status in the admin interface
- Display a 404 when accessing 'my' with a User that doesn't have an
ExtendedUser
- Simplify login template to inherit error reporting
author | Sylvain Beucler <beuc@beuc.net> |
---|---|
date | Wed, 29 Jul 2009 14:45:09 +0200 |
parents | 149fcd78909f |
children | 801166bc508a |
files | migrate_old_savane.sql src/savane/my/urls.py src/savane/my/views.py src/savane/svmain/admin.py src/savane/svmain/models.py src/savane/svmain/urls.py template/registration/login.html template/svmain/extendedgroup_detail.html template/svmain/extendedgroup_list.html template/svmain/extendeduser_detail.html template/svmain/license_detail.html template/svmain/license_list.html |
diffstat | 12 files changed, 323 insertions(+), 14 deletions(-) [+] |
line wrap: on
line diff
--- a/migrate_old_savane.sql +++ b/migrate_old_savane.sql @@ -270,3 +270,48 @@ url_extralink_documentation FROM savane_old.groups LEFT JOIN savane.svmain_license ON savane_old.groups.license = savane.svmain_license.slug WHERE savane_old.groups.group_id != 100; + +-- Import users<->groups relationships +-- Get rid of duplicates (long: several minutes): +DELETE FROM savane_old.user_group + WHERE user_group_id IN ( + SELECT A.user_group_id + FROM savane_old.user_group A, savane_old.user_group B + WHERE A.user_id = B.user_id AND A.group_id = B.group_id + GROUP BY A.user_id, A.group_id HAVING count(*) > 1 + ); +-- Actual import +INSERT INTO auth_user_groups + (user_id, group_id) + SELECT user_id, group_id + FROM savane_old.user_group; +INSERT INTO svmain_membership + (user_id, group_id, admin_flags, onduty) + SELECT user_id, group_id, admin_flags, onduty + FROM savane_old.user_group; +-- Get rid of ghost relationships (deleted group) +DELETE FROM svmain_membership + WHERE group_id IN ( + SELECT group_id FROM ( + SELECT group_id + FROM svmain_membership + LEFT JOIN svmain_extendedgroup ON svmain_membership.group_id = svmain_extendedgroup.group_ptr_id + WHERE group_ptr_id IS NULL + ) AS temp + ); +-- Get rid of ghost relationships (deleted user) +DELETE FROM svmain_membership WHERE user_id IN ( + SELECT user_id FROM ( + SELECT user_id + FROM svmain_membership + LEFT JOIN svmain_extendeduser ON svmain_membership.user_id = svmain_extendeduser.user_ptr_id + WHERE user_ptr_id IS NULL + ) AS temp + ); +-- Set members of 'administration' as superusers +-- TODO: get the supergroup name from the old Savane configuration +UPDATE auth_user SET is_staff=1, is_superuser=1 + WHERE id IN ( + SELECT user_id + FROM auth_user_groups JOIN auth_group ON auth_user_groups.group_id = auth_group.id + WHERE auth_group.name='administration');
--- a/src/savane/my/urls.py +++ b/src/savane/my/urls.py @@ -31,7 +31,6 @@ the current user""" request = args[0] user = request.user - print kwargs kwargs['queryset'] = kwargs['queryset'].filter(user=user.id) return f(*args, **kwargs) @@ -53,5 +52,5 @@ url('^conf/ssh_gpg$', views.sv_ssh_gpg), url(r'^groups/$', object_list__only_mine, { 'queryset' : svmain_models.ExtendedGroup.objects.all() }, - name='savane.my.generic.group_list'), + name='savane.my.group_list'), )
--- a/src/savane/my/views.py +++ b/src/savane/my/views.py @@ -84,7 +84,7 @@ context_instance=RequestContext(request)) @login_required() def sv_ssh_gpg( request ): - eu = ExtendedUser.objects.get(pk=request.user.pk) + eu = get_object_or_404(ExtendedUser, pk=request.user.pk) error_msg = None success_msg = None
--- a/src/savane/svmain/admin.py +++ b/src/savane/svmain/admin.py @@ -27,7 +27,7 @@ 'people_view_skills', 'email_hide', 'timezone', 'theme',)}), ) list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') - list_filter = ('is_staff', 'is_superuser') + list_filter = ('is_staff', 'is_superuser', 'status') search_fields = ('username', 'first_name', 'last_name', 'email') ordering = ('username',) filter_horizontal = ('user_permissions',)
--- a/src/savane/svmain/models.py +++ b/src/savane/svmain/models.py @@ -19,6 +19,42 @@ # 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/>. +""" +User/group extra attributes + +This may look like reinventing the User.get_profile() that comes with +Django; +http://www.b-list.org/weblog/2006/jun/06/django-tips-extending-user-model/ +http://mirobetm.blogspot.com/2007/04/en-django-extending-user_2281.html + +However profiles were mainly useful in Django < 1.0 where you couldn't +subclass User as we do. + +Profiles also have a few drawbacks, namely they are site-specific, +which means you cannot have multiple applications have different +profiles in the same website, while with subclassing you only need to +user different class names (to avoid parent->child fieldname clash). + +Moreover splitting the information in two different models can be +cumbersome when using ModelForms. + +Subclassing drawback: there's apparently a technique to use a +vhost-based profile class (with django.contrib.site and multiples +settings.py). But it's not useful for Savane IMHO. + +In addition, it seems impossible to convert an existing User to a +derived class from Python (this can be done through DB but that's +ugly). This apparently prevents auto-creating our derived class when a +new User is directly created (and sends a post_save signal). + +Profiles vs. inheritance is also described at +http://scottbarnham.com/blog/2008/08/21/extending-the-django-user-model-with-inheritance/ + +Note that Scott's authentication backend has the same issue than +profiles: only one profile class can be used on a single website, so +we don't use it. +""" + from django.db import models from django.contrib.auth import models as auth_models @@ -59,10 +95,36 @@ timezone = models.CharField(max_length=192, blank=True) theme = models.CharField(max_length=45, blank=True) + # Non-field link to extended groups + extendedgroup_set = models.ManyToManyField('ExtendedGroup', through='Membership') # Inherit specialized models.Manager with convenience functions objects = auth_models.UserManager() + @models.permalink + def get_absolute_url(self): + return ('savane.svmain.user_detail', [self.username]) + +# FIXME +# Let's make sure extendeduser is always created, even if somehow a +# normal User is created (from the admin interface, e.g.) +# This currently fails, and this doesn't support creating a model if +# another apps creates a derived class. +#from django.contrib.auth.models import User +#from django.db.models.signals import post_save +#def user_post_save_handler(sender, **kwargs): +# """ +# Create an ExtendedUser when a User is directly created +# +# Called when User is created +# (but not called when a ExtendedUser is created) +# """ +# u = kwargs['instance'] +# if kwargs['created'] == True: +# eu = ExtendedUser(user_ptr=u) +# eu.save() # ERROR +#post_save.connect(user_post_save_handler, sender=User) + class License(models.Model): """ Main license used by a project @@ -77,6 +139,10 @@ def __unicode__(self): return self.slug + ": " + self.name + @models.permalink + def get_absolute_url(self): + return ('savane.svmain.license_detail', [self.slug]) + class Meta: ordering = ['slug'] @@ -382,6 +448,50 @@ def __unicode__(self): return self.name + @models.permalink + def get_absolute_url(self): + return ('savane.svmain.group_detail', [self.name]) + class Meta: ordering = ['name'] + +class Membership(models.Model): + """ + Extra attributes about a User<->Group relationship + (e.g. "is the user an admin?") + """ + user = models.ForeignKey(ExtendedUser) + group = models.ForeignKey(ExtendedGroup) + + admin_flags_CHOICES = ( + ('A', 'Admin'), + # IMHO we need to put 'P' in a separate table, like 'pending + # membership', otherwise it's too easy to make mistakes + ('P', 'Pending moderation'), + ('SQD', 'Squad'), # FIXME: I dislike squad=user + ) + admin_flags = models.CharField(max_length=3, choices=admin_flags_CHOICES, + blank=True, help_text="membership properties") + onduty = models.BooleanField(default=True, + help_text="Untick to hide emeritous members from the project page") + + # TODO: split news params + #news_flags int(11) default NULL + + # Trackers-related + #privacy_flags = models.BooleanField(default=True) + #bugs_flags int(11) default NULL + #task_flags int(11) default NULL + #patch_flags int(11) default NULL + #support_flags int(11) default NULL + #cookbook_flags int(11) default NULL + + # Deprecated + #forum_flags int(11) default NULL + + def __unicode__(self): + return "[%s is a member of %s]" % (self.user.username, self.group.name) + + class Meta: + unique_together = (('user', 'group'),)
--- a/src/savane/svmain/urls.py +++ b/src/savane/svmain/urls.py @@ -19,6 +19,8 @@ from django.conf.urls.defaults import * +from savane.svmain import models as svmain_models + urlpatterns = patterns ('', url(r'^$', 'django.views.generic.simple.direct_to_template', { 'template' : 'index.html', @@ -27,4 +29,24 @@ url(r'^contact$', 'django.views.generic.simple.direct_to_template', { 'template' : 'contact.html' }, name='contact'), + + # TODO: not sure about the views naming convention - all this + # "models in 'svmain', views in 'my'" is getting messy, probably a + # mistake from me (Beuc) :P + url(r'^projects/(?P<slug>[-\w]+)$', 'django.views.generic.list_detail.object_detail', + { 'queryset' : svmain_models.ExtendedGroup.objects.all(), + 'slug_field' : 'name' }, + name='savane.svmain.group_detail'), + + url(r'^users/(?P<slug>[-\w]+)$', 'django.views.generic.list_detail.object_detail', + { 'queryset' : svmain_models.ExtendedUser.objects.all(), + 'slug_field' : 'username' }, + name='savane.svmain.user_detail'), + + url(r'^license/$', 'django.views.generic.list_detail.object_list', + { 'queryset' : svmain_models.License.objects.all(), }, + name='savane.svmain.license_list'), + url(r'^license/(?P<slug>[-\w]+)$', 'django.views.generic.list_detail.object_detail', + { 'queryset' : svmain_models.License.objects.all(), }, + name='savane.svmain.license_detail'), )
--- a/template/registration/login.html +++ b/template/registration/login.html @@ -2,14 +2,12 @@ {% extends "base.html" %} {% block content %} -Please login: +Login: + <form action="{% url django.contrib.auth.views.login %}" method="post"> -<dl> - <dt>Login Name:</dt> - <dd><input type="text" name="username" value="" size="12" /></dd> - <dt>Password:</dt> - <dd><input type="password" name="password" size="12" /></dd> - <dd><input type="submit" name="login" value="Login" /></dd> -</dl> +{{form.as_p}} +<input type="submit" name="login" value="Login" /> +</table> </form> + {% endblock %}
new file mode 100644 --- /dev/null +++ b/template/svmain/extendedgroup_detail.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block content %} + +<p> +Name: {{object.name}}<br /> +License: <a href="{{object.license.get_absolute_url}}">{{object.license.name}}</a><br /> +Development status: {{object.devel_status}}<br /> +</p> + +<p>Members:</p> + +{% if object.user_set.all %} +<ul> + {% for user in object.user_set.all %} + <li><a href="{{ user.extendeduser.get_absolute_url }}">{{ user.username }}</a></li> + {% endfor %} + </ul> +{% else %} + No members! +{% endif %} + +{% endblock %} + +{% comment %} +Local Variables: ** +mode: django-html ** +tab-width: 4 ** +indent-tabs-mode: nil ** +End: ** +{% endcomment %}
--- a/template/svmain/extendedgroup_list.html +++ b/template/svmain/extendedgroup_list.html @@ -5,11 +5,11 @@ {% if object_list %} <ul> {% for object in object_list %} - <li><a href="{{object.id}}">{{ object }}</a></li> + <li><a href="{{ object.get_absolute_url }}">{{ object }}</a></li> {% endfor %} </ul> {% else %} - <p>You are not part of any group.</p> + <p>No group.</p> {% endif %} {% endblock %}
new file mode 100644 --- /dev/null +++ b/template/svmain/extendeduser_detail.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} + +{% block content %} + +<p>Name: {{object.username}}</p> + +<p>Is a member of (using Django user.groups):</p> + +{% if object.groups.all %} +<ul> + {% for group in object.groups.all %} + <li> + <a href="{{ group.extendedgroup.get_absolute_url }}">{{ group.name }}</a> + </li> + {% endfor %} + </ul> +{% else %} + Not part of any group yet +{% endif %} + +<p>Is a member of (using Membership):</p> + +{% if object.membership_set.all %} +<ul> + {% for membership in object.membership_set.all %} + <li> + <a href="{{ membership.group.get_absolute_url }}">{{ membership.group.name }}</a> {{ membership.admin_flags}} + </li> + {% endfor %} + </ul> +{% else %} + Not part of any group yet +{% endif %} + + +<p>Is a member of (using Membership + direct field):</p> + +{% if object.extendedgroup_set.all %} +<ul> + {% for group in object.extendedgroup_set.all %} + <li> + <a href="{{ group.get_absolute_url }}">{{ group.name }}</a> + </li> + {% endfor %} + </ul> +{% else %} + Not part of any group yet +{% endif %} + + +{% endblock %} + +{% comment %} +Local Variables: ** +mode: django-html ** +tab-width: 4 ** +indent-tabs-mode: nil ** +End: ** +{% endcomment %}
new file mode 100644 --- /dev/null +++ b/template/svmain/license_detail.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} + +{% block content %} + +<p><a href="{% url savane.svmain.license_list %}">License list</a></p> + +<p> +Name: {{object.slug}}<br /> +URL: <a href="{{object.license.url}}">{{object.url}}</a><br /> +</p> + +<p>Projects that use it:</p> + +{{ object.extendedgroup_set.count }} project(s) use this license<br /> + +{% for eg in object.extendedgroup_set.all %} +<a href="{{eg.get_absolute_url}}">{{eg.name}}</a>{% if forloop.last %}{%else%},{% endif %} +{% endfor %} + +{% endblock %} + +{% comment %} +Local Variables: ** +mode: django-html ** +tab-width: 4 ** +indent-tabs-mode: nil ** +End: ** +{% endcomment %}
new file mode 100644 --- /dev/null +++ b/template/svmain/license_list.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block content %} + +{% for object in object_list %} +<a href="{{object.get_absolute_url}}">{{object.name}}</a><br /> +{% endfor %} + +{% endblock %} + +{% comment %} +Local Variables: ** +mode: django-html ** +tab-width: 4 ** +indent-tabs-mode: nil ** +End: ** +{% endcomment %}