commit 270dd48d753d7f76f38390cd15bc5fbf4c28d976
Author: Sofia Papagiannaki <papagian@gmail.com>
Date:   Wed Mar 21 15:35:36 2012 +0200

    Force user to accept service terms
    
    Refs: #2019

diff --git a/docs/source/devguide.rst b/docs/source/devguide.rst
index bfd31d7..98a7098 100644
--- a/docs/source/devguide.rst
+++ b/docs/source/devguide.rst
@@ -158,6 +158,8 @@ uniq                         User email (uniq identifier used by Astakos)
 auth_token                   Authentication token
 auth_token_expires           Token expiration date
 auth_token_created           Token creation date
+has_credits                  Whether user has credits
+has_signed_terms             Whether user has aggred on terms
 ===========================  ============================
 
 Example reply:
@@ -168,7 +170,9 @@ Example reply:
   "uniq": "papagian@example.com"
   "auth_token": "0000",
   "auth_token_expires": "Tue, 11-Sep-2012 09:17:14 ",
-  "auth_token_created": "Sun, 11-Sep-2011 09:17:14 "}
+  "auth_token_created": "Sun, 11-Sep-2011 09:17:14 ",
+  "has_credits": false,
+  "has_signed_terms": true}
 
 |
 
@@ -177,7 +181,7 @@ Return Code                 Description
 =========================== =====================
 204 (No Content)            The request succeeded
 400 (Bad Request)           The request is invalid
-401 (Unauthorized)          Missing token or inactive user
+401 (Unauthorized)          Missing token or inactive user or penging approval terms
 500 (Internal Server Error) The request cannot be completed because of an internal error
 =========================== =====================
 
diff --git a/snf-astakos-app/README b/snf-astakos-app/README
index a529f10..3b1faed 100644
--- a/snf-astakos-app/README
+++ b/snf-astakos-app/README
@@ -92,4 +92,5 @@ listusers        List users
 modifyuser       Modify a user's attributes
 showinvitation   Show invitation info
 showuser         Show user info
+addterms         Add new approval terms
 ===============  ===========================
diff --git a/snf-astakos-app/astakos/im/api.py b/snf-astakos-app/astakos/im/api.py
index 3d4557e..2e20c49 100644
--- a/snf-astakos-app/astakos/im/api.py
+++ b/snf-astakos-app/astakos/im/api.py
@@ -46,6 +46,7 @@ from django.core.urlresolvers import reverse
 from astakos.im.faults import BadRequest, Unauthorized, InternalServerError
 from astakos.im.models import AstakosUser
 from astakos.im.settings import CLOUD_SERVICES, INVITATIONS_ENABLED
+from astakos.im.util import has_signed_terms
 
 logger = logging.getLogger(__name__)
 
@@ -85,7 +86,10 @@ def authenticate(request):
         # Check if the token has expired.
         if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
             return render_fault(request, Unauthorized('Authentication expired'))
-
+        
+        if not has_signed_terms(user):
+            return render_fault(request, Unauthorized('Pending approval terms'))
+        
         response = HttpResponse()
         response.status=204
         user_info = {'username':user.username,
@@ -93,7 +97,8 @@ def authenticate(request):
                      'auth_token':user.auth_token,
                      'auth_token_created':user.auth_token_created.isoformat(),
                      'auth_token_expires':user.auth_token_expires.isoformat(),
-                     'has_credits':user.has_credits}
+                     'has_credits':user.has_credits,
+                     'has_signed_terms':has_signed_terms(user)}
         response.content = json.dumps(user_info)
         response['Content-Type'] = 'application/json; charset=UTF-8'
         response['Content-Length'] = len(response.content)
diff --git a/snf-astakos-app/astakos/im/context_processors.py b/snf-astakos-app/astakos/im/context_processors.py
index d8df8d8..d19e105 100644
--- a/snf-astakos-app/astakos/im/context_processors.py
+++ b/snf-astakos-app/astakos/im/context_processors.py
@@ -43,7 +43,8 @@ def im_modules(request):
     return {'im_modules': IM_MODULES}
 
 def next(request):
-    return {'next' : request.GET.get('next', '')}
+    query_dict = request.__getattribute__(request.method)
+    return {'next' : query_dict.get('next', '')}
 
 def code(request):
     return {'code' : request.GET.get('code', '')}
diff --git a/snf-astakos-app/astakos/im/forms.py b/snf-astakos-app/astakos/im/forms.py
index 08bb68b..e065170 100644
--- a/snf-astakos-app/astakos/im/forms.py
+++ b/snf-astakos-app/astakos/im/forms.py
@@ -31,6 +31,7 @@
 # interpreted as representing official policies, either expressed
 # or implied, of GRNET S.A.
 from urlparse import urljoin
+from datetime import datetime
 
 from django import forms
 from django.utils.translation import ugettext as _
@@ -40,10 +41,14 @@ from django.contrib.auth.tokens import default_token_generator
 from django.template import Context, loader
 from django.utils.http import int_to_base36
 from django.core.urlresolvers import reverse
+from django.utils.functional import lazy
 
 from astakos.im.models import AstakosUser
 from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL
-from astakos.im.widgets import DummyWidget, RecaptchaWidget
+from astakos.im.widgets import DummyWidget, RecaptchaWidget, ApprovalTermsWidget
+
+# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
+from astakos.im.util import reverse_lazy
 
 import logging
 import recaptcha.client.captcha as captcha
@@ -63,7 +68,8 @@ class LocalUserCreationForm(UserCreationForm):
     
     class Meta:
         model = AstakosUser
-        fields = ("email", "first_name", "last_name")
+        fields = ("email", "first_name", "last_name", "has_signed_terms")
+        widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
     
     def __init__(self, *args, **kwargs):
         """
@@ -75,8 +81,9 @@ class LocalUserCreationForm(UserCreationForm):
         super(LocalUserCreationForm, self).__init__(*args, **kwargs)
         self.fields.keyOrder = ['email', 'first_name', 'last_name',
                                 'password1', 'password2',
+                                'has_signed_terms',
                                 'recaptcha_challenge_field',
-                                'recaptcha_response_field']
+                                'recaptcha_response_field',]
     
     def clean_email(self):
         email = self.cleaned_data['email']
@@ -88,6 +95,12 @@ class LocalUserCreationForm(UserCreationForm):
         except AstakosUser.DoesNotExist:
             return email
     
+    def clean_has_signed_terms(self):
+        has_signed_terms = self.cleaned_data['has_signed_terms']
+        if not has_signed_terms:
+            raise forms.ValidationError(_('You have to agree with the terms'))
+        return has_signed_terms
+    
     def clean_recaptcha_response_field(self):
         if 'recaptcha_challenge_field' in self.cleaned_data:
             self.validate_captcha()
@@ -112,6 +125,7 @@ class LocalUserCreationForm(UserCreationForm):
         """
         user = super(LocalUserCreationForm, self).save(commit=False)
         user.renew_token()
+        user.date_signed_terms = datetime.now()
         if commit:
             user.save()
         logger.info('Created user %s', user)
@@ -126,7 +140,8 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
     
     class Meta:
         model = AstakosUser
-        fields = ("email", "first_name", "last_name")
+        fields = ("email", "first_name", "last_name", "has_signed_terms")
+        widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
     
     def __init__(self, *args, **kwargs):
         """
@@ -135,6 +150,7 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
         super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
         self.fields.keyOrder = ['email', 'inviter', 'first_name',
                                 'last_name', 'password1', 'password2',
+                                'has_signed_terms',
                                 'recaptcha_challenge_field',
                                 'recaptcha_response_field']
         #set readonly form fields
@@ -270,3 +286,28 @@ class ExtendedPasswordResetForm(PasswordResetForm):
             from_email = DEFAULT_FROM_EMAIL
             send_mail(_("Password reset on %s alpha2 testing") % SITENAME,
                 t.render(Context(c)), from_email, [user.email])
+
+class SignApprovalTermsForm(forms.ModelForm):
+    class Meta:
+        model = AstakosUser
+        fields = ("has_signed_terms",)
+    
+    def __init__(self, *args, **kwargs):
+        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
+    
+    def clean_has_signed_terms(self):
+        has_signed_terms = self.cleaned_data['has_signed_terms']
+        if not has_signed_terms:
+            raise forms.ValidationError(_('You have to agree with the terms'))
+        return has_signed_terms
+    
+    def save(self, commit=True):
+        """
+        Saves the , after the normal
+        save behavior is complete.
+        """
+        user = super(SignApprovalTermsForm, self).save(commit=False)
+        user.date_signed_terms = datetime.now()
+        if commit:
+            user.save()
+        return user
\ No newline at end of file
diff --git a/snf-astakos-app/astakos/im/management/commands/addterms.py b/snf-astakos-app/astakos/im/management/commands/addterms.py
new file mode 100644
index 0000000..783d956
--- /dev/null
+++ b/snf-astakos-app/astakos/im/management/commands/addterms.py
@@ -0,0 +1,62 @@
+# Copyright 2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from optparse import make_option
+from random import choice
+from string import digits, lowercase, uppercase
+from uuid import uuid4
+from time import time
+
+from django.core.management.base import BaseCommand, CommandError
+
+from astakos.im.models import ApprovalTerms
+
+class Command(BaseCommand):
+    args = "<location>"
+    help = "Insert approval terms"
+    
+    def handle(self, *args, **options):
+        if len(args) != 1:
+            raise CommandError("Invalid number of arguments")
+        
+        location = args[0].decode('utf8')
+        try:
+            f = open(location, 'r')
+        except IOError:
+            raise CommandError("Invalid location")
+        
+        terms = ApprovalTerms(location=location)
+        terms.save()
+        
+        msg = "Created term id %d" % (terms.id,)
+        self.stdout.write(msg + '\n')
diff --git a/snf-astakos-app/astakos/im/management/commands/showuser.py b/snf-astakos-app/astakos/im/management/commands/showuser.py
index 151720a..6ad803e 100644
--- a/snf-astakos-app/astakos/im/management/commands/showuser.py
+++ b/snf-astakos-app/astakos/im/management/commands/showuser.py
@@ -73,9 +73,11 @@ class Command(BaseCommand):
             'invitation level': user.level,
             'provider': user.provider,
             'verified': format_bool(user.is_verified),
-            'has_credits': format_bool(user.has_credits)
+            'has_credits': format_bool(user.has_credits),
+            'has_signed_terms': format_bool(user.has_signed_terms),
+            'date_signed_terms': format_date(user.date_signed_terms)
         }
         
         for key, val in sorted(kv.items()):
-            line = '%s: %s\n' % (key.rjust(16), val)
+            line = '%s: %s\n' % (key.rjust(17), val)
             self.stdout.write(line.encode('utf8'))
diff --git a/snf-astakos-app/astakos/im/migrations/0006_auto__add_approvalterms__add_field_astakosuser_has_signed_terms__add_f.py b/snf-astakos-app/astakos/im/migrations/0006_auto__add_approvalterms__add_field_astakosuser_has_signed_terms__add_f.py
new file mode 100644
index 0000000..f10e4a2
--- /dev/null
+++ b/snf-astakos-app/astakos/im/migrations/0006_auto__add_approvalterms__add_field_astakosuser_has_signed_terms__add_f.py
@@ -0,0 +1,114 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding model 'ApprovalTerms'
+        db.create_table('im_approvalterms', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('date', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2012, 3, 20, 14, 24, 30, 616341), db_index=True)),
+            ('location', self.gf('django.db.models.fields.CharField')(max_length=255)),
+        ))
+        db.send_create_signal('im', ['ApprovalTerms'])
+
+        # Adding field 'AstakosUser.has_signed_terms'
+        db.add_column('im_astakosuser', 'has_signed_terms', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+        # Adding field 'AstakosUser.date_signed_terms'
+        db.add_column('im_astakosuser', 'date_signed_terms', self.gf('django.db.models.fields.DateTimeField')(null=True), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'ApprovalTerms'
+        db.delete_table('im_approvalterms')
+
+        # Deleting field 'AstakosUser.has_signed_terms'
+        db.delete_column('im_astakosuser', 'has_signed_terms')
+
+        # Deleting field 'AstakosUser.date_signed_terms'
+        db.delete_column('im_astakosuser', 'date_signed_terms')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'im.approvalterms': {
+            'Meta': {'object_name': 'ApprovalTerms'},
+            'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 3, 20, 14, 24, 30, 616341)', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'location': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'im.astakosuser': {
+            'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']},
+            'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
+            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'default': '4'}),
+            'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'im.invitation': {
+            'Meta': {'object_name': 'Invitation'},
+            'accepted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+            'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}),
+            'is_accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+        }
+    }
+
+    complete_apps = ['im']
diff --git a/snf-astakos-app/astakos/im/models.py b/snf-astakos-app/astakos/im/models.py
index 1c5cc3c..89895ac 100644
--- a/snf-astakos-app/astakos/im/models.py
+++ b/snf-astakos-app/astakos/im/models.py
@@ -76,6 +76,8 @@ class AstakosUser(User):
     email_verified = models.BooleanField('Email verified?', default=False)
     
     has_credits = models.BooleanField('Has credits?', default=False)
+    has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
+    date_signed_terms = models.DateTimeField('Signed terms date', null=True)
     
     @property
     def realname(self):
@@ -130,6 +132,14 @@ class AstakosUser(User):
     def __unicode__(self):
         return self.username
 
+class ApprovalTerms(models.Model):
+    """
+    Model for approval terms
+    """
+    
+    date = models.DateTimeField('Issue date', db_index=True, default=datetime.now())
+    location = models.CharField('Terms location', max_length=255)
+
 class Invitation(models.Model):
     """
     Model for registring invitations
@@ -171,6 +181,8 @@ def report_user_event(user):
         eventType = 'create' if not user.id else 'modify'
         body = UserEvent(QUEUE_CLIENT_ID, user, eventType, {}).format()
         conn = exchange_connect(QUEUE_CONNECTION)
-        routing_key = '%s.user' % QUEUE_CONNECTION
+        parts = urlparse(exchange)
+        exchange = parts.path[1:]
+        routing_key = '%s.user' % exchange
         exchange_send(conn, routing_key, body)
         exchange_close(conn)
diff --git a/snf-astakos-app/astakos/im/templates/im/approval_terms.html b/snf-astakos-app/astakos/im/templates/im/approval_terms.html
new file mode 100644
index 0000000..eb4204f
--- /dev/null
+++ b/snf-astakos-app/astakos/im/templates/im/approval_terms.html
@@ -0,0 +1,14 @@
+{% block body %}
+{{ terms }}
+
+{% if form %}
+<div class="section">
+    <form action="{% url latest_terms %}" method="post" class="login innerlabels">{% csrf_token %}
+                {% include "im/form_render.html" %}
+                <input type="hidden" name="next" value="{{ next }}">
+                <div class="form-row submit">
+                    <input type="submit" class="submit altcol" value="SUBMIT" />
+                </div>
+        </form>
+{% endif %}
+{% endblock body %}
\ No newline at end of file
diff --git a/snf-astakos-app/astakos/im/urls.py b/snf-astakos-app/astakos/im/urls.py
index a152b88..cb89578 100644
--- a/snf-astakos-app/astakos/im/urls.py
+++ b/snf-astakos-app/astakos/im/urls.py
@@ -32,9 +32,11 @@
 # or implied, of GRNET S.A.
 
 from django.conf.urls.defaults import patterns, include, url
+from django.contrib.auth.views import password_change
 
 from astakos.im.forms import ExtendedPasswordResetForm, LoginForm
 from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED
+from astakos.im.views import signed_terms_required
 
 urlpatterns = patterns('astakos.im.views',
     url(r'^$', 'index', {}, name='index'),
@@ -43,7 +45,10 @@ urlpatterns = patterns('astakos.im.views',
     url(r'^feedback/?$', 'send_feedback'),
     url(r'^signup/?$', 'signup', {'on_success':'im/login.html', 'extra_context':{'form':LoginForm()}}),
     url(r'^logout/?$', 'logout', {'template':'im/login.html', 'extra_context':{'form':LoginForm()}}),
-    url(r'^activate/?$', 'activate')
+    url(r'^activate/?$', 'activate'),
+    url(r'^approval_terms/?$', 'approval_terms', {}, name='latest_terms'),
+    url(r'^approval_terms/(?P<term_id>\d+)?$', 'approval_terms'),
+    url(r'^password/?$', 'change_password', {}, name='password_change')
 )
 
 urlpatterns += patterns('astakos.im.target',
@@ -62,7 +67,7 @@ if 'local' in IM_MODULES:
         url(r'^local/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
          'password_reset_confirm'),
         url(r'^local/password/reset/complete/$', 'password_reset_complete'),
-        url(r'^password/?$', 'password_change', {'post_change_redirect':'profile'}, name='password_change')
+        url(r'^password_change/?$', 'password_change', {'post_change_redirect':'profile'})
     )
 
 if INVITATIONS_ENABLED:
diff --git a/snf-astakos-app/astakos/im/util.py b/snf-astakos-app/astakos/im/util.py
index c82eebb..901b549 100644
--- a/snf-astakos-app/astakos/im/util.py
+++ b/snf-astakos-app/astakos/im/util.py
@@ -46,7 +46,7 @@ from django.utils.translation import ugettext as _
 from django.contrib.auth import login, authenticate
 from django.core.urlresolvers import reverse
 
-from astakos.im.models import AstakosUser, Invitation
+from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
 from astakos.im.settings import INVITATIONS_PER_LEVEL, COOKIE_NAME, COOKIE_DOMAIN, COOKIE_SECURE, FORCE_PROFILE_UPDATE
 
 logger = logging.getLogger(__name__)
@@ -128,7 +128,6 @@ def prepare_response(request, user, next='', renew=False):
        expired, if the 'renew' parameter is present
        or user has not a valid token.
     """
-    
     renew = renew or (not user.auth_token)
     renew = renew or (user.auth_token_expires and user.auth_token_expires < datetime.datetime.now())
     if renew:
@@ -161,3 +160,28 @@ def set_cookie(response, user):
     response.set_cookie(COOKIE_NAME, value=cookie_value,
                         expires=expire_fmt, path='/',
                         domain=COOKIE_DOMAIN, secure=COOKIE_SECURE)
+
+class lazy_string(object):
+    def __init__(self, function, *args, **kwargs):
+        self.function=function
+        self.args=args
+        self.kwargs=kwargs
+        
+    def __str__(self):
+        if not hasattr(self, 'str'):
+            self.str=self.function(*self.args, **self.kwargs)
+        return self.str
+
+def reverse_lazy(*args, **kwargs):
+    return lazy_string(reverse, *args, **kwargs)
+
+def has_signed_terms(user):
+    if not user.has_signed_terms:
+        return False
+    try:
+        term = ApprovalTerms.objects.order_by('-id')[0]
+        if user.date_signed_terms < term.date:
+            return False
+    except IndexError:
+        pass
+    return True
\ No newline at end of file
diff --git a/snf-astakos-app/astakos/im/views.py b/snf-astakos-app/astakos/im/views.py
index 30c0cc6..f215d6b 100644
--- a/snf-astakos-app/astakos/im/views.py
+++ b/snf-astakos-app/astakos/im/views.py
@@ -51,10 +51,11 @@ from django.contrib.auth import logout as auth_logout
 from django.utils.http import urlencode
 from django.http import HttpResponseRedirect, HttpResponseBadRequest
 from django.db.utils import IntegrityError
+from django.contrib.auth.views import password_change
 
-from astakos.im.models import AstakosUser, Invitation
+from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
 from astakos.im.backends import get_backend
-from astakos.im.util import get_context, prepare_response, set_cookie
+from astakos.im.util import get_context, prepare_response, set_cookie, has_signed_terms
 from astakos.im.forms import *
 from astakos.im.functions import send_greeting
 from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, BASEURL, LOGOUT_NEXT
@@ -80,18 +81,34 @@ def render_response(template, tab=None, status=200, reset_cookie=False, context_
 
 def requires_anonymous(func):
     """
-    Decorator checkes whether the request.user is Anonymous and in that case
+    Decorator checkes whether the request.user is not Anonymous and in that case
     redirects to `logout`.
     """
     @wraps(func)
     def wrapper(request, *args):
         if not request.user.is_anonymous():
             next = urlencode({'next': request.build_absolute_uri()})
-            login_uri = reverse(logout) + '?' + next
-            return HttpResponseRedirect(login_uri)
+            logout_uri = reverse(logout) + '?' + next
+            return HttpResponseRedirect(logout_uri)
         return func(request, *args)
     return wrapper
 
+def signed_terms_required(func):
+    """
+    Decorator checkes whether the request.user is Anonymous and in that case
+    redirects to `logout`.
+    """
+    @wraps(func)
+    def wrapper(request, *args, **kwargs):
+        if request.user.is_authenticated() and not has_signed_terms(request.user):
+            params = urlencode({'next': request.build_absolute_uri(),
+                              'show_form':''})
+            terms_uri = reverse('latest_terms') + '?' + params
+            return HttpResponseRedirect(terms_uri)
+        return func(request, *args, **kwargs)
+    return wrapper
+
+@signed_terms_required
 def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
     """
     If there is logged on user renders the profile page otherwise renders login page.
@@ -124,6 +141,7 @@ def index(request, login_template_name='im/login.html', profile_template_name='i
                            context_instance = get_context(request, extra_context))
 
 @login_required
+@signed_terms_required
 @transaction.commit_manually
 def invite(request, template_name='im/invitations.html', extra_context={}):
     """
@@ -197,6 +215,7 @@ def invite(request, template_name='im/invitations.html', extra_context={}):
                            context_instance = context)
 
 @login_required
+@signed_terms_required
 def edit_profile(request, template_name='im/profile.html', extra_context={}):
     """
     Allows a user to edit his/her profile.
@@ -321,6 +340,7 @@ def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.
                            context_instance=get_context(request, extra_context))
 
 @login_required
+@signed_terms_required
 def send_feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
     """
     Allows a user to send feedback.
@@ -426,3 +446,45 @@ def activate(request, email_template_name='im/welcome_email.txt', on_failure='')
         messages.add_message(request, messages.ERROR, message)
         transaction.rollback()
         return signup(request, on_failure='im/signup.html')
+
+def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
+    term = None
+    terms = None
+    if not term_id:
+        try:
+            term = ApprovalTerms.objects.order_by('-id')[0]
+        except IndexError:
+            pass
+    else:
+        try:
+             term = ApprovalTerms.objects.get(id=term_id)
+        except ApprovalTermDoesNotExist, e:
+            pass
+    
+    if not term:
+        return HttpResponseBadRequest(_('No approval terms found.'))
+    f = open(term.location, 'r')
+    terms = f.read()
+    
+    if request.method == 'POST':
+        next = request.POST.get('next')
+        if not next:
+            return HttpResponseBadRequest(_('No next param.'))
+        form = SignApprovalTermsForm(request.POST, instance=request.user)
+        if not form.is_valid():
+            return render_response(template_name,
+                           terms = terms,
+                           form = form,
+                           context_instance = get_context(request, extra_context))
+        user = form.save()
+        return HttpResponseRedirect(next)
+    else:
+        form = SignApprovalTermsForm(instance=request.user) if request.user.is_authenticated() else None
+        return render_response(template_name,
+                               terms = terms,
+                               form = form,
+                               context_instance = get_context(request, extra_context))
+
+@signed_terms_required
+def change_password(request):
+    return password_change(request, post_change_redirect=reverse('astakos.im.views.edit_profile'))
\ No newline at end of file
diff --git a/snf-astakos-app/astakos/im/widgets.py b/snf-astakos-app/astakos/im/widgets.py
index 028525f..9996681 100644
--- a/snf-astakos-app/astakos/im/widgets.py
+++ b/snf-astakos-app/astakos/im/widgets.py
@@ -36,6 +36,7 @@ import recaptcha.client.captcha as captcha
 from django import forms
 from django.utils.safestring import mark_safe
 from django.utils import simplejson as json
+from django.utils.translation import ugettext as _
 
 from astakos.im.settings import RECAPTCHA_PUBLIC_KEY, RECAPTCHA_OPTIONS, \
         RECAPTCHA_USE_SSL
@@ -61,3 +62,16 @@ class DummyWidget(forms.Widget):
     is_hidden=True
     def render(self, *args, **kwargs):
         return ''
+
+class ApprovalTermsWidget(forms.CheckboxInput):
+    """
+    A CheckboxInput class with a link to the approval terms.
+    """
+    def __init__(self, attrs=None, check_test=bool, terms_uri='', terms_label=_('Read the terms')):
+        super(ApprovalTermsWidget, self).__init__(attrs, check_test)
+        self.uri = terms_uri
+        self.label = terms_label
+    
+    def render(self, name, value, attrs=None):
+        html = super(ApprovalTermsWidget, self).render(name, value, attrs)
+        return html + mark_safe('<a href=%s target="_blank">%s</a>' % (self.uri, self.label))
\ No newline at end of file
