merge with master
authorSofia Papagiannaki <papagian@gmail.com>
Fri, 9 Mar 2012 13:05:02 +0000 (15:05 +0200)
committerSofia Papagiannaki <papagian@gmail.com>
Fri, 9 Mar 2012 13:05:02 +0000 (15:05 +0200)
38 files changed:
docs/source/devguide.rst
snf-astakos-app/Changelog
snf-astakos-app/MANIFEST.in
snf-astakos-app/README
snf-astakos-app/astakos/im/api.py
snf-astakos-app/astakos/im/backends.py
snf-astakos-app/astakos/im/context_processors.py
snf-astakos-app/astakos/im/forms.py
snf-astakos-app/astakos/im/management/commands/activateuser.py
snf-astakos-app/astakos/im/management/commands/inviteuser.py
snf-astakos-app/astakos/im/management/commands/modifyuser.py
snf-astakos-app/astakos/im/management/commands/showinvitation.py
snf-astakos-app/astakos/im/management/commands/showuser.py
snf-astakos-app/astakos/im/migrations/0003_auto__add_unique_invitation_username.py [new file with mode: 0644]
snf-astakos-app/astakos/im/migrations/0004_auto__add_field_astakosuser_email_verified.py [new file with mode: 0644]
snf-astakos-app/astakos/im/models.py
snf-astakos-app/astakos/im/settings.py
snf-astakos-app/astakos/im/static/im/cloudbar/README.rst
snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.css
snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.js
snf-astakos-app/astakos/im/static/im/cloudbar/cloudbar.less
snf-astakos-app/astakos/im/static/im/css/boxsizing.htc [new file with mode: 0644]
snf-astakos-app/astakos/im/static/im/css/styles.css
snf-astakos-app/astakos/im/static/im/css/styles.less
snf-astakos-app/astakos/im/static/im/images/dashed_border.png [new file with mode: 0644]
snf-astakos-app/astakos/im/static/im/less/xtra.less
snf-astakos-app/astakos/im/synnefo_settings.py
snf-astakos-app/astakos/im/target/redirect.py
snf-astakos-app/astakos/im/templates/im/account_base.html
snf-astakos-app/astakos/im/templates/im/base.html
snf-astakos-app/astakos/im/templates/im/login.html
snf-astakos-app/astakos/im/templatetags/filters.py [new file with mode: 0644]
snf-astakos-app/astakos/im/urls.py
snf-astakos-app/astakos/im/util.py
snf-astakos-app/astakos/im/views.py
snf-astakos-app/conf/20-snf-astakos-app-cloudbar.conf [new file with mode: 0644]
snf-astakos-app/conf/20-snf-astakos-app-settings.conf
snf-astakos-app/setup.py

index ec99147..bfd31d7 100644 (file)
@@ -117,6 +117,7 @@ Request Parameter Name  Value
 ======================  =========================
 next                    The URI to redirect to when the process is finished
 renew                   Force token renewal (no value parameter)
+force                   Force logout current user (no value parameter)
 ======================  =========================
 
 External systems outside the domain scope can acquire the user information by a cookie set identified by ASTAKOS_COOKIE_NAME setting.
index 2088c8d..ba895ce 100644 (file)
@@ -1,3 +1,21 @@
+=======
+v0.3.3
+======
+
+- Updated grnet styles
+- Several styling fixes
+- Display page menu
+- Minor improvements in cloudbar js and styles
+- Use synnefo.lib.context_processors.cloudbar to display the cloudbar
+  You should set the CLOUDBAR_* settings to point to your astakos urls
+  (see sample conf file in snf-astakos-app/conf/20-snf-astakos-app-cloudbar.conf)
+- Updated snf-common dependency to >=0.9.0
+- New ASTAKOS_RE_USER_EMAIL_PATTERNS setting
+- Support for multiple accounts authentication 
+- New --set-active and --set-inactive in modifyuser command
+- Fixed circular redirects when visiting login page from the logout one
+- Removed im.context_processors.cloudbar (now using snf-common processor)
+
 v0.3.2
 ======
 
index 5596da9..cc48ad3 100644 (file)
@@ -4,6 +4,6 @@ global-include */templates/* */fixtures/* */static/*
 global-exclude */.DS_Store
 include astakos/settings.d/*
 recursive-include astakos/im/templates/ *.html *.txt
-recursive-include astakos/im/static/ *.js *.css *.less *.html *.txt *.png
+recursive-include astakos/im/static/ *.js *.css *.less *.html *.txt *.png *.htc
 prune docs
 prune other
index f639196..e579e22 100644 (file)
@@ -73,6 +73,7 @@ ASTAKOS_LOGOUT_NEXT
 ASTAKOS_BILLING_FIELDS              ['id', 'is_active', 'provider', 'third_party_identifier']                       AstakosUser fields to propagate in the billing system
 ASTAKOS_QUEUE_CONNECTION                                                                                            The queue connection ex. 'rabbitmq://guest:guest@localhost:5672/astakos.userEvent.#'
                                                                                                                     (if it is not set, it does not send messages)
+ASTAKOS_RE_USER_EMAIL_PATTERNS      []                                                                              Email patterns that are automatically activated ex. ['^[a-zA-Z0-9\._-]+@grnet\.gr$']
 ==============================      =============================================================================   ===========================================================================================
 
 Administrator functions
index d321046..893b9ac 100644 (file)
@@ -117,17 +117,23 @@ def get_services(request):
     return HttpResponse(content=data, mimetype=mimetype)
 
 def get_menu(request):
-    if request.method != 'GET':
-        raise BadRequest('Method not allowed.')
     location = request.GET.get('location', '')
+    exclude = []
+    index_url = reverse('index')
+    login_url = reverse('login')
+    logout_url = reverse('astakos.im.views.logout')
     absolute = lambda (url): request.build_absolute_uri(url)
-    index_url = absolute(reverse('astakos.im.views.index'))
-    if urlparse(location).query.rfind('next=') == -1:
+    l = index_url, login_url, logout_url
+    forbidden = []
+    for url in l:
+        url = url.rstrip('/')
+        forbidden.extend([url, url + '/', absolute(url), absolute(url + '/')])
+    if location not in forbidden:
         index_url = '%s?next=%s' % (index_url, quote(location))
-    l = [{ 'url': index_url, 'name': "Sign in"}]
+    l = [{ 'url': absolute(index_url), 'name': "Sign in"}]
     if request.user.is_authenticated():
         l = []
-        l.append({ 'url': absolute(reverse('astakos.im.views.edit_profile')),
+        l.append({ 'url': absolute(reverse('astakos.im.views.index')),
                   'name': request.user.email})
         l.append({ 'url': absolute(reverse('astakos.im.views.edit_profile')),
                   'name': "View your profile" })
index 12e7037..9bcaf54 100644 (file)
@@ -48,10 +48,11 @@ from urlparse import urljoin
 from astakos.im.models import AstakosUser, Invitation
 from astakos.im.forms import *
 from astakos.im.util import get_invitation
-from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL
+from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
 
 import socket
 import logging
+import re
 
 logger = logging.getLogger(__name__)
 
@@ -78,7 +79,15 @@ def get_backend(request):
         raise ImproperlyConfigured('Module "%s" does not define a registration backend named "%s"' % (module, attr))
     return backend_class(request)
 
-class InvitationsBackend(object):
+class SignupBackend(object):
+    def _is_preaccepted(self, user):
+        # return True if user email matches specific patterns
+        for pattern in RE_USER_EMAIL_PATTERNS:
+            if re.match(pattern, user.email):
+                return True
+        return False
+
+class InvitationsBackend(SignupBackend):
     """
     A registration backend which implements the following workflow: a user
     supplies the necessary registation information, if the request contains a valid
@@ -93,6 +102,7 @@ class InvitationsBackend(object):
         """
         self.request = request
         self.invitation = get_invitation(request)
+        super(InvitationsBackend, self).__init__()
 
     def get_signup_form(self, provider):
         """
@@ -136,6 +146,8 @@ class InvitationsBackend(object):
         If there is a valid, not-consumed invitation code for the specific user
         returns True else returns False.
         """
+        if super(InvitationsBackend, self)._is_preaccepted(user):
+            return True
         invitation = self.invitation
         if not invitation:
             return False
@@ -145,7 +157,7 @@ class InvitationsBackend(object):
         return False
 
     @transaction.commit_manually
-    def signup(self, form, admin_email_template_name='im/admin_notification.txt'):
+    def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
         """
         Initially creates an inactive user account. If the user is preaccepted
         (has a valid invitation code) the user is activated and if the request
@@ -159,9 +171,18 @@ class InvitationsBackend(object):
         try:
             user = form.save()
             if self._is_preaccepted(user):
-                user.is_active = True
-                user.save()
-                message = _('Registration completed. You can now login.')
+                if user.email_verified:
+                    user.is_active = True
+                    user.save()
+                    message = _('Registration completed. You can now login.')
+                else:
+                    try:
+                        _send_verification(self.request, user, email_template_name)
+                        message = _('Verification sent to %s' % user.email)
+                    except (SMTPException, socket.error) as e:
+                        status = messages.ERROR
+                        name = 'strerror'
+                        message = getattr(e, name) if hasattr(e, name) else e
             else:
                 _send_notification(user, admin_email_template_name)
                 message = _('Your request for an account was successfully sent \
@@ -183,7 +204,7 @@ class InvitationsBackend(object):
             transaction.commit()
         return status, message, user
 
-class SimpleBackend(object):
+class SimpleBackend(SignupBackend):
     """
     A registration backend which implements the following workflow: a user
     supplies the necessary registation information, an incative user account is
@@ -191,6 +212,7 @@ class SimpleBackend(object):
     """
     def __init__(self, request):
         self.request = request
+        super(SimpleBackend, self).__init__()
 
     def get_signup_form(self, provider):
         """
@@ -207,7 +229,14 @@ class SimpleBackend(object):
         ip = self.request.META.get('REMOTE_ADDR',
                 self.request.META.get('HTTP_X_REAL_IP', None))
         return globals()[formclass](initial_data, ip=ip)
-
+    
+    def _is_preaccepted(self, user):
+        if super(SimpleBackend, self)._is_preaccepted(user):
+            return True
+        if MODERATION_ENABLED:
+            return False
+        return True
+    
     @transaction.commit_manually
     def signup(self, form, email_template_name='im/activation_email.txt', admin_email_template_name='im/admin_notification.txt'):
         """
@@ -233,7 +262,7 @@ class SimpleBackend(object):
         """
         user = form.save()
         status = messages.SUCCESS
-        if MODERATION_ENABLED:
+        if not self._is_preaccepted(user):
             try:
                 _send_notification(user, admin_email_template_name)
                 message = _('Your request for an account was successfully sent \
index 029e9c8..647e0e4 100644 (file)
 
 from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED, IM_STATIC_URL, \
         COOKIE_NAME
+from astakos.im.api import get_menu
+
 from django.conf import settings
 from django.core.urlresolvers import reverse
+from django.utils import simplejson as json
 
 def im_modules(request):
     return {'im_modules': IM_MODULES}
@@ -51,18 +54,10 @@ def invitations(request):
 def media(request):
     return {'IM_STATIC_URL' : IM_STATIC_URL}
 
-def cloudbar(request):
-    """
-    Cloudbar configuration
-    """
-    CB_LOCATION = getattr(settings, 'CLOUDBAR_LOCATION', IM_STATIC_URL + 'cloudbar/')
-    CB_COOKIE_NAME = getattr(settings, 'CLOUDBAR_COOKIE_NAME', COOKIE_NAME)
-    CB_ACTIVE_SERVICE = getattr(settings, 'CLOUDBAR_ACTIVE_SERVICE', 'cloud')
-    
+def menu(request):
     absolute = lambda (url): request.build_absolute_uri(url)
-    
-    return {'CLOUDBAR_LOC': CB_LOCATION,
-            'CLOUDBAR_COOKIE_NAME': CB_COOKIE_NAME,
-            'ACTIVE_SERVICE': CB_ACTIVE_SERVICE,
-            'GET_SERVICES_URL': absolute(reverse('astakos.im.api.get_services')),
-            'GET_MENU_URL': absolute(reverse('astakos.im.api.get_menu'))}
+    resp = get_menu(request)
+    menu_items = json.loads(resp.content)[1:]
+    for item in menu_items:
+        item['is_active'] = absolute(request.path) == item['url']
+    return {'menu':menu_items}
index 633312c..8ca90b1 100644 (file)
@@ -54,7 +54,7 @@ class LocalUserCreationForm(UserCreationForm):
     """
     Extends the built in UserCreationForm in several ways:
     
-    * Adds email, first_name and last_name field.
+    * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
     * The username field isn't visible and it is assigned a generated id.
     * User created is not active. 
     """
@@ -134,7 +134,9 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
         """
         super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
         self.fields.keyOrder = ['email', 'inviter', 'first_name',
-                                'last_name', 'password1', 'password2']
+                                'last_name', 'password1', 'password2',
+                                'recaptcha_challenge_field',
+                                'recaptcha_response_field']
         #set readonly form fields
         self.fields['inviter'].widget.attrs['readonly'] = True
         self.fields['email'].widget.attrs['readonly'] = True
@@ -144,7 +146,8 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm):
         user = super(InvitedLocalUserCreationForm, self).save(commit=False)
         level = user.invitation.inviter.level + 1
         user.level = level
-        user.invitations = INVITATIONS_PER_LEVEL[level]
+        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
+        user.email_verified = True
         if commit:
             user.save()
         return user
index ea28313..716700b 100644 (file)
@@ -40,7 +40,7 @@ from ._common import get_user
     
 
 class Command(BaseCommand):
-    args = "<user id or email> [user id or email] ..."
+    args = "<user ID or email> [user ID or email] ..."
     help = "Activates one or more users"
     
     @transaction.commit_manually
index a16b64d..ecc8fd3 100644 (file)
@@ -36,6 +36,7 @@ import socket
 from smtplib import SMTPException
 
 from django.core.management.base import BaseCommand, CommandError
+from django.db.utils import IntegrityError
 
 from astakos.im.functions import invite
 
@@ -63,5 +64,7 @@ class Command(BaseCommand):
                 self.stdout.write("Invitation sent to '%s'\n" % (email,))
             except (SMTPException, socket.error) as e:
                 raise CommandError("Error sending the invitation")
+            except IntegrityError, e:
+                raise CommandError("There is already an invitation for %s" % (email,))
         else:
             raise CommandError("No invitations left")
index b75bd59..cf77dcd 100644 (file)
@@ -39,7 +39,7 @@ from ._common import get_user
 
 
 class Command(BaseCommand):
-    args = "<user_id or email>"
+    args = "<user ID or email>"
     help = "Modify a user's attributes"
     
     option_list = BaseCommand.option_list + (
@@ -47,6 +47,10 @@ class Command(BaseCommand):
             dest='invitations',
             metavar='NUM',
             help="Update user's invitations"),
+        make_option('--level',
+            dest='level',
+            metavar='NUM',
+            help="Update user's level"),
         make_option('--password',
             dest='password',
             metavar='PASSWORD',
@@ -66,7 +70,12 @@ class Command(BaseCommand):
             dest='noadmin',
             default=False,
             help="Revoke user's admin rights"),
-        make_option('--inactive',
+        make_option('--set-active',
+            action='store_true',
+            dest='active',
+            default=False,
+            help="Change user's state to inactive"),
+        make_option('--set-inactive',
             action='store_true',
             dest='inactive',
             default=False,
@@ -75,7 +84,7 @@ class Command(BaseCommand):
     
     def handle(self, *args, **options):
         if len(args) != 1:
-            raise CommandError("Please provide a user_id or email")
+            raise CommandError("Please provide a user ID or email")
         
         user = get_user(args[0])
         if not user:
@@ -86,10 +95,19 @@ class Command(BaseCommand):
         elif options.get('noadmin'):
             user.is_superuser = False
         
+        if options.get('active'):
+            user.is_active = True
+        elif options.get('inactive'):
+            user.is_active = False
+        
         invitations = options.get('invitations')
         if invitations is not None:
             user.invitations = int(invitations)
         
+        level = options.get('level')
+        if level is not None:
+            user.level = int(level)
+        
         password = options.get('password')
         if password is not None:
             user.set_password(password)
@@ -97,6 +115,4 @@ class Command(BaseCommand):
         if options['renew_token']:
             user.renew_token()
         
-        if options.get('inactive'):
-            user.is_active = False
-        user.save()
\ No newline at end of file
+        user.save()
index b40d01c..2e136d0 100644 (file)
@@ -39,6 +39,7 @@ from ._common import format_bool, format_date
 
 
 class Command(BaseCommand):
+    args = "<invitation ID>"
     help = "Show invitation info"
     
     def handle(self, *args, **options):
index bf2e9bd..0f859b9 100644 (file)
@@ -39,11 +39,12 @@ from ._common import format_bool, format_date
 
 
 class Command(BaseCommand):
+    args = "<user ID or email>"
     help = "Show user info"
     
     def handle(self, *args, **options):
         if len(args) != 1:
-            raise CommandError("Please provide a user_id or email")
+            raise CommandError("Please provide a user ID or email")
         
         email_or_id = args[0]
         try:
diff --git a/snf-astakos-app/astakos/im/migrations/0003_auto__add_unique_invitation_username.py b/snf-astakos-app/astakos/im/migrations/0003_auto__add_unique_invitation_username.py
new file mode 100644 (file)
index 0000000..3c67560
--- /dev/null
@@ -0,0 +1,87 @@
+# 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 unique constraint on 'Invitation', fields ['username']
+        db.create_unique('im_invitation', ['username'])
+
+
+    def backwards(self, orm):
+        
+        # Removing unique constraint on 'Invitation', fields ['username']
+        db.delete_unique('im_invitation', ['username'])
+
+
+    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.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'}),
+            '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/migrations/0004_auto__add_field_astakosuser_email_verified.py b/snf-astakos-app/astakos/im/migrations/0004_auto__add_field_astakosuser_email_verified.py
new file mode 100644 (file)
index 0000000..6b27b42
--- /dev/null
@@ -0,0 +1,88 @@
+# 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 field 'AstakosUser.email_verified'
+        db.add_column('im_astakosuser', 'email_verified', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'AstakosUser.email_verified'
+        db.delete_column('im_astakosuser', 'email_verified')
+
+
+    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.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'}),
+            'email_verified': ('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']
index d004b09..4cc54d1 100644 (file)
@@ -59,7 +59,7 @@ class AstakosUser(User):
     #for invitations
     user_level = DEFAULT_USER_LEVEL
     level = models.IntegerField('Inviter level', default=user_level)
-    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL[user_level])
+    invitations = models.IntegerField('Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
     
     auth_token = models.CharField('Authentication Token', max_length=32,
                                   null=True, blank=True)
@@ -72,6 +72,8 @@ class AstakosUser(User):
     # ex. screen_name for twitter, eppn for shibboleth
     third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
     
+    email_verified = models.BooleanField('Email verified?', default=False)
+    
     @property
     def realname(self):
         return '%s %s' %(self.first_name, self.last_name)
@@ -132,7 +134,7 @@ class Invitation(models.Model):
     inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
                                 null=True)
     realname = models.CharField('Real name', max_length=255)
-    username = models.CharField('Unique ID', max_length=255)
+    username = models.CharField('Unique ID', max_length=255, unique=True)
     code = models.BigIntegerField('Invitation code', db_index=True)
     #obsolete: we keep it just for transfering the data
     is_accepted = models.BooleanField('Accepted?', default=False)
@@ -170,4 +172,4 @@ def report_user_event(user):
         conn = exchange_connect(QUEUE_CONNECTION)
         routing_key = QUEUE_CONNECTION.replace('#', body['id'])
         exchange_send(conn, routing_key, body)
-        exchange_close(conn)
\ No newline at end of file
+        exchange_close(conn)
index 8d9c400..26fc370 100644 (file)
@@ -65,4 +65,7 @@ BILLING_FIELDS = getattr(settings, 'ASTAKOS_BILLING_FIELDS', ['id', 'is_active',
 QUEUE_CONNECTION = getattr(settings, 'ASTAKOS_QUEUE_CONNECTION', None) # Example: 'rabbitmq://guest:guest@localhost:5672/astakos.userEvent.#'
 
 # Set where the user should be redirected after logout
-LOGOUT_NEXT = getattr(settings, 'ASTAKOS_LOGOUT_NEXT', '')
\ No newline at end of file
+LOGOUT_NEXT = getattr(settings, 'ASTAKOS_LOGOUT_NEXT', '')
+
+# Set user email patterns that are automatically activated
+RE_USER_EMAIL_PATTERNS = getattr(settings, 'ASTAKOS_RE_USER_EMAIL_PATTERNS', [])
\ No newline at end of file
index b4b32f0..9e4faef 100644 (file)
@@ -36,8 +36,9 @@ Each page that wants to display the navigation bar should:
       and the authentication status of the current visitor of the page.
     - Set the ``CLOUDBAR_ACTIVE_SERVICE`` to the id of the service the current
       page refers to so that script cat set the appropriate active styles to
-      the services menu for services ids see ``SERVICES_LINK``
-      object in cloudbar.js.
+      the services menu for services ids see ``SERVICES_LINK`` object in 
+      cloudbar.js. Use special **accounts** value to set account menu as the
+      active link.
     - Set the ``CLOUDBAR_LOCATION`` to the url where bar files are served from.
     - Include the servicesbar.js script.
 
index baf2efa..15cd273 100644 (file)
   font-weight: bold !important;
   font-size: 12px !important;
 }
+.servicesbar .profile .user.active {
+  background-color: #333;
+}
+
 .servicesbar .profile a {
   float: none;
 }
index 906c3a2..6c8765b 100644 (file)
@@ -58,6 +58,7 @@ $(document).ready(function(){
     
     // create profile links
     var user = $('<div class="user"></div>');    
+    if (ACTIVE_MENU == "accounts") { user.addClass("hover active")}
     var username = $('<a href="#"></a>');
     var usermenu = $("<ul>");
     var get_menu_url = (window.GET_MENU_URL || window.CLOUDBAR_MENU) + '?callback=?&location=' + window.location.toString();
index 4bf8d19..9595c51 100644 (file)
                 font-weight: bold !important;
                 font-size: 12px !important;
             }    
+
+            &.active {
+                background-color: #333;
+            }
         }
         a {
                     float: none;
diff --git a/snf-astakos-app/astakos/im/static/im/css/boxsizing.htc b/snf-astakos-app/astakos/im/static/im/css/boxsizing.htc
new file mode 100644 (file)
index 0000000..6d2e835
--- /dev/null
@@ -0,0 +1,308 @@
+/**
+* CSS-JS-BOOSTER
+* 
+* A polyfill for box-sizing: border-box for IE6 & IE7.
+* 
+* JScript
+* 
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Lesser General Public License as published 
+* by the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+* 
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Lesser General Public License for more details.
+* 
+* See <http://www.gnu.org/licenses/lgpl-3.0.txt>
+* 
+* @category  JScript 
+* @package   box-sizing-polyfill
+* @author    Christian Schepp Schaefer <schaepp@gmx.de> <http://twitter.com/derSchepp>
+* @copyright 2010 Christian Schepp Schaefer
+* @license   http://www.gnu.org/copyleft/lesser.html The GNU LESSER GENERAL PUBLIC LICENSE, Version 3.0
+* @link      http://github.com/Schepp/box-sizing-polyfill 
+*
+* PREFACE:
+*
+* This box-sizing polyfill is based on previous work done by Erik Arvidsson, 
+* which he published in 2002 on http://webfx.eae.net/dhtml/boxsizing/boxsizing.html.
+*
+* USAGE:
+*      
+* Add the behavior/HTC after every `box-sizing: border-box;` that you assign:
+* 
+* box-sizing: border-box;
+* *behavior: url(/scripts/boxsizing.htc);`
+* 
+* If you prefix the `behavior` property with a star, like seen above, it will only be seen by 
+* IE6 & IE7, not by IE8+ (it's a hack) which is better for the performance on those newer browsers.
+*
+* The URL to the HTC file must be relative to your HTML(!) document, not relative to your CSS.
+* That's why I'd advise you to use absolute paths like in the example.
+*
+*/
+<component lightWeight="true">
+<attach event="onpropertychange" onevent="checkPropertyChange()" />
+<attach event="ondetach" onevent="restore()" />
+<attach event="onresize" for="window" onevent="restore();init()" />
+<script type="text/javascript">
+//<![CDATA[
+
+var viewportwidth = (typeof window.innerWidth != 'undefined' ? window.innerWidth : element.document.documentElement.clientWidth);
+// Shortcut for the document object
+var doc = element.document;
+
+/*
+* init gets called once at the start and then never again, 
+* triggers box-sizing calculations and updates width and height
+*/
+function init(){
+       // check for IE8+
+       if(typeof(element.style.boxSizing) == "undefined"){
+               updateBorderBoxWidth();
+               updateBorderBoxHeight();
+       }
+}
+
+/*
+* restore gets called when the behavior is being detached (see event binding at the top),
+* resets everything like it was before applying the behavior
+*/
+function restore(){
+       // check for IE8+
+       if(typeof(element.style.boxSizing) == "undefined"){
+               element.runtimeStyle.width = "";
+               element.runtimeStyle.height = "";
+       }
+}
+
+/*
+* checkPropertyChange gets called as soon as an element property changes 
+* (see event binding at the top), it then checks if any property influencing its 
+* dimensions was changed and if yes recalculates width and height 
+*/
+function checkPropertyChange(){
+       // check for IE8+
+       if(typeof(element.style.boxSizing) == "undefined"){
+               var pn = event.propertyName;
+               var undef;
+               if(pn == "style.boxSizing" && element.style.boxSizing == ""){
+                       element.style.removeAttribute("boxSizing");
+                       element.runtimeStyle.boxSizing = undef;
+               }
+               switch (pn){
+                       case "style.width":
+                       case "style.borderLeftWidth":
+                       case "style.borderLeftStyle":
+                       case "style.borderRightWidth":
+                       case "style.borderRightStyle":
+                       case "style.paddingLeft":
+                       case "style.paddingRight":
+                               updateBorderBoxWidth();
+                       break;
+               
+                       case "style.height":
+                       case "style.borderTopWidth":
+                       case "style.borderTopStyle":
+                       case "style.borderBottomWidth":
+                       case "style.borderBottomStyle":
+                       case "style.paddingTop":
+                       case "style.paddingBottom":
+                               updateBorderBoxHeight();
+                       break;
+               
+                       case "className":
+                       case "style.boxSizing":
+                               updateBorderBoxWidth();
+                               updateBorderBoxHeight();
+                       break;
+               }
+       }
+}
+
+/* 
+ * Helper function, taken from Dean Edward's IE7 framework,
+ * added by Schepp on 12.06.2010.
+ * http://code.google.com/p/ie7-js/
+ *
+ * Allows us to convert from relative to pixel-values.
+ */
+function getPixelValue(value){
+       var PIXEL = /^\d+(px)?$/i;
+       if (PIXEL.test(value)) return parseInt(value);
+       var style = element.style.left;
+       var runtimeStyle = element.runtimeStyle.left;
+       element.runtimeStyle.left = element.currentStyle.left;
+       element.style.left = value || 0;
+       value = parseInt(element.style.pixelLeft);
+       element.style.left = style;
+       element.runtimeStyle.left = runtimeStyle;
+       
+       return value;
+}
+
+function getPixelWidth(object, value){
+       // For Pixel Values
+       var PIXEL = /^\d+(px)?$/i;
+       if (PIXEL.test(value)) return parseInt(value);
+       
+       // For Percentage Values
+       var PERCENT = /^[\d\.]+%$/i;
+       if (PERCENT.test(value)){
+               try{
+                       parentWidth = getPixelWidth(object.parentElement,(object.parentElement.currentStyle.width != "auto" ? object.parentElement.currentStyle.width : "100%"));
+                       value = (parseFloat(value) / 100) * parentWidth;
+               }
+               catch(e){
+                       value = (parseFloat(value) / 100) * element.document.documentElement.clientWidth;
+               }
+               return parseInt(value);
+       }
+       
+       // For EM Values
+       var style = object.style.left;
+       var runtimeStyle = object.runtimeStyle.left;
+       object.runtimeStyle.left = object.currentStyle.left;
+       object.style.left = value || 0;
+       value = parseInt(object.style.pixelLeft);
+       object.style.left = style;
+       object.runtimeStyle.left = runtimeStyle;
+       
+       return value;
+}
+
+
+/*
+ * getBorderWidth & friends
+ * Border width getters
+ */
+function getBorderWidth(sSide){
+       if(element.currentStyle["border" + sSide + "Style"] == "none"){
+               return 0;
+       }
+       var n = getPixelValue(element.currentStyle["border" + sSide + "Width"]);
+       return n || 0;
+}
+function getBorderLeftWidth() { return getBorderWidth("Left"); }
+function getBorderRightWidth() { return getBorderWidth("Right"); }
+function getBorderTopWidth() { return getBorderWidth("Top"); }
+function getBorderBottomWidth() { return getBorderWidth("Bottom"); }
+
+
+/*
+ * getPadding & friends
+ * Padding width getters
+ */
+function getPadding(sSide) {
+       var n = getPixelValue(element.currentStyle["padding" + sSide]);
+       return n || 0;
+}
+function getPaddingLeft() { return getPadding("Left"); }
+function getPaddingRight() { return getPadding("Right"); }
+function getPaddingTop() { return getPadding("Top"); }
+function getPaddingBottom() { return getPadding("Bottom"); }
+
+
+
+/*
+ * getBoxSizing
+ * Get the box-sizing value for the current element
+ */
+function getBoxSizing(){
+       var s = element.style;
+       var cs = element.currentStyle
+       if(typeof s.boxSizing != "undefined" && s.boxSizing != ""){
+               return s.boxSizing;
+       }
+       if(typeof s["box-sizing"] != "undefined" && s["box-sizing"] != ""){
+               return s["box-sizing"];
+       }
+       if(typeof cs.boxSizing != "undefined" && cs.boxSizing != ""){
+               return cs.boxSizing;
+       }
+       if(typeof cs["box-sizing"] != "undefined" && cs["box-sizing"] != ""){
+               return cs["box-sizing"];
+       }
+       return getDocumentBoxSizing();
+}
+
+
+/*
+ * getDocumentBoxSizing
+ * Get the default document box sizing (check for quirks mode)
+ */
+function getDocumentBoxSizing(){
+       if(doc.compatMode == null || doc.compatMode == "BackCompat"){
+               return "border-box";
+       }
+       return "content-box"
+}
+
+
+/*
+ * setBorderBoxWidth & friends
+ * Width and height setters
+ */
+function setBorderBoxWidth(n){
+       element.runtimeStyle.width = Math.max(0, n - getBorderLeftWidth() -
+               getPaddingLeft() - getPaddingRight() - getBorderRightWidth()) + "px";
+}
+function setBorderBoxHeight(n){
+       element.runtimeStyle.height = Math.max(0, n - getBorderTopWidth() -
+               getPaddingTop() - getPaddingBottom() - getBorderBottomWidth()) + "px";
+}
+function setContentBoxWidth(n){
+       element.runtimeStyle.width = Math.max(0, n + getBorderLeftWidth() +
+               getPaddingLeft() + getPaddingRight() + getBorderRightWidth()) + "px";
+}
+function setContentBoxHeight(n){
+       element.runtimeStyle.height = Math.max(0, n + getBorderTopWidth() +
+               getPaddingTop() + getPaddingBottom() + getBorderBottomWidth()) + "px";
+}
+
+
+/*
+ * updateBorderBoxWidth & updateBorderBoxHeight
+ * 
+ */
+function updateBorderBoxWidth() {
+       if(getDocumentBoxSizing() == getBoxSizing()){
+               return;
+       }
+       var csw = element.currentStyle.width;
+       if(csw != "auto"){
+               csw = getPixelWidth(element,csw);
+               if(getBoxSizing() == "border-box"){
+                       setBorderBoxWidth(parseInt(csw));
+               }
+               else{
+                       setContentBoxWidth(parseInt(csw));
+               }
+       }
+}
+
+function updateBorderBoxHeight() {
+       if(getDocumentBoxSizing() == getBoxSizing()){
+               return;
+       }
+       var csh = element.currentStyle.height;
+       if(csh != "auto"){
+               csh = getPixelValue(csh);
+               if(getBoxSizing() == "border-box"){
+                       setBorderBoxHeight(parseInt(csh));
+               }
+               else{
+                       setContentBoxHeight(parseInt(csh));
+               }
+       }
+}
+
+
+// Run the calculations
+init();
+
+//]]>
+</script>
+</component>
\ No newline at end of file
index 610ebd1..495c9eb 100644 (file)
@@ -200,10 +200,6 @@ textarea {
   line-height: 22px;
   letter-spacing: 1px;
   background-color: #3582ac;
-  -webkit-transition: background-color 0.15s linear;
-  transition: background-color 0.15s linear;
-  -webkit-transition: background-color 0.15s linear;
-  transition: background-color 0.15s linear;
   color: #ffffff;
   border: none;
   padding: 0.8em 22px;
@@ -212,61 +208,14 @@ textarea {
 .button:hover {
   background-color: #f89a1c;
 }
-.button a {
-  color: #ffffff !important;
-  text-decoration: none !important;
-  border: none !important;
-}
 a.button {
-  color: #ffffff !important;
-  text-decoration: none !important;
-  border: none !important;
-}
-.makeRow {
-  zoom: 1;
-  margin-left: -22px;
-}
-.makeRow:before, .makeRow:after {
-  display: table;
-  content: "";
-  zoom: 1;
-}
-.makeRow:after {
-  clear: both;
-}
-.button {
-  font-family: 'Antic', sans-serif;
-  font-size: 14px;
-  font-weight: normal;
-  line-height: 22px;
-  letter-spacing: 1px;
-  font-family: 'Antic', sans-serif;
-  font-size: 14px;
-  font-weight: normal;
-  line-height: 22px;
-  letter-spacing: 1px;
-  background-color: #3582ac;
-  -webkit-transition: background-color 0.15s linear;
-  transition: background-color 0.15s linear;
-  -webkit-transition: background-color 0.15s linear;
-  transition: background-color 0.15s linear;
-  color: #ffffff;
-  border: none;
-  padding: 0.8em 22px;
-  font-size: 1em;
-}
-.button:hover {
-  background-color: #f89a1c;
-}
-.button a {
-  color: #ffffff !important;
-  text-decoration: none !important;
+  text-align: center !important;
+  color: #fff !important;
   border: none !important;
+  display: block !important;
 }
-a.button {
-  color: #ffffff !important;
-  text-decoration: none !important;
-  border: none !important;
+a.button:hover {
+  color: #fff !important;
 }
 /*addon to style django forms rendered with as_p filter*/
 /*
@@ -470,6 +419,53 @@ table .purple {
 table .headerSortUp.purple, table .headerSortDown.purple {
   background-color: #e2d5f0;
 }
+.makeRow {
+  zoom: 1;
+  margin-left: -22px;
+}
+.makeRow:before, .makeRow:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+.makeRow:after {
+  clear: both;
+}
+.button {
+  font-family: 'Antic', sans-serif;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 22px;
+  letter-spacing: 1px;
+  font-family: 'Antic', sans-serif;
+  font-size: 14px;
+  font-weight: normal;
+  line-height: 22px;
+  letter-spacing: 1px;
+  background-color: #3582ac;
+  color: #ffffff;
+  border: none;
+  padding: 0.8em 22px;
+  font-size: 1em;
+}
+.button:hover {
+  background-color: #f89a1c;
+}
+a.button {
+  text-align: center !important;
+  color: #fff !important;
+  border: none !important;
+  display: block !important;
+}
+a.button:hover {
+  color: #fff !important;
+}
+.border-box {
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  *behavior: url(boxsizing.htc);
+}
 body {
   font-family: 'Antic', sans-serif;
   font-size: 14px;
@@ -516,7 +512,8 @@ body {
 section a,
 p a,
 form a,
-.section a {
+.section a,
+.styledlinks a {
   color: #000000;
   text-decoration: none;
   border-bottom: 1px solid #f89a1c;
@@ -524,21 +521,27 @@ form a,
 section a:hover,
 p a:hover,
 form a:hover,
-.section a:hover {
+.section a:hover,
+.styledlinks a:hover {
   color: #f89a1c;
 }
 section a.noborder,
 p a.noborder,
 form a.noborder,
-.section a.noborder {
+.section a.noborder,
+.styledlinks a.noborder {
   border: none;
 }
 section a em,
 p a em,
 form a em,
-.section a em {
+.section a em,
+.styledlinks a em {
   color: #3582ac;
 }
+a.simple {
+  border: none;
+}
 a.action {
   color: #f89a1c;
   border-bottom: none;
@@ -581,7 +584,7 @@ a img {
 }
 div.header {
   position: relative;
-  margin-top: 42px;
+  margin-top: 88px;
   margin-bottom: 22px;
 }
 div.header h1 {
@@ -591,6 +594,12 @@ div.header h1 {
   border-bottom: 1px solid #cfcdc7;
   padding-bottom: 3px;
 }
+.mainlogo {
+  height: 36px;
+}
+.mainlogo h1 {
+  height: 36px;
+}
 .mainlogo img {
   margin-left: -10px;
 }
@@ -626,14 +635,17 @@ ul.inline li {
   margin-right: 0;
   margin-left: 1em;
 }
+.navigation {
+  height: 83px;
+}
 .mainnav {
-  font-size: 1.1em;
+  font-size: 1.2em;
 }
 .mainnav.subnav {
   margin-bottom: -22px;
 }
 .mainnav.subnav li {
-  margin-top: 11px;
+  margin-top: 1.2em;
 }
 .mainnav li {
   margin-top: 66px;
@@ -655,31 +667,56 @@ ul.inline li {
   border-bottom: 1px solid #f89a1c;
   color: #f89a1c;
 }
-.page {
+.bottom-section {
+  zoom: 1;
+}
+.bottom-section:before, .bottom-section:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+.bottom-section:after {
+  clear: both;
+}
+.bottom-section .section img {
+  width: 200px;
+}
+.top-section {
+  zoom: 1;
+}
+.top-section:before, .top-section:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+.top-section:after {
+  clear: both;
+}
+div.page {
   zoom: 1;
   margin-left: -22px;
   zoom: 1;
   margin-left: -22px;
-  margin-top: 42px;
+  margin-top: 132px;
   font-size: 1.1em;
 }
-.page:before, .page:after {
+div.page:before, div.page:after {
   display: table;
   content: "";
   zoom: 1;
 }
-.page:after {
+div.page:after {
   clear: both;
 }
-.page:before, .page:after {
+div.page:before, div.page:after {
   display: table;
   content: "";
   zoom: 1;
 }
-.page:after {
+div.page:after {
   clear: both;
 }
-.page .page-inner {
+div.page .page-inner {
   position: relative;
 }
 .maincol {
@@ -736,36 +773,12 @@ ul.inline li {
 .rightcol input[type=text], .rightcol input[type=password] {
   width: 273px;
 }
-/* recaptcha */
-#recaptcha_widget_div {
-  margin-top: 10px;
-  margin-left: -4px;
-}
-#recaptcha_widget_div #recaptcha_instructions_image {
-  font-size: 0.8em;
-  margin-bottom: 10px;
-  display: block !important;
-}
 /* generic form styles */
 input, textarea, .form-widget {
   background-color: #ffffff;
-  color: #000000;
+  color: #808080;
   border-color: #000000;
 }
-.checkbox-widget.checked {
-  background-color: #f00;
-  background-image: url("../images/checkbox.png");
-  background-position: 50% 50%;
-}
-.checkbox-widget {
-  border: 1px solid #808080;
-  width: 25px;
-  height: 25px;
-  display: block;
-  float: left;
-  cursor: pointer;
-  margin-top: 9px;
-}
 #forms .input, #forms input {
   font-family: 'Antic', sans-serif;
   font-size: 14px;
@@ -778,6 +791,8 @@ input, textarea, .form-widget {
   line-height: 22px;
   letter-spacing: 1px;
   border: 1px solid #808080;
+  height: 21px;
+  display: inline-block;
   margin-bottom: -1px;
   padding: 0.8em;
   padding-left: 1.5em;
@@ -788,6 +803,9 @@ input, textarea, .form-widget {
   border: 1px solid #000;
   z-index: 100;
 }
+#forms .input:focus label, #forms input:focus label {
+  z-index: 300;
+}
 .altcol {
   background-color: #c3c3b9 !important;
 }
@@ -795,7 +813,13 @@ input, textarea, .form-widget {
   background-color: #f89a1c !important;
 }
 .section {
-  margin-bottom: 2em;
+  margin-bottom: 66px;
+}
+.section em {
+  color: #3582ac;
+}
+.section.positioned {
+  margin-bottom: 0;
 }
 .section.positioned .content {
   display: inline;
@@ -813,6 +837,7 @@ input, textarea, .form-widget {
   float: left;
 }
 .section.positioned h3 {
+  font-size: 1.2em;
   margin-bottom: 22px;
 }
 .section.positioned .text {
@@ -841,7 +866,7 @@ input, textarea, .form-widget {
 }
 input[readonly=true] {
   background-color: #ddd;
-  color: #2b2b2b;
+  color: #5e5e5e;
 }
 form.withlabels label {
   width: 224px;
@@ -855,8 +880,23 @@ form.withlabels input[type=text], form.withlabels input[type=password], form.wit
 form.withlabels input[type=text].long, form.withlabels input[type=password].long, form.withlabels textarea.long {
   width: 224px;
 }
+.login-section a.button {
+  margin-bottom: 12px;
+}
+.login-section a.button:last-child {
+  margin-bottom: none;
+}
+.login-section a.button.withicon {
+  background-repeat: no-repeat;
+  background-position: 15px 50%;
+  padding-left: 40px;
+}
+.login-section.loggedin {
+  padding-bottom: 0 !important;
+  background-image: none !important;
+}
 form.login {
-  margin-bottom: 3em;
+  margin-bottom: 33px;
 }
 form h2 {
   color: #000000;
@@ -871,7 +911,7 @@ form .form-row {
   position: relative;
 }
 form .form-row.submit {
-  margin-top: 22px;
+  margin-top: 33px;
 }
 form .form-row .extra-link {
   color: #808080;
@@ -905,6 +945,8 @@ form input[type="password"] {
   line-height: 22px;
   letter-spacing: 1px;
   border: 1px solid #808080;
+  height: 21px;
+  display: inline-block;
   margin-bottom: -1px;
   padding: 0.8em;
   padding-left: 1.5em;
@@ -918,6 +960,12 @@ form input[type="password"]:focus {
   border: 1px solid #000;
   z-index: 100;
 }
+form textarea:focus label,
+form input.text:focus label,
+form input[type="text"]:focus label,
+form input[type="password"]:focus label {
+  z-index: 300;
+}
 form input.submit, form input[type="submit"] {
   font-family: 'Antic', sans-serif;
   font-size: 14px;
@@ -930,10 +978,6 @@ form input.submit, form input[type="submit"] {
   line-height: 22px;
   letter-spacing: 1px;
   background-color: #3582ac;
-  -webkit-transition: background-color 0.15s linear;
-  transition: background-color 0.15s linear;
-  -webkit-transition: background-color 0.15s linear;
-  transition: background-color 0.15s linear;
   color: #ffffff;
   border: none;
   padding: 0.8em 22px;
@@ -949,10 +993,6 @@ form input.submit, form input[type="submit"] {
   line-height: 22px;
   letter-spacing: 1px;
   background-color: #3582ac;
-  -webkit-transition: background-color 0.15s linear;
-  transition: background-color 0.15s linear;
-  -webkit-transition: background-color 0.15s linear;
-  transition: background-color 0.15s linear;
   color: #ffffff;
   border: none;
   padding: 0.8em 22px;
@@ -961,18 +1001,12 @@ form input.submit, form input[type="submit"] {
 form input.submit:hover, form input[type="submit"]:hover {
   background-color: #f89a1c;
 }
-form input.submit a, form input[type="submit"] a {
-  color: #ffffff !important;
-  text-decoration: none !important;
-  border: none !important;
-}
 form input.submit:hover, form input[type="submit"]:hover {
   background-color: #f89a1c;
 }
-form input.submit a, form input[type="submit"] a {
-  color: #ffffff !important;
-  text-decoration: none !important;
-  border: none !important;
+form textarea {
+  height: 200px;
+  width: 350px !important;
 }
 form .with-errors input, form .with-errors textarea, form .with-errors select {
   color: #9d261d;
@@ -995,24 +1029,42 @@ form .with-errors label {
 div.form-stacked {
   margin-bottom: 4em;
 }
+.rightcol .section {
+  background-image: url("../images/dashed_border.png");
+  background-repeat: repeat-x;
+  background-position: left bottom;
+  padding-bottom: 44px;
+  margin-bottom: 44px;
+}
 .section h2 {
   font-size: 1.1em;
+  line-height: 1.3em;
   margin-bottom: 33px;
 }
 .section h2 a {
   color: #4085A6;
   border: none;
+  line-height: 1.3em;
+}
+.section h3 {
+  font-size: 1.1em;
+  line-height: 1.3em;
+  margin-bottom: 33px;
 }
 .section p {
   line-height: 1.7em;
 }
+.section .section-img {
+  margin-bottom: 22px;
+}
 .messages {
   display: inline;
   float: left;
   margin-left: 22px;
   width: 798px;
-  margin-bottom: 2em;
+  margin: 2em 0;
   background-color: #ddd;
+  margin-left: 0;
 }
 .messages li {
   padding: 1em;
@@ -1105,12 +1157,21 @@ table tr.consumed td.consumed {
   margin-right: 0;
 }
 /*pagination*/
+.pagination a.page {
+  display: inline !important;
+}
 /*blog styles*/
 .blog-entry {
-  margin-bottom: 2em;
+  margin-bottom: 66px;
   zoom: 1;
   margin-bottom: 44px;
 }
+.blog-entry em {
+  color: #3582ac;
+}
+.blog-entry.positioned {
+  margin-bottom: 0;
+}
 .blog-entry.positioned .content {
   display: inline;
   float: left;
@@ -1127,6 +1188,7 @@ table tr.consumed td.consumed {
   float: left;
 }
 .blog-entry.positioned h3 {
+  font-size: 1.2em;
   margin-bottom: 22px;
 }
 .blog-entry.positioned .text {
@@ -1155,15 +1217,25 @@ table tr.consumed td.consumed {
 }
 .blog-entry h2 {
   font-size: 1.1em;
+  line-height: 1.3em;
   margin-bottom: 33px;
 }
 .blog-entry h2 a {
   color: #4085A6;
   border: none;
+  line-height: 1.3em;
+}
+.blog-entry h3 {
+  font-size: 1.1em;
+  line-height: 1.3em;
+  margin-bottom: 33px;
 }
 .blog-entry p {
   line-height: 1.7em;
 }
+.blog-entry .section-img {
+  margin-bottom: 22px;
+}
 .blog-entry:before, .blog-entry:after {
   display: table;
   content: "";
@@ -1175,6 +1247,7 @@ table tr.consumed td.consumed {
 .blog-entry .title {
   margin-bottom: 1em;
   font-size: 1.1em;
+  line-height: 1.4em;
 }
 .blog-entry .media img {
   border: 1px solid #808080;
@@ -1199,6 +1272,11 @@ table tr.consumed td.consumed {
   margin-bottom: 22px;
   color: #808080;
 }
+.section.twitter-feed .tweet:last-child {
+  margin-bottom: 0;
+  padding-bottom: 0;
+  border-bottom: none;
+}
 .section.twitter-feed .tweet .date {
   display: block;
   font-size: 0.7em;
@@ -1207,3 +1285,83 @@ table tr.consumed td.consumed {
   text-decoration: none !important;
   border: none;
 }
+.pagination .page {
+  margin-left: 0;
+}
+.entry-list .since {
+  font-size: 0.8em;
+}
+.entry-list .title {
+  margin-bottom: 1em;
+}
+.entry-list .content, .entry-list .text {
+  margin-bottom: 2em;
+  font-size: 0.8em;
+}
+.initial_hidden {
+  display: none;
+}
+/*resources styles*/
+.resources .categories ul {
+  zoom: 1;
+}
+.resources .categories ul:before, .resources .categories ul:after {
+  display: table;
+  content: "";
+  zoom: 1;
+}
+.resources .categories ul:after {
+  clear: both;
+}
+.resources .categories ul li {
+  float: left;
+}
+.resources .categories .title {
+  margin-bottom: 11px;
+}
+.resources .categories ul li a {
+  text-decoration: none;
+  color: #faaf40;
+  margin-right: 22px;
+}
+.resources .list {
+  margin-top: 88px;
+}
+.resources .list .resource {
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  *behavior: url(boxsizing.htc);
+  width: 33%;
+  padding: 44px;
+  float: left;
+  border: 1px solid #faaf40;
+  height: 264px;
+}
+.resources .list .resource .description {
+  display: none;
+}
+/* recaptcha */
+#recaptcha_widget_div {
+  margin-top: 10px;
+  margin-left: -4px;
+}
+#recaptcha_widget_div #recaptcha_instructions_image {
+  font-size: 0.8em;
+  margin-bottom: 10px;
+  display: block !important;
+}
+.checkbox-widget.checked {
+  background-color: #f00;
+  background-image: url("../images/checkbox.png");
+  background-position: 50% 50%;
+}
+.checkbox-widget {
+  border: 1px solid #808080;
+  width: 25px;
+  height: 25px;
+  display: block;
+  float: left;
+  cursor: pointer;
+  margin-top: 9px;
+}
index 4a93e29..5673330 100644 (file)
@@ -2,7 +2,17 @@
 @import "../less/xtra.less";
 @import "../less/django_forms.less";
 @import "../less/tables.less";
+@import "../less/xtra.less";
+
 
+// mixins
+
+.border-box {
+    -moz-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    *behavior: url(boxsizing.htc);
+}
 
 @gradCol: #ddd;
 body {
@@ -36,7 +46,7 @@ body {
 }
 
 // default link styles
-section a, p a, form a, .section a {
+section a, p a, form a, .section a, .styledlinks a {
     color: @black;
     text-decoration: none;
     border-bottom: 1px solid @linkColor;
@@ -54,6 +64,10 @@ section a, p a, form a, .section a {
     }
 }
 
+a.simple {
+    border: none;
+
+}
 a.action {
     color: @linkColor;
     border-bottom: none;
@@ -91,7 +105,7 @@ a img {
 
 div.header {
     position: relative;
-    margin-top: 42px;
+    margin-top: 4*@gridGutterWidth;
     margin-bottom: @gridGutterWidth;
     h1 {
         color: @shadowColor;
@@ -102,6 +116,9 @@ div.header {
     }
 }
 .mainlogo {
+    height: 36px;
+
+    h1 { height: 36px }
     img {
         margin-left: -10px;
     }
@@ -135,14 +152,17 @@ ul.inline {
     }
 }
 
+.navigation {
+    height: 83px;    
+}
 
 .mainnav {
-    font-size: 1.1em;
+    font-size: 1.2em;
     
     &.subnav {
         margin-bottom: -@gridGutterWidth;
         li {
-            margin-top: @gridGutterWidth/2;
+            margin-top: 1.2em;
         }
     }
 
@@ -172,9 +192,23 @@ ul.inline {
     }
 }
 
-.page {
+.bottom-section {
+    .clearfix();
+
+    .section {
+        img {
+            width: 200px;    
+        }
+    }
+}
+
+.top-section {
+    .clearfix();
+}
+
+div.page {
     .makeRow();
-    margin-top: 42px;
+    margin-top: 6*@gridGutterWidth;
     font-size: 1.1em;
     .page-inner {
         position: relative;    
@@ -214,54 +248,31 @@ ul.inline {
     }
 }
 
-
-/* recaptcha */
-#recaptcha_widget_div {
-    margin-top: 10px;
-    margin-left: -4px;
-
-    #recaptcha_instructions_image {
-        font-size: 0.8em;
-        margin-bottom: 10px;
-        display: block !important;
-    }
-}
 /* generic form styles */
 input, textarea, .form-widget {
     background-color: @white;
-    color: @black;
+    color: @gray;
     border-color: @black;
 }
 
-.checkbox-widget.checked {
-    background-color: #f00;
-    background-image: url("../images/checkbox.png");
-    background-position: 50% 50%;
-}
-
-.checkbox-widget {
-    border: 1px solid @gray;
-    width: 25px;
-    height: 25px;
-    display: block;
-    float: left;
-    cursor: pointer;
-    margin-top: @gridGutterWidth/2 - 2;
-}
-
 #forms {
     .input, input {
         #font.main();
         border: 1px solid @gray;
+        height:21px;
+        display: inline-block;
         margin-bottom: -1px;
         padding: 0.8em;
         padding-left: 1.5em;
         z-index: 2;
-
         &:focus {
             position: relative;
             border: 1px solid #000;
             z-index: 100;
+
+            label {
+                z-index: 300;    
+            }
         }
     }
 }
@@ -275,9 +286,13 @@ input, textarea, .form-widget {
 }
 
 .section {
+    
+    em {
+        color: @blue;
+    }
 
     &.positioned {
-        
+        margin-bottom: 0;
         .content {
             .makeColumn(4);
         }
@@ -296,6 +311,7 @@ input, textarea, .form-widget {
         }
         
         h3 {
+            font-size: 1.2em;
             margin-bottom: @gridGutterWidth;    
         }
 
@@ -304,7 +320,7 @@ input, textarea, .form-widget {
         }
     }
 
-    margin-bottom: 2em; 
+    margin-bottom: 3*@gridGutterWidth; 
 
     .left, .right {
         width: 50%;
@@ -324,7 +340,7 @@ input, textarea, .form-widget {
 
 input[readonly=true] {
     background-color: #ddd;
-    color: darken(#ddd, 70%);
+    color: darken(#ddd, 50%);
 }
 
 form.withlabels {
@@ -345,12 +361,34 @@ form.withlabels {
 }
 
 
+.login-section {
+    a.button {
+        margin-bottom: 0.2*@gridColumnWidth;    
+        
+        &:last-child {
+            margin-bottom: none;    
+        }
+
+        &.withicon {
+            background-repeat: no-repeat;
+            background-position: 15px 50%;
+            padding-left: 40px;
+        }
+    }    
+
+    &.loggedin {
+        padding-bottom: 0 !important;
+        background-image: none !important;
+    }
+}
+
+
 @errorColor: lighten(@red, 30%);
 // forms
 form {
     
     &.login {
-        margin-bottom: 3em; 
+        margin-bottom: 1.5*@gridGutterWidth; 
     }
 
     h2 {
@@ -367,7 +405,7 @@ form {
         min-height: 2*@gridGutterWidth;
         position: relative;
         &.submit {
-            margin-top: @gridGutterWidth;
+            margin-top: 1.5*@gridGutterWidth;
         }
 
         .extra-link {
@@ -400,6 +438,10 @@ form {
     }
 
     
+    textarea {
+        height: 200px;
+        width: 350px !important;
+    }
     .with-errors {
         input, textarea, select {
             color: @red;
@@ -430,20 +472,42 @@ div.form-stacked {
     margin-bottom: 4em;
 }
 // content types
+
+.rightcol .section {
+    background-image: url("../images/dashed_border.png");
+    background-repeat: repeat-x;
+    background-position: left bottom;
+    padding-bottom: 2*@gridGutterWidth;
+    margin-bottom: 2*@gridGutterWidth;
+
+}
+
 .section {
     h2 {
         font-size: 1.1em;
+        line-height: 1.3em;
         margin-bottom: 1.5*@gridGutterWidth;    
 
         a {
             color: #4085A6;
             border: none;
+            line-height: 1.3em;
         }
     }
 
+    h3 {
+        font-size: 1.1em;    
+        line-height: 1.3em;
+        margin-bottom: 1.5*@gridGutterWidth;    
+    }
+
     p {
         line-height: 1.7em;    
     }
+
+    .section-img {
+        margin-bottom: 1*@gridGutterWidth;    
+    }
 }
 
 
@@ -451,8 +515,9 @@ div.form-stacked {
 .messages {
     .makeColumn(10);
 
-    margin-bottom: 2em;
+    margin: 2em 0;
     background-color: #ddd;
+    margin-left: 0;
 
     li {
         padding: 1em;    
@@ -538,7 +603,12 @@ table {
 
 
 /*pagination*/
+.pagination {
+    a.page {
+        display: inline !important;
 
+    }    
+}
 
 /*blog styles*/
 .blog-entries {
@@ -554,6 +624,7 @@ table {
     .title {
         margin-bottom: 1em;    
         font-size: 1.1em;
+        line-height: 1.4em;
     }    
 
     .media {
@@ -586,6 +657,7 @@ table {
 .section.twitter-feed {
     
     .tweet {
+        &:last-child { margin-bottom:0; padding-bottom:0; border-bottom: none }
         line-height: 1.3em;
         font-size: 0.9em;
         margin-bottom: @gridGutterWidth;
@@ -601,3 +673,100 @@ table {
         }
     }
 }
+
+.pagination .page {
+    margin-left: 0;    
+}
+
+
+.entry-list {
+    
+    .since {
+        font-size: 0.8em;    
+    }
+
+    .title {
+        margin-bottom: 1em;    
+    }
+
+    .content, .text {
+        margin-bottom: 2em;
+        font-size: 0.8em;
+    }
+}
+
+
+.initial_hidden {
+    display: none;    
+}
+
+/*resources styles*/
+
+@resCol: #FAAF40;
+.resources {
+    
+    .categories {
+        ul { .clearfix() }
+        ul li { float: left; }
+        .title {
+            margin-bottom: @gridGutterWidth/2;    
+        }
+
+        ul li a {
+            text-decoration: none;
+            color: @resCol;
+            margin-right: @gridGutterWidth;
+        }
+    }
+
+    .list {
+        
+        margin-top: 4*@gridGutterWidth;
+
+        .resource {
+            .border-box();
+            width: 33%;
+            padding: 2*@gridGutterWidth;
+            float: left;
+            border: 1px solid @resCol;
+            height: 12*@gridGutterWidth;
+
+            .description {
+                display: none;    
+            }
+        }    
+    }
+    
+}
+
+/* recaptcha */
+#recaptcha_widget_div {
+    margin-top: 10px;
+    margin-left: -4px;
+
+    #recaptcha_instructions_image {
+        font-size: 0.8em;
+        margin-bottom: 10px;
+        display: block !important;
+    }
+}
+
+.checkbox-widget.checked {
+    background-color: #f00;
+    background-image: url("../images/checkbox.png");
+    background-position: 50% 50%;
+}
+
+.checkbox-widget {
+    border: 1px solid @gray;
+    width: 25px;
+    height: 25px;
+    display: block;
+    float: left;
+    cursor: pointer;
+    margin-top: @gridGutterWidth/2 - 2;
+}
+
+
+
diff --git a/snf-astakos-app/astakos/im/static/im/images/dashed_border.png b/snf-astakos-app/astakos/im/static/im/images/dashed_border.png
new file mode 100644 (file)
index 0000000..1973052
Binary files /dev/null and b/snf-astakos-app/astakos/im/static/im/images/dashed_border.png differ
index 6ee580b..aad56a5 100644 (file)
@@ -39,6 +39,18 @@ a.button {
     border: none !important;
 }
 
+// specific styles for A buttons
+a.button {
+    text-align: center !important;
+    color: #fff !important;
+    border: none !important;
+    display: block !important;
+
+    &:hover {
+        color: #fff !important;    
+    }
+}
+
 
 .transit(@type:color, @time:.15s, @easing:linear) {
     -webkit-transition: @type @time @easing;
index 4fb5384..2035706 100644 (file)
@@ -49,11 +49,12 @@ context_processors = [
     'django.core.context_processors.media',
     'django.core.context_processors.request',
     'astakos.im.context_processors.media',
-    'astakos.im.context_processors.cloudbar',
     'astakos.im.context_processors.im_modules',
     'astakos.im.context_processors.next',
     'astakos.im.context_processors.code',
-    'astakos.im.context_processors.invitations'
+    'astakos.im.context_processors.invitations',
+    'astakos.im.context_processors.menu',
+    'synnefo.lib.context_processors.cloudbar'
 ]
 
 middlware_classes = [
index 98765dd..3d16f04 100644 (file)
@@ -36,11 +36,11 @@ from django.shortcuts import redirect
 from django.utils.translation import ugettext as _
 from django.contrib import messages
 from django.utils.http import urlencode
-from django.contrib.auth import login as auth_login, authenticate
-from django.http import HttpResponse
+from django.contrib.auth import login as auth_login, authenticate, logout
+from django.http import HttpResponse, HttpResponseBadRequest
 
 from urllib import quote
-from urlparse import urlunsplit, urlsplit
+from urlparse import urlunsplit, urlsplit, urlparse, parse_qsl
 
 from astakos.im.settings import COOKIE_NAME, COOKIE_DOMAIN
 from astakos.im.util import set_cookie
@@ -51,39 +51,53 @@ logger = logging.getLogger(__name__)
 
 def login(request):
     """
-    If the request user is authenticated, redirects to `next` request parameter
-    if exists, otherwise redirects to astakos index page displaying an error
+    If there is no `next` request parameter redirects to astakos index page displaying an error
     message.
-    If the request user is not authenticated, redirects to login in order to
-    return back here after successful login.
+    If the request user is authenticated, redirects to `next` request parameter.
+    Otherwise, redirects to login in order to return back here after successful login.
     """
+    next = request.GET.get('next')
+    if not next:
+        return HttpResponseBadRequest(_('No next parameter'))
+    force = request.GET.get('force', None)
+    response = HttpResponse()
+    if force == '':
+        logout(request)
+        response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
     if request.user.is_authenticated():
-        next = request.GET.get('next')
         renew = request.GET.get('renew', None)
-        if next:
-            response = HttpResponse()
-            if renew == '':
-                request.user.renew_token()
-                request.user.save()
-                
-                # authenticate before login
-                user = authenticate(email=request.user.email, auth_token=request.user.auth_token)
-                auth_login(request, user)
-                set_cookie(response, user)
-                logger.info('Token reset for %s' % request.user.email)
-            parts = list(urlsplit(next))
-            parts[3] = urlencode({'user': request.user.email, 'token': request.user.auth_token})
-            url = urlunsplit(parts)
-            response['Location'] = url
-            response.status_code = 302
-            return response
-        else:
-            msg = _('No next parameter')
-            messages.add_message(request, messages.ERROR, msg)
-            url = reverse('astakos.im.views.index')
-            return redirect(url)
+        if renew == '':
+            request.user.renew_token()
+            request.user.save()
+            
+            # authenticate before login
+            user = authenticate(email=request.user.email, auth_token=request.user.auth_token)
+            auth_login(request, user)
+            set_cookie(response, user)
+            logger.info('Token reset for %s' % request.user.email)
+        parts = list(urlsplit(next))
+        parts[3] = urlencode({'user': request.user.email, 'token': request.user.auth_token})
+        url = urlunsplit(parts)
+        response['Location'] = url
+        response.status_code = 302
+        return response
     else:
         # redirect to login with self as next
-        url = reverse('astakos.im.views.index')
-        url = '%s?next=%s' % (url, quote(request.build_absolute_uri()))
-        return redirect(url)
+        
+        # first build next parameter
+        parts = list(urlsplit(request.build_absolute_uri()))
+        params = dict(parse_qsl(parts[3], keep_blank_values=True))
+        # delete force parameter
+        if 'force' in params:
+            del params['force']
+        parts[3] = urlencode(params)
+        next = urlunsplit(parts)
+        
+        # build url location
+        parts[2] = reverse('astakos.im.views.index')
+        params = {'next':next}
+        parts[3] = urlencode(params)
+        url = urlunsplit(parts)
+        response['Location'] = url
+        response.status_code = 302
+        return response
\ No newline at end of file
index d651941..ebfb26d 100644 (file)
@@ -1,30 +1,23 @@
 {% extends "im/base_two_cols.html" %}
 
+{% load filters %}
+
 {% block page.title %}Profile{% endblock %}
 {% block page.nav.classes %}{% endblock %}
 
-{% comment %}{% block page.quicknav.items %}
+{% block page.quicknav.items %}
         <li class="{% block signup_class %}{% endblock %}">
             <a href="{% url astakos.im.views.logout %}">LOGOUT</a>
         </li>
-{% endblock %}{% endcomment %}
+{% endblock %}
 
-{% comment %}{% block page.nav.items %}
-    <li class="{% if tab == "im/profile" %}active{% endif %}">
-        <a href="{% url astakos.im.views.edit_profile %}">Profile</a>
-    </li>
-    <li class="{% if not tab %}active{% endif %}">
-        <a href="{% url django.contrib.auth.views.password_change %}">Change password</a>
-    </li>
-    {% if invitations_enabled %}
-    <li class="{% if tab == "im/invitations" %}active{% endif %}">
-        <a href="{% url astakos.im.views.invite %}">Invitations</a>
-    </li>
-    {% endif %}
-    <li class="{% if tab == "im/feedback" %}active{% endif %}">
-        <a href="{% url astakos.im.views.send_feedback %}">Send feedback</a>
-    </li>
-{% endblock %}{% endcomment %}
+{% block page.nav.items %}
+    {% for item in menu%}
+        <li class="{% if item|lookup:"is_active" %}active{% endif %}">
+            <a href="{{ item|lookup:"url" }}">{{ item|lookup:"name" }}</a>
+        </li>
+    {% endfor %}
+{% endblock %}
     
 {% block page.body %}
 <div class="maincol {% block innerpage.class %}full{% endblock %}">
index bee5ea0..8fcbaa8 100644 (file)
           $("label[for=id_recaptcha_challenge_field]").closest('.form-row').hide();
     })
   </script>
-  <script>
-    var CLOUDBAR_LOCATION = "{{ CLOUDBAR_LOC }}";
-    var CLOUDBAR_COOKIE_NAME = "{{ CLOUDBAR_COOKIE_NAME }}";
-    var CLOUDBAR_ACTIVE_SERVICE = '{{ CLOUDBAR_ACTIVE_SERVICE }}';
-    
-    var GET_SERVICES_URL = "{{ GET_SERVICES_URL }}";
-    var GET_MENU_URL = "{{ GET_MENU_URL }}";
 
-    $(document).ready(function(){
-      $.getScript(CLOUDBAR_LOCATION + 'cloudbar.js');
-    })
-  </script>
+    {% if CLOUDBAR_ACTIVE %}
+        {{ CLOUDBAR_CODE }}
+    {% endif %}
 </head>
 
 <body>
                 <h1>accounts</h1>
             </div>
             
-            <ul class="mainnav inline quicknav">
-                {% comment %}{% block page.quicknav.items %}
-                    <li class="{% block signup_class %}{% endblock %}">
-                        <a href="{% url astakos.im.views.index %}">Login</a>
-                    </li>
-                    <li class="{% block signin_class %}{% endblock %}">
-                        <a href="{% url astakos.im.views.signup %}">Sign up</a>
-                    </li>
-                {% endblock %}{% endcomment %}
-            </ul>
-
-            {% comment %}{% block page.nav %}
+            {% block page.nav %}
+            <div class="navigation">
             <ul class="mainnav inline">
                 {% block page.nav.items %}
                 {% endblock %}
             </ul>
-            {% endblock %}{% endcomment %}
+            <ul class="mainnav inline subnav">
+                {% block page.subnav %}{%endblock %}
+            </ul>
+            </div>
+            {% endblock %}
         </div>
+        {% if messages %}
+        <ul class="messages">
+            {% for message in messages %}
+            <li{% if message.tags %} 
+                class="{{ message.tags }}"{% endif %}>
+                {{ message }}</li>
+            {% endfor %}
+        </ul>
+        {% endif %}
         <div class="page">
-            {% if messages %}
-            <ul class="messages">
-                {% for message in messages %}
-                <li{% if message.tags %} 
-                    class="{{ message.tags }}"{% endif %}>
-                    {{ message }}</li>
-                {% endfor %}
-            </ul>
-            {% endif %}
             {% block page.body %}
                 <div class="maincol">
                     {% block body %}
index d611ad2..2a1b6dc 100644 (file)
@@ -13,6 +13,7 @@ Login
 {% endblock body %}
     
 {% block body.right %}
+<div class="section">
      {% if "local" in im_modules %}
         <form action="{% url astakos.im.target.local.login %}" method="post"
             class="login innerlabels">{% csrf_token %}
@@ -26,7 +27,7 @@ Login
         </form>
       {% endif %}
 
-          <div class="section">
+          <div class="extralogin">
               {% for o in im_modules %}
               <div>
             {% if o != 'local' %}
@@ -38,12 +39,15 @@ Login
             {% endfor %}
         </div>
         {% block body.signup %}
-        <div class="section signup">
-            <br /><br />
-                <h2>NEW TO OKEANOS ?</h2>
-                <a class="button" href="{% url astakos.im.views.signup %}">CREATE ACCOUNT</a>
+        {% for o in im_modules %}
+            {% if o != 'local' %}
+            <br />
+            {% endif %}
+        {% endfor %}
+        <div class="bottom">
+                new to okeanos ? <a href="{% url astakos.im.views.signup %}">SIGN UP</a>
             </div>
         </div>
         {% endblock %}
-
+    </div>
 {% endblock %}
diff --git a/snf-astakos-app/astakos/im/templatetags/filters.py b/snf-astakos-app/astakos/im/templatetags/filters.py
new file mode 100644 (file)
index 0000000..9e5d10d
--- /dev/null
@@ -0,0 +1,40 @@
+# Copyright 2011-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 django import template
+
+register = template.Library()
+
+@register.filter
+def lookup(d, key):
+    return d[key]
\ No newline at end of file
index 52cb118..a152b88 100644 (file)
@@ -37,8 +37,8 @@ from astakos.im.forms import ExtendedPasswordResetForm, LoginForm
 from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED
 
 urlpatterns = patterns('astakos.im.views',
-    url(r'^$', 'index'),
-    url(r'^login/?$', 'index'),
+    url(r'^$', 'index', {}, name='index'),
+    url(r'^login/?$', 'index', {}, name='login'),
     url(r'^profile/?$', 'edit_profile'),
     url(r'^feedback/?$', 'send_feedback'),
     url(r'^signup/?$', 'signup', {'on_success':'im/login.html', 'extra_context':{'form':LoginForm()}}),
index 285f460..c82eebb 100644 (file)
@@ -75,7 +75,7 @@ def get_or_create_user(email, realname='', first_name='', last_name='', affiliat
             'password':password,
             'affiliation':affiliation,
             'level':level,
-            'invitations':INVITATIONS_PER_LEVEL[level],
+            'invitations':INVITATIONS_PER_LEVEL.get(level, 0),
             'provider':provider,
             'realname':realname,
             'first_name':first_name,
index 31011e6..36e257f 100644 (file)
@@ -50,6 +50,7 @@ from django.db import transaction
 from django.contrib.auth import logout as auth_logout
 from django.utils.http import urlencode
 from django.http import HttpResponseRedirect
+from django.db.utils import IntegrityError
 
 from astakos.im.models import AstakosUser, Invitation
 from astakos.im.backends import get_backend
@@ -116,9 +117,7 @@ def index(request, login_template_name='im/login.html', profile_template_name='i
     formclass = 'LoginForm'
     kwargs = {}
     if request.user.is_authenticated():
-        template_name = profile_template_name
-        formclass = 'ProfileForm'
-        kwargs.update({'instance':request.user})
+        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
     return render_response(template_name,
                            form = globals()[formclass](**kwargs),
                            context_instance = get_context(request, extra_context))
@@ -177,6 +176,10 @@ def invite(request, template_name='im/invitations.html', extra_context={}):
                 status = messages.ERROR
                 message = getattr(e, 'strerror', '')
                 transaction.rollback()
+            except IntegrityError, e:
+                status = messages.ERROR
+                message = _('There is already invitation for %s' % username)
+                transaction.rollback()
         else:
             status = messages.ERROR
             message = _('No invitations left')
@@ -261,7 +264,7 @@ def signup(request, on_failure='im/signup.html', on_success='im/signup_complete.
     Upon successful user creation if ``next`` url parameter is present the user is redirected there
     otherwise renders the same page with a success message.
     
-    On unsuccessful creation, renders the same page with an error message.
+    On unsuccessful creation, renders ``on_failure`` with an error message.
     
     **Arguments**
     
@@ -405,5 +408,6 @@ def activate(request):
         return HttpResponseBadRequest('No such user')
     
     user.is_active = True
+    user.email_verified = True
     user.save()
     return prepare_response(request, user, next, renew=True)
diff --git a/snf-astakos-app/conf/20-snf-astakos-app-cloudbar.conf b/snf-astakos-app/conf/20-snf-astakos-app-cloudbar.conf
new file mode 100644 (file)
index 0000000..b22e8f8
--- /dev/null
@@ -0,0 +1,7 @@
+#CLOUDBAR_ACTIVE = True
+#CLOUDBAR_LOCATION = '/static/im/cloudbar/'
+#CLOUDBAR_COOKIE_NAME = '_pithos2_a'
+#CLOUDBAR_ACTIVE_SERVICE = 'cloud'
+#CLOUDBAR_SERVICES_URL = '/im/get_services'
+#CLOUDBAR_MENU_URL = '/im/get_menu'
+
index a1e4f27..d49d196 100644 (file)
@@ -59,4 +59,9 @@
 #ASTAKOS_RECAPTCHA_PRIVATE_KEY = ''
 #ASTAKOS_RECAPTCHA_OPTIONS = {'theme':'white'}
 
+# Set where the user should be redirected after logout
+#ASTAKOS_LOGOUT_NEXT = ''
+
+# Set user email patterns that are automatically activated
+#RE_USER_EMAIL_PATTERNS = getattr(settings, 'ASTAKOS_RE_USER_EMAIL_PATTERNS', [])
 
index 1e1a56a..93ccad8 100644 (file)
@@ -77,8 +77,8 @@ CLASSIFIERS = [
 INSTALL_REQUIRES = [
     'Django>=1.2, <1.3',
     'South>=0.7, <=0.7.3',
-    'httplib2==0.6.0',
-    'snf-common>=0.9.0rc',
+    'httplib2>=0.6.0',
+    'snf-common>=0.9.0',
     'recaptcha-client>=1.0.5'
 ]