Revision 270dd48d

b/docs/source/devguide.rst
158 158
auth_token                   Authentication token
159 159
auth_token_expires           Token expiration date
160 160
auth_token_created           Token creation date
161
has_credits                  Whether user has credits
162
has_signed_terms             Whether user has aggred on terms
161 163
===========================  ============================
162 164

  
163 165
Example reply:
......
168 170
  "uniq": "papagian@example.com"
169 171
  "auth_token": "0000",
170 172
  "auth_token_expires": "Tue, 11-Sep-2012 09:17:14 ",
171
  "auth_token_created": "Sun, 11-Sep-2011 09:17:14 "}
173
  "auth_token_created": "Sun, 11-Sep-2011 09:17:14 ",
174
  "has_credits": false,
175
  "has_signed_terms": true}
172 176

  
173 177
|
174 178

  
......
177 181
=========================== =====================
178 182
204 (No Content)            The request succeeded
179 183
400 (Bad Request)           The request is invalid
180
401 (Unauthorized)          Missing token or inactive user
184
401 (Unauthorized)          Missing token or inactive user or penging approval terms
181 185
500 (Internal Server Error) The request cannot be completed because of an internal error
182 186
=========================== =====================
183 187

  
b/snf-astakos-app/README
92 92
modifyuser       Modify a user's attributes
93 93
showinvitation   Show invitation info
94 94
showuser         Show user info
95
addterms         Add new approval terms
95 96
===============  ===========================
b/snf-astakos-app/astakos/im/api.py
46 46
from astakos.im.faults import BadRequest, Unauthorized, InternalServerError
47 47
from astakos.im.models import AstakosUser
48 48
from astakos.im.settings import CLOUD_SERVICES, INVITATIONS_ENABLED
49
from astakos.im.util import has_signed_terms
49 50

  
50 51
logger = logging.getLogger(__name__)
51 52

  
......
85 86
        # Check if the token has expired.
86 87
        if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
87 88
            return render_fault(request, Unauthorized('Authentication expired'))
88

  
89
        
90
        if not has_signed_terms(user):
91
            return render_fault(request, Unauthorized('Pending approval terms'))
92
        
89 93
        response = HttpResponse()
90 94
        response.status=204
91 95
        user_info = {'username':user.username,
......
93 97
                     'auth_token':user.auth_token,
94 98
                     'auth_token_created':user.auth_token_created.isoformat(),
95 99
                     'auth_token_expires':user.auth_token_expires.isoformat(),
96
                     'has_credits':user.has_credits}
100
                     'has_credits':user.has_credits,
101
                     'has_signed_terms':has_signed_terms(user)}
97 102
        response.content = json.dumps(user_info)
98 103
        response['Content-Type'] = 'application/json; charset=UTF-8'
99 104
        response['Content-Length'] = len(response.content)
b/snf-astakos-app/astakos/im/context_processors.py
43 43
    return {'im_modules': IM_MODULES}
44 44

  
45 45
def next(request):
46
    return {'next' : request.GET.get('next', '')}
46
    query_dict = request.__getattribute__(request.method)
47
    return {'next' : query_dict.get('next', '')}
47 48

  
48 49
def code(request):
49 50
    return {'code' : request.GET.get('code', '')}
b/snf-astakos-app/astakos/im/forms.py
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.
33 33
from urlparse import urljoin
34
from datetime import datetime
34 35

  
35 36
from django import forms
36 37
from django.utils.translation import ugettext as _
......
40 41
from django.template import Context, loader
41 42
from django.utils.http import int_to_base36
42 43
from django.core.urlresolvers import reverse
44
from django.utils.functional import lazy
43 45

  
44 46
from astakos.im.models import AstakosUser
45 47
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL
46
from astakos.im.widgets import DummyWidget, RecaptchaWidget
48
from astakos.im.widgets import DummyWidget, RecaptchaWidget, ApprovalTermsWidget
49

  
50
# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
51
from astakos.im.util import reverse_lazy
47 52

  
48 53
import logging
49 54
import recaptcha.client.captcha as captcha
......
63 68
    
64 69
    class Meta:
65 70
        model = AstakosUser
66
        fields = ("email", "first_name", "last_name")
71
        fields = ("email", "first_name", "last_name", "has_signed_terms")
72
        widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
67 73
    
68 74
    def __init__(self, *args, **kwargs):
69 75
        """
......
75 81
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
76 82
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
77 83
                                'password1', 'password2',
84
                                'has_signed_terms',
78 85
                                'recaptcha_challenge_field',
79
                                'recaptcha_response_field']
86
                                'recaptcha_response_field',]
80 87
    
81 88
    def clean_email(self):
82 89
        email = self.cleaned_data['email']
......
88 95
        except AstakosUser.DoesNotExist:
89 96
            return email
90 97
    
98
    def clean_has_signed_terms(self):
99
        has_signed_terms = self.cleaned_data['has_signed_terms']
100
        if not has_signed_terms:
101
            raise forms.ValidationError(_('You have to agree with the terms'))
102
        return has_signed_terms
103
    
91 104
    def clean_recaptcha_response_field(self):
92 105
        if 'recaptcha_challenge_field' in self.cleaned_data:
93 106
            self.validate_captcha()
......
112 125
        """
113 126
        user = super(LocalUserCreationForm, self).save(commit=False)
114 127
        user.renew_token()
128
        user.date_signed_terms = datetime.now()
115 129
        if commit:
116 130
            user.save()
117 131
        logger.info('Created user %s', user)
......
126 140
    
127 141
    class Meta:
128 142
        model = AstakosUser
129
        fields = ("email", "first_name", "last_name")
143
        fields = ("email", "first_name", "last_name", "has_signed_terms")
144
        widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
130 145
    
131 146
    def __init__(self, *args, **kwargs):
132 147
        """
......
135 150
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
136 151
        self.fields.keyOrder = ['email', 'inviter', 'first_name',
137 152
                                'last_name', 'password1', 'password2',
153
                                'has_signed_terms',
138 154
                                'recaptcha_challenge_field',
139 155
                                'recaptcha_response_field']
140 156
        #set readonly form fields
......
270 286
            from_email = DEFAULT_FROM_EMAIL
271 287
            send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
272 288
                t.render(Context(c)), from_email, [user.email])
289

  
290
class SignApprovalTermsForm(forms.ModelForm):
291
    class Meta:
292
        model = AstakosUser
293
        fields = ("has_signed_terms",)
294
    
295
    def __init__(self, *args, **kwargs):
296
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
297
    
298
    def clean_has_signed_terms(self):
299
        has_signed_terms = self.cleaned_data['has_signed_terms']
300
        if not has_signed_terms:
301
            raise forms.ValidationError(_('You have to agree with the terms'))
302
        return has_signed_terms
303
    
304
    def save(self, commit=True):
305
        """
306
        Saves the , after the normal
307
        save behavior is complete.
308
        """
309
        user = super(SignApprovalTermsForm, self).save(commit=False)
310
        user.date_signed_terms = datetime.now()
311
        if commit:
312
            user.save()
313
        return user
b/snf-astakos-app/astakos/im/management/commands/addterms.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from optparse import make_option
35
from random import choice
36
from string import digits, lowercase, uppercase
37
from uuid import uuid4
38
from time import time
39

  
40
from django.core.management.base import BaseCommand, CommandError
41

  
42
from astakos.im.models import ApprovalTerms
43

  
44
class Command(BaseCommand):
45
    args = "<location>"
46
    help = "Insert approval terms"
47
    
48
    def handle(self, *args, **options):
49
        if len(args) != 1:
50
            raise CommandError("Invalid number of arguments")
51
        
52
        location = args[0].decode('utf8')
53
        try:
54
            f = open(location, 'r')
55
        except IOError:
56
            raise CommandError("Invalid location")
57
        
58
        terms = ApprovalTerms(location=location)
59
        terms.save()
60
        
61
        msg = "Created term id %d" % (terms.id,)
62
        self.stdout.write(msg + '\n')
b/snf-astakos-app/astakos/im/management/commands/showuser.py
73 73
            'invitation level': user.level,
74 74
            'provider': user.provider,
75 75
            'verified': format_bool(user.is_verified),
76
            'has_credits': format_bool(user.has_credits)
76
            'has_credits': format_bool(user.has_credits),
77
            'has_signed_terms': format_bool(user.has_signed_terms),
78
            'date_signed_terms': format_date(user.date_signed_terms)
77 79
        }
78 80
        
79 81
        for key, val in sorted(kv.items()):
80
            line = '%s: %s\n' % (key.rjust(16), val)
82
            line = '%s: %s\n' % (key.rjust(17), val)
81 83
            self.stdout.write(line.encode('utf8'))
b/snf-astakos-app/astakos/im/migrations/0006_auto__add_approvalterms__add_field_astakosuser_has_signed_terms__add_f.py
1
# encoding: utf-8
2
import datetime
3
from south.db import db
4
from south.v2 import SchemaMigration
5
from django.db import models
6

  
7
class Migration(SchemaMigration):
8

  
9
    def forwards(self, orm):
10
        
11
        # Adding model 'ApprovalTerms'
12
        db.create_table('im_approvalterms', (
13
            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14
            ('date', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2012, 3, 20, 14, 24, 30, 616341), db_index=True)),
15
            ('location', self.gf('django.db.models.fields.CharField')(max_length=255)),
16
        ))
17
        db.send_create_signal('im', ['ApprovalTerms'])
18

  
19
        # Adding field 'AstakosUser.has_signed_terms'
20
        db.add_column('im_astakosuser', 'has_signed_terms', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
21

  
22
        # Adding field 'AstakosUser.date_signed_terms'
23
        db.add_column('im_astakosuser', 'date_signed_terms', self.gf('django.db.models.fields.DateTimeField')(null=True), keep_default=False)
24

  
25

  
26
    def backwards(self, orm):
27
        
28
        # Deleting model 'ApprovalTerms'
29
        db.delete_table('im_approvalterms')
30

  
31
        # Deleting field 'AstakosUser.has_signed_terms'
32
        db.delete_column('im_astakosuser', 'has_signed_terms')
33

  
34
        # Deleting field 'AstakosUser.date_signed_terms'
35
        db.delete_column('im_astakosuser', 'date_signed_terms')
36

  
37

  
38
    models = {
39
        'auth.group': {
40
            'Meta': {'object_name': 'Group'},
41
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
42
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
43
            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
44
        },
45
        'auth.permission': {
46
            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
47
            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
48
            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
49
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
50
            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
51
        },
52
        'auth.user': {
53
            'Meta': {'object_name': 'User'},
54
            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
55
            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
56
            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
57
            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
58
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
59
            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
60
            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
61
            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
62
            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
63
            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
64
            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
65
            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
66
            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
67
        },
68
        'contenttypes.contenttype': {
69
            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
70
            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
71
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
72
            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
73
            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
74
        },
75
        'im.approvalterms': {
76
            'Meta': {'object_name': 'ApprovalTerms'},
77
            'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 3, 20, 14, 24, 30, 616341)', 'db_index': 'True'}),
78
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
79
            'location': ('django.db.models.fields.CharField', [], {'max_length': '255'})
80
        },
81
        'im.astakosuser': {
82
            'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']},
83
            'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
84
            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
85
            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
86
            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
87
            'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
88
            'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
89
            'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
90
            'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
91
            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
92
            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
93
            'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}),
94
            'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
95
            'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
96
            'updated': ('django.db.models.fields.DateTimeField', [], {}),
97
            'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'})
98
        },
99
        'im.invitation': {
100
            'Meta': {'object_name': 'Invitation'},
101
            'accepted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
102
            'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
103
            'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
104
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
105
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
106
            'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}),
107
            'is_accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
108
            'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
109
            'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
110
            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
111
        }
112
    }
113

  
114
    complete_apps = ['im']
b/snf-astakos-app/astakos/im/models.py
76 76
    email_verified = models.BooleanField('Email verified?', default=False)
77 77
    
78 78
    has_credits = models.BooleanField('Has credits?', default=False)
79
    has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
80
    date_signed_terms = models.DateTimeField('Signed terms date', null=True)
79 81
    
80 82
    @property
81 83
    def realname(self):
......
130 132
    def __unicode__(self):
131 133
        return self.username
132 134

  
135
class ApprovalTerms(models.Model):
136
    """
137
    Model for approval terms
138
    """
139
    
140
    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
141
    location = models.CharField('Terms location', max_length=255)
142

  
133 143
class Invitation(models.Model):
134 144
    """
135 145
    Model for registring invitations
......
171 181
        eventType = 'create' if not user.id else 'modify'
172 182
        body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
173 183
        conn = exchange_connect(QUEUE_CONNECTION)
174
        routing_key = '%s.user' % QUEUE_CONNECTION
184
        parts = urlparse(exchange)
185
        exchange = parts.path[1:]
186
        routing_key = '%s.user' % exchange
175 187
        exchange_send(conn, routing_key, body)
176 188
        exchange_close(conn)
b/snf-astakos-app/astakos/im/templates/im/approval_terms.html
1
{% block body %}
2
{{ terms }}
3

  
4
{% if form %}
5
<div class="section">
6
    <form action="{% url latest_terms %}" method="post" class="login innerlabels">{% csrf_token %}
7
                {% include "im/form_render.html" %}
8
                <input type="hidden" name="next" value="{{ next }}">
9
                <div class="form-row submit">
10
                    <input type="submit" class="submit altcol" value="SUBMIT" />
11
                </div>
12
        </form>
13
{% endif %}
14
{% endblock body %}
b/snf-astakos-app/astakos/im/urls.py
32 32
# or implied, of GRNET S.A.
33 33

  
34 34
from django.conf.urls.defaults import patterns, include, url
35
from django.contrib.auth.views import password_change
35 36

  
36 37
from astakos.im.forms import ExtendedPasswordResetForm, LoginForm
37 38
from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED
39
from astakos.im.views import signed_terms_required
38 40

  
39 41
urlpatterns = patterns('astakos.im.views',
40 42
    url(r'^$', 'index', {}, name='index'),
......
43 45
    url(r'^feedback/?$', 'send_feedback'),
44 46
    url(r'^signup/?$', 'signup', {'on_success':'im/login.html', 'extra_context':{'form':LoginForm()}}),
45 47
    url(r'^logout/?$', 'logout', {'template':'im/login.html', 'extra_context':{'form':LoginForm()}}),
46
    url(r'^activate/?$', 'activate')
48
    url(r'^activate/?$', 'activate'),
49
    url(r'^approval_terms/?$', 'approval_terms', {}, name='latest_terms'),
50
    url(r'^approval_terms/(?P<term_id>\d+)?$', 'approval_terms'),
51
    url(r'^password/?$', 'change_password', {}, name='password_change')
47 52
)
48 53

  
49 54
urlpatterns += patterns('astakos.im.target',
......
62 67
        url(r'^local/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
63 68
         'password_reset_confirm'),
64 69
        url(r'^local/password/reset/complete/$', 'password_reset_complete'),
65
        url(r'^password/?$', 'password_change', {'post_change_redirect':'profile'}, name='password_change')
70
        url(r'^password_change/?$', 'password_change', {'post_change_redirect':'profile'})
66 71
    )
67 72

  
68 73
if INVITATIONS_ENABLED:
b/snf-astakos-app/astakos/im/util.py
46 46
from django.contrib.auth import login, authenticate
47 47
from django.core.urlresolvers import reverse
48 48

  
49
from astakos.im.models import AstakosUser, Invitation
49
from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
50 50
from astakos.im.settings import INVITATIONS_PER_LEVEL, COOKIE_NAME, COOKIE_DOMAIN, COOKIE_SECURE, FORCE_PROFILE_UPDATE
51 51

  
52 52
logger = logging.getLogger(__name__)
......
128 128
       expired, if the 'renew' parameter is present
129 129
       or user has not a valid token.
130 130
    """
131
    
132 131
    renew = renew or (not user.auth_token)
133 132
    renew = renew or (user.auth_token_expires and user.auth_token_expires < datetime.datetime.now())
134 133
    if renew:
......
161 160
    response.set_cookie(COOKIE_NAME, value=cookie_value,
162 161
                        expires=expire_fmt, path='/',
163 162
                        domain=COOKIE_DOMAIN, secure=COOKIE_SECURE)
163

  
164
class lazy_string(object):
165
    def __init__(self, function, *args, **kwargs):
166
        self.function=function
167
        self.args=args
168
        self.kwargs=kwargs
169
        
170
    def __str__(self):
171
        if not hasattr(self, 'str'):
172
            self.str=self.function(*self.args, **self.kwargs)
173
        return self.str
174

  
175
def reverse_lazy(*args, **kwargs):
176
    return lazy_string(reverse, *args, **kwargs)
177

  
178
def has_signed_terms(user):
179
    if not user.has_signed_terms:
180
        return False
181
    try:
182
        term = ApprovalTerms.objects.order_by('-id')[0]
183
        if user.date_signed_terms < term.date:
184
            return False
185
    except IndexError:
186
        pass
187
    return True
b/snf-astakos-app/astakos/im/views.py
51 51
from django.utils.http import urlencode
52 52
from django.http import HttpResponseRedirect, HttpResponseBadRequest
53 53
from django.db.utils import IntegrityError
54
from django.contrib.auth.views import password_change
54 55

  
55
from astakos.im.models import AstakosUser, Invitation
56
from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
56 57
from astakos.im.backends import get_backend
57
from astakos.im.util import get_context, prepare_response, set_cookie
58
from astakos.im.util import get_context, prepare_response, set_cookie, has_signed_terms
58 59
from astakos.im.forms import *
59 60
from astakos.im.functions import send_greeting
60 61
from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
......
80 81

  
81 82
def requires_anonymous(func):
82 83
    """
83
    Decorator checkes whether the request.user is Anonymous and in that case
84
    Decorator checkes whether the request.user is not Anonymous and in that case
84 85
    redirects to `logout`.
85 86
    """
86 87
    @wraps(func)
87 88
    def wrapper(request, *args):
88 89
        if not request.user.is_anonymous():
89 90
            next = urlencode({'next': request.build_absolute_uri()})
90
            login_uri = reverse(logout) + '?' + next
91
            return HttpResponseRedirect(login_uri)
91
            logout_uri = reverse(logout) + '?' + next
92
            return HttpResponseRedirect(logout_uri)
92 93
        return func(request, *args)
93 94
    return wrapper
94 95

  
96
def signed_terms_required(func):
97
    """
98
    Decorator checkes whether the request.user is Anonymous and in that case
99
    redirects to `logout`.
100
    """
101
    @wraps(func)
102
    def wrapper(request, *args, **kwargs):
103
        if request.user.is_authenticated() and not has_signed_terms(request.user):
104
            params = urlencode({'next': request.build_absolute_uri(),
105
                              'show_form':''})
106
            terms_uri = reverse('latest_terms') + '?' + params
107
            return HttpResponseRedirect(terms_uri)
108
        return func(request, *args, **kwargs)
109
    return wrapper
110

  
111
@signed_terms_required
95 112
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
96 113
    """
97 114
    If there is logged on user renders the profile page otherwise renders login page.
......
124 141
                           context_instance = get_context(request, extra_context))
125 142

  
126 143
@login_required
144
@signed_terms_required
127 145
@transaction.commit_manually
128 146
def invite(request, template_name='im/invitations.html', extra_context={}):
129 147
    """
......
197 215
                           context_instance = context)
198 216

  
199 217
@login_required
218
@signed_terms_required
200 219
def edit_profile(request, template_name='im/profile.html', extra_context={}):
201 220
    """
202 221
    Allows a user to edit his/her profile.
......
321 340
                           context_instance=get_context(request, extra_context))
322 341

  
323 342
@login_required
343
@signed_terms_required
324 344
def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
325 345
    """
326 346
    Allows a user to send feedback.
......
426 446
        messages.add_message(request, messages.ERROR, message)
427 447
        transaction.rollback()
428 448
        return signup(request, on_failure='im/signup.html')
449

  
450
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
451
    term = None
452
    terms = None
453
    if not term_id:
454
        try:
455
            term = ApprovalTerms.objects.order_by('-id')[0]
456
        except IndexError:
457
            pass
458
    else:
459
        try:
460
             term = ApprovalTerms.objects.get(id=term_id)
461
        except ApprovalTermDoesNotExist, e:
462
            pass
463
    
464
    if not term:
465
        return HttpResponseBadRequest(_('No approval terms found.'))
466
    f = open(term.location, 'r')
467
    terms = f.read()
468
    
469
    if request.method == 'POST':
470
        next = request.POST.get('next')
471
        if not next:
472
            return HttpResponseBadRequest(_('No next param.'))
473
        form = SignApprovalTermsForm(request.POST, instance=request.user)
474
        if not form.is_valid():
475
            return render_response(template_name,
476
                           terms = terms,
477
                           form = form,
478
                           context_instance = get_context(request, extra_context))
479
        user = form.save()
480
        return HttpResponseRedirect(next)
481
    else:
482
        form = SignApprovalTermsForm(instance=request.user) if request.user.is_authenticated() else None
483
        return render_response(template_name,
484
                               terms = terms,
485
                               form = form,
486
                               context_instance = get_context(request, extra_context))
487

  
488
@signed_terms_required
489
def change_password(request):
490
    return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))
b/snf-astakos-app/astakos/im/widgets.py
36 36
from django import forms
37 37
from django.utils.safestring import mark_safe
38 38
from django.utils import simplejson as json
39
from django.utils.translation import ugettext as _
39 40

  
40 41
from astakos.im.settings import RECAPTCHA_PUBLIC_KEY, RECAPTCHA_OPTIONS, \
41 42
        RECAPTCHA_USE_SSL
......
61 62
    is_hidden=True
62 63
    def render(self, *args, **kwargs):
63 64
        return ''
65

  
66
class ApprovalTermsWidget(forms.CheckboxInput):
67
    """
68
    A CheckboxInput class with a link to the approval terms.
69
    """
70
    def __init__(self, attrs=None, check_test=bool, terms_uri='', terms_label=_('Read the terms')):
71
        super(ApprovalTermsWidget, self).__init__(attrs, check_test)
72
        self.uri = terms_uri
73
        self.label = terms_label
74
    
75
    def render(self, name, value, attrs=None):
76
        html = super(ApprovalTermsWidget, self).render(name, value, attrs)
77
        return html + mark_safe('<a href=%s target="_blank">%s</a>' % (self.uri, self.label))

Also available in: Unified diff