commit 01ac12d50d7ab303395a27fd27e4e4a821e57b12
Author: Sofia Papagiannaki <papagian@gmail.com>
Date:   Mon Aug 6 19:01:19 2012 +0300

    Progress IV

diff --git a/snf-astakos-app/astakos/im/api/__init__.py b/snf-astakos-app/astakos/im/api/__init__.py
index 71dd97c..579b28d 100644
--- a/snf-astakos-app/astakos/im/api/__init__.py
+++ b/snf-astakos-app/astakos/im/api/__init__.py
@@ -164,19 +164,14 @@ def get_menu(request, with_extra_links=False, with_signout=True):
         l.append(dict(url=absolute(reverse('index')), name=user.email))
         l.append(dict(url=absolute(reverse('edit_profile')), name="My account"))
         if with_extra_links:
-            if user.has_usable_password() and user.provider == 'local':
+            if user.has_usable_password() and user.provider in ('local', ''):
                 l.append(dict(url=absolute(reverse('password_change')), name="Change password"))
             if EMAILCHANGE_ENABLED:
                 l.append(dict(url=absolute(reverse('email_change')), name="Change email"))
             if INVITATIONS_ENABLED:
                 l.append(dict(url=absolute(reverse('invite')), name="Invitations"))
             l.append(dict(url=absolute(reverse('feedback')), name="Feedback"))
-            if request.user.has_perm('im.add_astakosgroup'):
-                l.append(dict(url=absolute(reverse('group_add')), name="Add group"))
-            url = absolute(reverse('group_list'))
-            l.append(dict(url=url, name="Subscribed groups"))
-            url = '%s?relation=owner' % url
-            l.append(dict(url=url, name="My groups"))
+            l.append(dict(url=absolute(reverse('group_list')), name="Groups"))
         if with_signout:
             l.append(dict(url=absolute(reverse('logout')), name="Sign out"))
 
diff --git a/snf-astakos-app/astakos/im/forms.py b/snf-astakos-app/astakos/im/forms.py
index f1a5444..538c4c9 100644
--- a/snf-astakos-app/astakos/im/forms.py
+++ b/snf-astakos-app/astakos/im/forms.py
@@ -48,6 +48,7 @@ from django.contrib import messages
 from django.utils.encoding import smart_str
 from django.forms.extras.widgets import SelectDateWidget
 from django.db.models import Q
+from django.db.models.query import EmptyQuerySet
 
 from astakos.im.models import *
 from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, \
@@ -527,3 +528,19 @@ def get_astakos_group_policy_creation_form(astakosgroup):
             model = AstakosGroupQuota
     
     return AstakosGroupPolicyCreationForm
+
+class AstakosGroupSearchForm(forms.Form):
+    q = forms.CharField(max_length=200, label='')
+
+class MembershipCreationForm(forms.ModelForm):
+    # TODO check not to hit the db
+    group = forms.ModelChoiceField(queryset=AstakosGroup.objects.all(), widget=forms.HiddenInput())
+    person = forms.ModelChoiceField(queryset=AstakosUser.objects.all(), widget=forms.HiddenInput())
+    date_requested = forms.DateField(widget=forms.HiddenInput(), input_formats="%d/%m/%Y")
+    
+    class Meta:
+        model = Membership
+        exclude = ('date_joined',)
+    
+    def __init__(self, *args, **kwargs):
+        super(MembershipCreationForm, self).__init__(*args, **kwargs)
\ No newline at end of file
diff --git a/snf-astakos-app/astakos/im/models.py b/snf-astakos-app/astakos/im/models.py
index 0ab85b0..9c10d9a 100644
--- a/snf-astakos-app/astakos/im/models.py
+++ b/snf-astakos-app/astakos/im/models.py
@@ -141,9 +141,7 @@ class AstakosGroup(Group):
     
     @property
     def participants(self):
-        if not self.id:
-            return 0
-        return self.user_set.count()
+        return len(self.approved_members)
     
     def approve(self):
         self.approval_date = datetime.now()
@@ -153,23 +151,31 @@ class AstakosGroup(Group):
         self.approval_date = None
         self.save()
     
-    def approve_member(self, member):
-        m, created = self.membership_set.get_or_create(person=member, group=self)
-        m.date_joined = datetime.now()
-        m.save()
-        
-    def disapprove_member(self, member):
-        m = self.membership_set.remove(member)
+    def approve_member(self, person):
+        try:
+            self.membership_set.create(person=person, date_joined=datetime.now())
+        except IntegrityError:
+            m = self.membership_set.get(person=person)
+            m.date_joined = datetime.now()
+            m.save()
     
-    def get_members(self, approved=True):
-        if approved:
-            return self.membership_set().filter(is_approved=True)
-        return self.membership_set().all()
+    def disapprove_member(self, person):
+        self.membership_set.remove(person=person)
+    
+    @property
+    def members(self):
+        return map(lambda m:m.person, self.membership_set.all())
     
-    def get_policies(self):
-        related = self.policy.through.objects
-        return map(lambda r: {r.name:related.get(resource__id=r.id, group__id=self.id).limit}, self.policy.all())
+    @property
+    def approved_members(self):
+        f = filter(lambda m:m.is_approved, self.membership_set.all())
+        return map(lambda m:m.person, f)
     
+    @property
+    def policies(self):
+        return self.astakosgroupquota_set.all()
+    
+    @property
     def has_undefined_policies(self):
         # TODO: can avoid query?
         return Resource.objects.filter(~Q(astakosgroup=self)).exists()
@@ -346,8 +352,8 @@ class AstakosUser(User):
 class Membership(models.Model):
     person = models.ForeignKey(AstakosUser)
     group = models.ForeignKey(AstakosGroup)
-    date_requested = models.DateField(default=datetime.now())
-    date_joined = models.DateField(null=True, db_index=True)
+    date_requested = models.DateField(default=datetime.now(), blank=True)
+    date_joined = models.DateField(null=True, db_index=True, blank=True)
     
     class Meta:
         unique_together = ("person", "group")
@@ -357,6 +363,13 @@ class Membership(models.Model):
         if self.date_joined:
             return True
         return False
+    
+    def approve(self):
+        self.date_joined = datetime.now()
+        self.save()
+        
+    def disapprove(self):
+        self.delete()
 
 class AstakosGroupQuota(models.Model):
     limit = models.PositiveIntegerField('Limit')
diff --git a/snf-astakos-app/astakos/im/templates/im/astakosgroup_detail.html b/snf-astakos-app/astakos/im/templates/im/astakosgroup_detail.html
index 6390e12..bacfa97 100644
--- a/snf-astakos-app/astakos/im/templates/im/astakosgroup_detail.html
+++ b/snf-astakos-app/astakos/im/templates/im/astakosgroup_detail.html
@@ -1,29 +1,61 @@
 {% extends "im/account_base.html" %}
 
-{% load filters %}
-
 {% block page.body %}
 <div class="maincol {% block innerpage.class %}{% endblock %}">
         <table class="zebra-striped id-sorted">
               <tr>
                 <th>Name: {{object.name}}</th>
               </tr>
+              <tr>
+                <th>Type: {{object.kind}}</th>
+              </tr>
+              <tr>
+                <th>Issue date: {{object.issue_date}}</th>
+              </tr>
+              <tr>
+                <th>Expiration date: {{object.expiration_date}}</th>
+              </tr>
+              <tr>
+                <th>Owner: {% for o in object.owner.all %}
+                                {% if user == o %}
+                                    Me!
+                                {% else%}
+                                    {{o.realname}} ({{o.email}})
+                                
+                                {% endif %}
+                            {% endfor %}
+                </th>
+              </tr>
         </table>
     <div class="section">
         <h2>Members:</h2>
-        {% if members %}
+        {% if object.members %}
           <table class="zebra-striped id-sorted">
             <thead>
               <tr>
+                <th>Email</th>
                 <th>Realname</th>
                 <th>Status</th>
               </tr>
             </thead>
             <tbody>
-            {% for name, approved in members %}
+            {% for m in object.membership_set.all %}
               <tr>
-                <td>{{name}}</td>
-                <td>{{approved}}</td>
+                <td>{{m.person.email}}</td>
+                <td>{{m.person.realname}}</td>
+                {% if m.person in m.group.owner.all %}
+                <td>Owner</td>
+                {% else %}
+                    {% if m.is_approved %}
+                    <td>Approved</td>
+                    {% else %}
+                    <td>Pending</td>
+                        {% if user in m.group.owner.all %}
+                            <td><a href="{% url approve_member m.id %}">Approve</a></td>
+                            <td><a href="{% url disapprove_member m.id %}">Disapprove</a></td>
+                        {% endif %}
+                    {% endif %}
+                {% endif %}
               </tr>
             {% endfor %}
             </tbody>
@@ -44,12 +76,10 @@
             </thead>
             <tbody>
             {% for q in quota %}
-              {% for k in q|dkeys %}
                 <tr>
-                    <td>{{k}}</td>
-                    <td>{{q|lookup:k}}</td>
+                    <td>{{q.resource.name}}</td>
+                    <td>{{q.limit}}</td>
                   </tr>
-                {% endfor %}
             {% endfor %}
             </tbody>
         </table>
diff --git a/snf-astakos-app/astakos/im/templates/im/astakosgroup_list.html b/snf-astakos-app/astakos/im/templates/im/astakosgroup_list.html
index fcc77c9..7bb0f0a 100644
--- a/snf-astakos-app/astakos/im/templates/im/astakosgroup_list.html
+++ b/snf-astakos-app/astakos/im/templates/im/astakosgroup_list.html
@@ -1,19 +1,76 @@
 {% extends "im/account_base.html" %}
 
+{% load filters %}
+
 {% block page.body %}
 <div class="maincol {% block innerpage.class %}{% endblock %}">
+    {% if form %}
+    <form action="{% url group_search %}" method="post" class="innerlabels signup">{% csrf_token %}
+        <h2><span>Search group:</span></h2>
+            {% include "im/form_render.html" %}
+            <div class="form-row submit">
+                <input type="submit" class="submit altcol" value="SEARCH" />
+            </div>
+    </form>
+    {% else %}
+        <p class="submit-rt">
+            <a href="{% url group_add %}" class="submit">Create a group</a>
+            <a href="{% url group_search %}" class="submit">Join a group</a>
+        </p>
+    {% endif %}
       {% if object_list %}
       <h2>Groups:</h2>
       <table class="zebra-striped id-sorted">
             <thead>
               <tr>
                 <th>Name</th>
+                <th>Type</th>
+                <th>Issue date</th>
+                <th>Expiration date</th>
+                <th>Owner?</th>
+                <th>Participants</th>
+                <th>Enrollment status</th>
               </tr>
             </thead>
             <tbody>
               {% for o in object_list %}
               <tr>
-                <td><a class="extra-link" href="{% url group_detail o.id %}">{{ o.name }}</a></td>
+                <td><a class="extra-link" href="{% url group_detail o.id %}">{{o.name}}</a></td>
+                <td>{{o.kind}}</td>
+                <td>{{o.issue_date|date:"D d M Y"}}</td>
+                <td>{{o.expiration_date|date:"D d M Y"}}</td>
+                <td>{% if user in o.owner.all %}Yes{% else %}No{% endif %}</td>
+                <td>{{ o.approved_members|length }}/{{ o.members|length }}</td>
+                {% if user in o.approved_members %}
+                    <td>Active</td>
+                    {% if user not in o.owner.all %}
+                    <td>
+                        <form action="{% url group_leave o.id %}" method="post"class="login innerlabels">{% csrf_token %}
+                            <div class="form-row submit clearfix">
+                                <input type="submit" class="submit altcol" value="LEAVE" />
+                            </div>
+                        </form>
+                    </td>
+                    {% endif %}
+                {% else %}
+                    {% if user in o.members %}
+                        <td>Pending</td>
+                    {% else %}
+                        <td>Not member</td>
+                        {% if join_forms %}
+                        <td>
+                            <form action="{% url group_join o.id %}" method="post"class="login innerlabels">{% csrf_token %}
+                                {% with join_forms|lookup:o.name as form %}
+                                    {% include "im/form_render.html" %}
+                                {% endwith %}
+                                <div class="form-row submit clearfix">
+                                    <input type="submit" class="submit altcol" value="JOIN" />
+                                </div>
+                            </form>
+                        </td>
+                        {% endif %}
+                    {% endif %}
+                {% endif %}
               </tr>
               {% endfor %}
             </tbody>
diff --git a/snf-astakos-app/astakos/im/urls.py b/snf-astakos-app/astakos/im/urls.py
index 0780e74..b40236a 100644
--- a/snf-astakos-app/astakos/im/urls.py
+++ b/snf-astakos-app/astakos/im/urls.py
@@ -54,7 +54,12 @@ urlpatterns = patterns('astakos.im.views',
     url(r'^group/(?P<group_id>\d+)/?$', 'group_detail', {}, name='group_detail'),
     url(r'^group/(?P<group_id>\d+)/policies/list/?$', 'group_policies_list', {}, name='group_policies_list'),
     url(r'^group/(?P<group_id>\d+)/policies/add/?$', 'group_policies_add', {}, name='group_policies_add'),
+    url(r'^group/search/?$', 'group_search', {}, name='group_search'),
+    url(r'^group/(?P<group_id>\d+)/join/?$', 'group_join', {}, name='group_join'),
+    url(r'^group/(?P<group_id>\d+)/leave/?$', 'group_leave', {}, name='group_leave'),
     url(r'^group/(?P<group_id>\d+)/request/approval/?$', 'group_approval_request', {}, name='group_approval_request'),
+    url(r'^group/(?P<membership_id>\d+)/approve/?$', 'approve_member', {}, name='approve_member'),
+    url(r'^group/(?P<membership_id>\d+)/disapprove/?$', 'disapprove_member', {}, name='disapprove_member'),
 )
 
 if EMAILCHANGE_ENABLED:
diff --git a/snf-astakos-app/astakos/im/views.py b/snf-astakos-app/astakos/im/views.py
index 8ab78ed..7013906 100644
--- a/snf-astakos-app/astakos/im/views.py
+++ b/snf-astakos-app/astakos/im/views.py
@@ -38,23 +38,22 @@ from smtplib import SMTPException
 from urllib import quote
 from functools import wraps
 
-from django.core.mail import send_mail
-from django.http import HttpResponse, HttpResponseBadRequest
-from django.shortcuts import redirect
-from django.template.loader import render_to_string
-from django.utils.translation import ugettext as _
-from django.core.urlresolvers import reverse
-from django.contrib.auth.decorators import login_required
 from django.contrib import messages
-from django.db import transaction
-from django.utils.http import urlencode
-from django.http import HttpResponseRedirect, HttpResponseBadRequest
-from django.db.utils import IntegrityError
+from django.contrib.auth.decorators import login_required
 from django.contrib.auth.views import password_change
 from django.core.exceptions import ValidationError
+from django.core.mail import send_mail
+from django.core.urlresolvers import reverse
+from django.db import transaction
 from django.db.models import Q
-from django.forms.models import inlineformset_factory
-from django.forms.models import inlineformset_factory
+from django.db.utils import IntegrityError
+from django.forms.fields import URLField
+from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
+    HttpResponseRedirect, HttpResponseBadRequest
+from django.shortcuts import redirect
+from django.template.loader import render_to_string
+from django.utils.http import urlencode
+from django.utils.translation import ugettext as _
 from django.views.generic.create_update import *
 from django.views.generic.list_detail import *
 
@@ -588,20 +587,16 @@ def change_email(request, activation_key=None,
                                                           extra_context))
 
 @signed_terms_required
+@login_required
 def group_add(request):
     return create_object(request,
-                            form_class=get_astakos_group_creation_form(request),
-                            login_required = True,
-                            post_save_redirect = '/im/group/%(id)s/')
+                         form_class=get_astakos_group_creation_form(request),
+                         post_save_redirect = '/im/group/%(id)s/')
 
 @signed_terms_required
 @login_required
 def group_list(request):
-    relation = get_query(request).get('relation', 'member')
-    if relation == 'member':
-        list = AstakosGroup.objects.filter(membership__person=request.user)
-    else:
-        list = AstakosGroup.objects.filter(owner__id=request.user.id)
+    list = AstakosGroup.objects.filter(membership__person=request.user)
     return object_list(request, queryset=list)
 
 @signed_terms_required
@@ -611,14 +606,12 @@ def group_detail(request, group_id):
         group = AstakosGroup.objects.select_related().get(id=group_id)
     except AstakosGroup.DoesNotExist:
         return HttpResponseBadRequest(_('Invalid group.'))
-    members = map(lambda m:{m.person.realname:m.is_approved}, group.membership_set.all())
     return object_detail(request,
-                            AstakosGroup.objects.all(),
-                            object_id=group_id,
-                            extra_context = {'quota':group.get_policies(),
-                                             'members':members,
-                                             'form':get_astakos_group_policy_creation_form(group),
-                                             'more_policies':group.has_undefined_policies()})
+                         AstakosGroup.objects.all(),
+                         object_id=group_id,
+                         extra_context = {'form':get_astakos_group_policy_creation_form(group),
+                                          'quota':group.policies,
+                                          'more_policies':group.has_undefined_policies})
 
 @signed_terms_required
 @login_required
@@ -627,21 +620,113 @@ def group_policies_list(request, group_id):
     return object_list(request, queryset=list)
 
 @signed_terms_required
+@login_required
 def group_policies_add(request, group_id):
     try:
         group = AstakosGroup.objects.select_related().get(id=group_id)
     except AstakosGroup.DoesNotExist:
         return HttpResponseBadRequest(_('Invalid group.'))
     return create_object(request,
-                            form_class=get_astakos_group_policy_creation_form(group),
-                            login_required=True,
-                            template_name = 'im/astakosgroup_detail.html',
-                            post_save_redirect = reverse('group_detail', kwargs=dict(group_id=group_id)),
-                            extra_context = {'group':group,
-                                             'quota':group.get_policies(),
-                                             'more_policies':group.has_undefined_policies()})
+                         form_class=get_astakos_group_policy_creation_form(group),
+                         template_name = 'im/astakosgroup_detail.html',
+                         post_save_redirect = reverse('group_detail', kwargs=dict(group_id=group_id)),
+                         extra_context = {'group':group,
+                                          'quota':group.policies,
+                                          'more_policies':group.has_undefined_policies})
 @signed_terms_required
 @login_required
 def group_approval_request(request, group_id):
     return HttpResponse()
+
+@signed_terms_required
+@login_required
+def group_search(request, queryset=EmptyQuerySet(), extra_context={}, **kwargs):
+    join_forms = {}
+    if request.method == 'GET':
+        form = AstakosGroupSearchForm()
+    else:
+        form = AstakosGroupSearchForm(get_query(request))
+        if form.is_valid():
+            q = form.cleaned_data['q'].strip()
+            q = URLField().to_python(q)
+            queryset = AstakosGroup.objects.select_related().filter(name=q)
+            f = MembershipCreationForm
+            for g in queryset:
+                join_forms[g.name] = f(dict(group=g,
+                                            person=request.user,
+                                            date_requested=datetime.now().strftime("%d/%m/%Y")))
+    return object_list(request,
+                        queryset,
+                        template_name='im/astakosgroup_list.html',
+                        extra_context=dict(form=form, is_search=True, join_forms=join_forms))
+
+@signed_terms_required
+@login_required
+def group_join(request, group_id):
+    return create_object(request,
+                         model=Membership,
+                         template_name='im/astakosgroup_list.html',
+                         post_save_redirect = reverse('group_detail', kwargs=dict(group_id=group_id)))
+
+@signed_terms_required
+@login_required
+def group_leave(request, group_id):
+    try:
+        m = Membership.objects.select_related().get(group__id=group_id, person=request.user)
+    except Membership.DoesNotExist:
+        return HttpResponseBadRequest(_('Invalid membership.'))
+    if request.user in m.group.owner.all():
+        return HttpResponseForbidden(_('Owner can not leave the group.'))
+    return delete_object(request,
+                         model=Membership,
+                         object_id = m.id,
+                         template_name='im/astakosgroup_list.html',
+                         post_delete_redirect = reverse('group_detail', kwargs=dict(group_id=group_id)))
+
+def handle_membership():
+    def decorator(func):
+        @wraps(func)
+        def wrapper(request, membership_id):
+            try:
+                m = Membership.objects.select_related().get(id=membership_id)
+            except Membership.DoesNotExist:
+                return HttpResponseBadRequest(_('Invalid membership.'))
+            else:
+                if request.user not in m.group.owner.all():
+                    return HttpResponseForbidden(_('User is not a group owner.'))
+                func(request, m)
+                return render_response(template='im/astakosgroup_detail.html',
+                                       context_instance=get_context(request),
+                                       object=m.group,
+                                       quota=m.group.policies,
+                                       more_policies=m.group.has_undefined_policies)
+        return wrapper
+    return decorator
+
+@signed_terms_required
+@login_required
+@handle_membership()
+def approve_member(request, membership):
+    try:
+        membership.approve()
+        realname = membership.person.realname
+        msg = _('%s has been successfully joined the group.' % realname)
+        messages.success(request, msg)
+    except BaseException, e:
+        logger.exception(e)
+        msg = _('Something went wrong during %s\'s approval.' % realname)
+        messages.error(request, msg)
     
+@signed_terms_required
+@login_required
+@handle_membership()
+def disapprove_member(request, membership):
+    try:
+        membership.disapprove()
+        realname = membership.person.realname
+        msg = _('%s has been successfully removed from the group.' % realname)
+        messages.success(request, msg)
+    except BaseException, e:
+        logger.exception(e)
+        msg = _('Something went wrong during %s\'s disapproval.' % realname)
+        messages.error(request, msg)
