Revision 01ac12d5

b/snf-astakos-app/astakos/im/api/__init__.py
164 164
        l.append(dict(url=absolute(reverse('index')), name=user.email))
165 165
        l.append(dict(url=absolute(reverse('edit_profile')), name="My account"))
166 166
        if with_extra_links:
167
            if user.has_usable_password() and user.provider == 'local':
167
            if user.has_usable_password() and user.provider in ('local', ''):
168 168
                l.append(dict(url=absolute(reverse('password_change')), name="Change password"))
169 169
            if EMAILCHANGE_ENABLED:
170 170
                l.append(dict(url=absolute(reverse('email_change')), name="Change email"))
171 171
            if INVITATIONS_ENABLED:
172 172
                l.append(dict(url=absolute(reverse('invite')), name="Invitations"))
173 173
            l.append(dict(url=absolute(reverse('feedback')), name="Feedback"))
174
            if request.user.has_perm('im.add_astakosgroup'):
175
                l.append(dict(url=absolute(reverse('group_add')), name="Add group"))
176
            url = absolute(reverse('group_list'))
177
            l.append(dict(url=url, name="Subscribed groups"))
178
            url = '%s?relation=owner' % url
179
            l.append(dict(url=url, name="My groups"))
174
            l.append(dict(url=absolute(reverse('group_list')), name="Groups"))
180 175
        if with_signout:
181 176
            l.append(dict(url=absolute(reverse('logout')), name="Sign out"))
182 177

  
b/snf-astakos-app/astakos/im/forms.py
48 48
from django.utils.encoding import smart_str
49 49
from django.forms.extras.widgets import SelectDateWidget
50 50
from django.db.models import Q
51
from django.db.models.query import EmptyQuerySet
51 52

  
52 53
from astakos.im.models import *
53 54
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, \
......
527 528
            model = AstakosGroupQuota
528 529
    
529 530
    return AstakosGroupPolicyCreationForm
531

  
532
class AstakosGroupSearchForm(forms.Form):
533
    q = forms.CharField(max_length=200, label='')
534

  
535
class MembershipCreationForm(forms.ModelForm):
536
    # TODO check not to hit the db
537
    group = forms.ModelChoiceField(queryset=AstakosGroup.objects.all(), widget=forms.HiddenInput())
538
    person = forms.ModelChoiceField(queryset=AstakosUser.objects.all(), widget=forms.HiddenInput())
539
    date_requested = forms.DateField(widget=forms.HiddenInput(), input_formats="%d/%m/%Y")
540
    
541
    class Meta:
542
        model = Membership
543
        exclude = ('date_joined',)
544
    
545
    def __init__(self, *args, **kwargs):
546
        super(MembershipCreationForm, self).__init__(*args, **kwargs)
b/snf-astakos-app/astakos/im/models.py
141 141
    
142 142
    @property
143 143
    def participants(self):
144
        if not self.id:
145
            return 0
146
        return self.user_set.count()
144
        return len(self.approved_members)
147 145
    
148 146
    def approve(self):
149 147
        self.approval_date = datetime.now()
......
153 151
        self.approval_date = None
154 152
        self.save()
155 153
    
156
    def approve_member(self, member):
157
        m, created = self.membership_set.get_or_create(person=member, group=self)
158
        m.date_joined = datetime.now()
159
        m.save()
160
        
161
    def disapprove_member(self, member):
162
        m = self.membership_set.remove(member)
154
    def approve_member(self, person):
155
        try:
156
            self.membership_set.create(person=person, date_joined=datetime.now())
157
        except IntegrityError:
158
            m = self.membership_set.get(person=person)
159
            m.date_joined = datetime.now()
160
            m.save()
163 161
    
164
    def get_members(self, approved=True):
165
        if approved:
166
            return self.membership_set().filter(is_approved=True)
167
        return self.membership_set().all()
162
    def disapprove_member(self, person):
163
        self.membership_set.remove(person=person)
164
    
165
    @property
166
    def members(self):
167
        return map(lambda m:m.person, self.membership_set.all())
168 168
    
169
    def get_policies(self):
170
        related = self.policy.through.objects
171
        return map(lambda r: {r.name:related.get(resource__id=r.id, group__id=self.id).limit}, self.policy.all())
169
    @property
170
    def approved_members(self):
171
        f = filter(lambda m:m.is_approved, self.membership_set.all())
172
        return map(lambda m:m.person, f)
172 173
    
174
    @property
175
    def policies(self):
176
        return self.astakosgroupquota_set.all()
177
    
178
    @property
173 179
    def has_undefined_policies(self):
174 180
        # TODO: can avoid query?
175 181
        return Resource.objects.filter(~Q(astakosgroup=self)).exists()
......
346 352
class Membership(models.Model):
347 353
    person = models.ForeignKey(AstakosUser)
348 354
    group = models.ForeignKey(AstakosGroup)
349
    date_requested = models.DateField(default=datetime.now())
350
    date_joined = models.DateField(null=True, db_index=True)
355
    date_requested = models.DateField(default=datetime.now(), blank=True)
356
    date_joined = models.DateField(null=True, db_index=True, blank=True)
351 357
    
352 358
    class Meta:
353 359
        unique_together = ("person", "group")
......
357 363
        if self.date_joined:
358 364
            return True
359 365
        return False
366
    
367
    def approve(self):
368
        self.date_joined = datetime.now()
369
        self.save()
370
        
371
    def disapprove(self):
372
        self.delete()
360 373

  
361 374
class AstakosGroupQuota(models.Model):
362 375
    limit = models.PositiveIntegerField('Limit')
b/snf-astakos-app/astakos/im/templates/im/astakosgroup_detail.html
1 1
{% extends "im/account_base.html" %}
2 2

  
3
{% load filters %}
4

  
5 3
{% block page.body %}
6 4
<div class="maincol {% block innerpage.class %}{% endblock %}">
7 5
        <table class="zebra-striped id-sorted">
8 6
              <tr>
9 7
                <th>Name: {{object.name}}</th>
10 8
              </tr>
9
              <tr>
10
                <th>Type: {{object.kind}}</th>
11
              </tr>
12
              <tr>
13
                <th>Issue date: {{object.issue_date}}</th>
14
              </tr>
15
              <tr>
16
                <th>Expiration date: {{object.expiration_date}}</th>
17
              </tr>
18
              <tr>
19
                <th>Owner: {% for o in object.owner.all %}
20
                                {% if user == o %}
21
                                    Me!
22
                                {% else%}
23
                                    {{o.realname}} ({{o.email}})
24
                                
25
                                {% endif %}
26
                            {% endfor %}
27
                </th>
28
              </tr>
11 29
        </table>
12 30
    <div class="section">
13 31
        <h2>Members:</h2>
14
        {% if members %}
32
        {% if object.members %}
15 33
          <table class="zebra-striped id-sorted">
16 34
            <thead>
17 35
              <tr>
36
                <th>Email</th>
18 37
                <th>Realname</th>
19 38
                <th>Status</th>
20 39
              </tr>
21 40
            </thead>
22 41
            <tbody>
23
            {% for name, approved in members %}
42
            {% for m in object.membership_set.all %}
24 43
              <tr>
25
                <td>{{name}}</td>
26
                <td>{{approved}}</td>
44
                <td>{{m.person.email}}</td>
45
                <td>{{m.person.realname}}</td>
46
                {% if m.person in m.group.owner.all %}
47
                <td>Owner</td>
48
                {% else %}
49
                    {% if m.is_approved %}
50
                    <td>Approved</td>
51
                    {% else %}
52
                    <td>Pending</td>
53
                        {% if user in m.group.owner.all %}
54
                            <td><a href="{% url approve_member m.id %}">Approve</a></td>
55
                            <td><a href="{% url disapprove_member m.id %}">Disapprove</a></td>
56
                        {% endif %}
57
                    {% endif %}
58
                {% endif %}
27 59
              </tr>
28 60
            {% endfor %}
29 61
            </tbody>
......
44 76
            </thead>
45 77
            <tbody>
46 78
            {% for q in quota %}
47
              {% for k in q|dkeys %}
48 79
                <tr>
49
                    <td>{{k}}</td>
50
                    <td>{{q|lookup:k}}</td>
80
                    <td>{{q.resource.name}}</td>
81
                    <td>{{q.limit}}</td>
51 82
                  </tr>
52
                {% endfor %}
53 83
            {% endfor %}
54 84
            </tbody>
55 85
        </table>
b/snf-astakos-app/astakos/im/templates/im/astakosgroup_list.html
1 1
{% extends "im/account_base.html" %}
2 2

  
3
{% load filters %}
4

  
3 5
{% block page.body %}
4 6
<div class="maincol {% block innerpage.class %}{% endblock %}">
7
    {% if form %}
8
    <form action="{% url group_search %}" method="post" class="innerlabels signup">{% csrf_token %}
9
        <h2><span>Search group:</span></h2>
10
            {% include "im/form_render.html" %}
11
            <div class="form-row submit">
12
                <input type="submit" class="submit altcol" value="SEARCH" />
13
            </div>
14
    </form>
15
    {% else %}
16
        <p class="submit-rt">
17
            <a href="{% url group_add %}" class="submit">Create a group</a>
18
            <a href="{% url group_search %}" class="submit">Join a group</a>
19
        </p>
20
    {% endif %}
5 21
      {% if object_list %}
6 22
      <h2>Groups:</h2>
7 23
      <table class="zebra-striped id-sorted">
8 24
            <thead>
9 25
              <tr>
10 26
                <th>Name</th>
27
                <th>Type</th>
28
                <th>Issue date</th>
29
                <th>Expiration date</th>
30
                <th>Owner?</th>
31
                <th>Participants</th>
32
                <th>Enrollment status</th>
11 33
              </tr>
12 34
            </thead>
13 35
            <tbody>
14 36
              {% for o in object_list %}
15 37
              <tr>
16
                <td><a class="extra-link" href="{% url group_detail o.id %}">{{ o.name }}</a></td>
38
                <td><a class="extra-link" href="{% url group_detail o.id %}">{{o.name}}</a></td>
39
                <td>{{o.kind}}</td>
40
                <td>{{o.issue_date|date:"D d M Y"}}</td>
41
                <td>{{o.expiration_date|date:"D d M Y"}}</td>
42
                <td>{% if user in o.owner.all %}Yes{% else %}No{% endif %}</td>
43
                <td>{{ o.approved_members|length }}/{{ o.members|length }}</td>
44
                {% if user in o.approved_members %}
45
                    <td>Active</td>
46
                    {% if user not in o.owner.all %}
47
                    <td>
48
                        <form action="{% url group_leave o.id %}" method="post"class="login innerlabels">{% csrf_token %}
49
                            <div class="form-row submit clearfix">
50
                                <input type="submit" class="submit altcol" value="LEAVE" />
51
                            </div>
52
                        </form>
53
                    </td>
54
                    {% endif %}
55
                {% else %}
56
                    {% if user in o.members %}
57
                        <td>Pending</td>
58
                    {% else %}
59
                        <td>Not member</td>
60
                        {% if join_forms %}
61
                        <td>
62
                            <form action="{% url group_join o.id %}" method="post"class="login innerlabels">{% csrf_token %}
63
                                {% with join_forms|lookup:o.name as form %}
64
                                    {% include "im/form_render.html" %}
65
                                {% endwith %}
66
                                <div class="form-row submit clearfix">
67
                                    <input type="submit" class="submit altcol" value="JOIN" />
68
                                </div>
69
                            </form>
70
                        </td>
71
                        {% endif %}
72
                    {% endif %}
73
                {% endif %}
17 74
              </tr>
18 75
              {% endfor %}
19 76
            </tbody>
b/snf-astakos-app/astakos/im/urls.py
54 54
    url(r'^group/(?P<group_id>\d+)/?$', 'group_detail', {}, name='group_detail'),
55 55
    url(r'^group/(?P<group_id>\d+)/policies/list/?$', 'group_policies_list', {}, name='group_policies_list'),
56 56
    url(r'^group/(?P<group_id>\d+)/policies/add/?$', 'group_policies_add', {}, name='group_policies_add'),
57
    url(r'^group/search/?$', 'group_search', {}, name='group_search'),
58
    url(r'^group/(?P<group_id>\d+)/join/?$', 'group_join', {}, name='group_join'),
59
    url(r'^group/(?P<group_id>\d+)/leave/?$', 'group_leave', {}, name='group_leave'),
57 60
    url(r'^group/(?P<group_id>\d+)/request/approval/?$', 'group_approval_request', {}, name='group_approval_request'),
61
    url(r'^group/(?P<membership_id>\d+)/approve/?$', 'approve_member', {}, name='approve_member'),
62
    url(r'^group/(?P<membership_id>\d+)/disapprove/?$', 'disapprove_member', {}, name='disapprove_member'),
58 63
)
59 64

  
60 65
if EMAILCHANGE_ENABLED:
b/snf-astakos-app/astakos/im/views.py
38 38
from urllib import quote
39 39
from functools import wraps
40 40

  
41
from django.core.mail import send_mail
42
from django.http import HttpResponse, HttpResponseBadRequest
43
from django.shortcuts import redirect
44
from django.template.loader import render_to_string
45
from django.utils.translation import ugettext as _
46
from django.core.urlresolvers import reverse
47
from django.contrib.auth.decorators import login_required
48 41
from django.contrib import messages
49
from django.db import transaction
50
from django.utils.http import urlencode
51
from django.http import HttpResponseRedirect, HttpResponseBadRequest
52
from django.db.utils import IntegrityError
42
from django.contrib.auth.decorators import login_required
53 43
from django.contrib.auth.views import password_change
54 44
from django.core.exceptions import ValidationError
45
from django.core.mail import send_mail
46
from django.core.urlresolvers import reverse
47
from django.db import transaction
55 48
from django.db.models import Q
56
from django.forms.models import inlineformset_factory
57
from django.forms.models import inlineformset_factory
49
from django.db.utils import IntegrityError
50
from django.forms.fields import URLField
51
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
52
    HttpResponseRedirect, HttpResponseBadRequest
53
from django.shortcuts import redirect
54
from django.template.loader import render_to_string
55
from django.utils.http import urlencode
56
from django.utils.translation import ugettext as _
58 57
from django.views.generic.create_update import *
59 58
from django.views.generic.list_detail import *
60 59

  
......
588 587
                                                          extra_context))
589 588

  
590 589
@signed_terms_required
590
@login_required
591 591
def group_add(request):
592 592
    return create_object(request,
593
                            form_class=get_astakos_group_creation_form(request),
594
                            login_required = True,
595
                            post_save_redirect = '/im/group/%(id)s/')
593
                         form_class=get_astakos_group_creation_form(request),
594
                         post_save_redirect = '/im/group/%(id)s/')
596 595

  
597 596
@signed_terms_required
598 597
@login_required
599 598
def group_list(request):
600
    relation = get_query(request).get('relation', 'member')
601
    if relation == 'member':
602
        list = AstakosGroup.objects.filter(membership__person=request.user)
603
    else:
604
        list = AstakosGroup.objects.filter(owner__id=request.user.id)
599
    list = AstakosGroup.objects.filter(membership__person=request.user)
605 600
    return object_list(request, queryset=list)
606 601

  
607 602
@signed_terms_required
......
611 606
        group = AstakosGroup.objects.select_related().get(id=group_id)
612 607
    except AstakosGroup.DoesNotExist:
613 608
        return HttpResponseBadRequest(_('Invalid group.'))
614
    members = map(lambda m:{m.person.realname:m.is_approved}, group.membership_set.all())
615 609
    return object_detail(request,
616
                            AstakosGroup.objects.all(),
617
                            object_id=group_id,
618
                            extra_context = {'quota':group.get_policies(),
619
                                             'members':members,
620
                                             'form':get_astakos_group_policy_creation_form(group),
621
                                             'more_policies':group.has_undefined_policies()})
610
                         AstakosGroup.objects.all(),
611
                         object_id=group_id,
612
                         extra_context = {'form':get_astakos_group_policy_creation_form(group),
613
                                          'quota':group.policies,
614
                                          'more_policies':group.has_undefined_policies})
622 615

  
623 616
@signed_terms_required
624 617
@login_required
......
627 620
    return object_list(request, queryset=list)
628 621

  
629 622
@signed_terms_required
623
@login_required
630 624
def group_policies_add(request, group_id):
631 625
    try:
632 626
        group = AstakosGroup.objects.select_related().get(id=group_id)
633 627
    except AstakosGroup.DoesNotExist:
634 628
        return HttpResponseBadRequest(_('Invalid group.'))
635 629
    return create_object(request,
636
                            form_class=get_astakos_group_policy_creation_form(group),
637
                            login_required=True,
638
                            template_name = 'im/astakosgroup_detail.html',
639
                            post_save_redirect = reverse('group_detail', kwargs=dict(group_id=group_id)),
640
                            extra_context = {'group':group,
641
                                             'quota':group.get_policies(),
642
                                             'more_policies':group.has_undefined_policies()})
630
                         form_class=get_astakos_group_policy_creation_form(group),
631
                         template_name = 'im/astakosgroup_detail.html',
632
                         post_save_redirect = reverse('group_detail', kwargs=dict(group_id=group_id)),
633
                         extra_context = {'group':group,
634
                                          'quota':group.policies,
635
                                          'more_policies':group.has_undefined_policies})
643 636
@signed_terms_required
644 637
@login_required
645 638
def group_approval_request(request, group_id):
646 639
    return HttpResponse()
640

  
641
@signed_terms_required
642
@login_required
643
def group_search(request, queryset=EmptyQuerySet(), extra_context={}, **kwargs):
644
    join_forms = {}
645
    if request.method == 'GET':
646
        form = AstakosGroupSearchForm()
647
    else:
648
        form = AstakosGroupSearchForm(get_query(request))
649
        if form.is_valid():
650
            q = form.cleaned_data['q'].strip()
651
            q = URLField().to_python(q)
652
            queryset = AstakosGroup.objects.select_related().filter(name=q)
653
            f = MembershipCreationForm
654
            for g in queryset:
655
                join_forms[g.name] = f(dict(group=g,
656
                                            person=request.user,
657
                                            date_requested=datetime.now().strftime("%d/%m/%Y")))
658
    return object_list(request,
659
                        queryset,
660
                        template_name='im/astakosgroup_list.html',
661
                        extra_context=dict(form=form, is_search=True, join_forms=join_forms))
662

  
663
@signed_terms_required
664
@login_required
665
def group_join(request, group_id):
666
    return create_object(request,
667
                         model=Membership,
668
                         template_name='im/astakosgroup_list.html',
669
                         post_save_redirect = reverse('group_detail', kwargs=dict(group_id=group_id)))
670

  
671
@signed_terms_required
672
@login_required
673
def group_leave(request, group_id):
674
    try:
675
        m = Membership.objects.select_related().get(group__id=group_id, person=request.user)
676
    except Membership.DoesNotExist:
677
        return HttpResponseBadRequest(_('Invalid membership.'))
678
    if request.user in m.group.owner.all():
679
        return HttpResponseForbidden(_('Owner can not leave the group.'))
680
    return delete_object(request,
681
                         model=Membership,
682
                         object_id = m.id,
683
                         template_name='im/astakosgroup_list.html',
684
                         post_delete_redirect = reverse('group_detail', kwargs=dict(group_id=group_id)))
685

  
686
def handle_membership():
687
    def decorator(func):
688
        @wraps(func)
689
        def wrapper(request, membership_id):
690
            try:
691
                m = Membership.objects.select_related().get(id=membership_id)
692
            except Membership.DoesNotExist:
693
                return HttpResponseBadRequest(_('Invalid membership.'))
694
            else:
695
                if request.user not in m.group.owner.all():
696
                    return HttpResponseForbidden(_('User is not a group owner.'))
697
                func(request, m)
698
                return render_response(template='im/astakosgroup_detail.html',
699
                                       context_instance=get_context(request),
700
                                       object=m.group,
701
                                       quota=m.group.policies,
702
                                       more_policies=m.group.has_undefined_policies)
703
        return wrapper
704
    return decorator
705

  
706
@signed_terms_required
707
@login_required
708
@handle_membership()
709
def approve_member(request, membership):
710
    try:
711
        membership.approve()
712
        realname = membership.person.realname
713
        msg = _('%s has been successfully joined the group.' % realname)
714
        messages.success(request, msg)
715
    except BaseException, e:
716
        logger.exception(e)
717
        msg = _('Something went wrong during %s\'s approval.' % realname)
718
        messages.error(request, msg)
647 719
    
720
@signed_terms_required
721
@login_required
722
@handle_membership()
723
def disapprove_member(request, membership):
724
    try:
725
        membership.disapprove()
726
        realname = membership.person.realname
727
        msg = _('%s has been successfully removed from the group.' % realname)
728
        messages.success(request, msg)
729
    except BaseException, e:
730
        logger.exception(e)
731
        msg = _('Something went wrong during %s\'s disapproval.' % realname)
732
        messages.error(request, msg)

Also available in: Unified diff