====================== =========================
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.
+=======
+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
======
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
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
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" })
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__)
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
"""
self.request = request
self.invitation = get_invitation(request)
+ super(InvitationsBackend, self).__init__()
def get_signup_form(self, provider):
"""
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
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
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 \
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
"""
def __init__(self, request):
self.request = request
+ super(SimpleBackend, self).__init__()
def get_signup_form(self, provider):
"""
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'):
"""
"""
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 \
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}
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}
"""
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.
"""
"""
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
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
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
from smtplib import SMTPException
from django.core.management.base import BaseCommand, CommandError
+from django.db.utils import IntegrityError
from astakos.im.functions import invite
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")
class Command(BaseCommand):
- args = "<user_id or email>"
+ args = "<user ID or email>"
help = "Modify a user's attributes"
option_list = BaseCommand.option_list + (
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',
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,
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:
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)
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()
class Command(BaseCommand):
+ args = "<invitation ID>"
help = "Show invitation info"
def handle(self, *args, **options):
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:
--- /dev/null
+# 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']
--- /dev/null
+# 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']
#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)
# 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)
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)
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)
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
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.
font-weight: bold !important;
font-size: 12px !important;
}
+.servicesbar .profile .user.active {
+ background-color: #333;
+}
+
.servicesbar .profile a {
float: none;
}
// 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();
font-weight: bold !important;
font-size: 12px !important;
}
+
+ &.active {
+ background-color: #333;
+ }
}
a {
float: none;
--- /dev/null
+/**
+* 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
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;
.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*/
/*
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;
section a,
p a,
form a,
-.section a {
+.section a,
+.styledlinks a {
color: #000000;
text-decoration: none;
border-bottom: 1px solid #f89a1c;
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;
}
div.header {
position: relative;
- margin-top: 42px;
+ margin-top: 88px;
margin-bottom: 22px;
}
div.header h1 {
border-bottom: 1px solid #cfcdc7;
padding-bottom: 3px;
}
+.mainlogo {
+ height: 36px;
+}
+.mainlogo h1 {
+ height: 36px;
+}
.mainlogo img {
margin-left: -10px;
}
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;
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 {
.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;
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;
border: 1px solid #000;
z-index: 100;
}
+#forms .input:focus label, #forms input:focus label {
+ z-index: 300;
+}
.altcol {
background-color: #c3c3b9 !important;
}
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;
float: left;
}
.section.positioned h3 {
+ font-size: 1.2em;
margin-bottom: 22px;
}
.section.positioned .text {
}
input[readonly=true] {
background-color: #ddd;
- color: #2b2b2b;
+ color: #5e5e5e;
}
form.withlabels label {
width: 224px;
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;
position: relative;
}
form .form-row.submit {
- margin-top: 22px;
+ margin-top: 33px;
}
form .form-row .extra-link {
color: #808080;
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;
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;
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;
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;
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;
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;
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;
float: left;
}
.blog-entry.positioned h3 {
+ font-size: 1.2em;
margin-bottom: 22px;
}
.blog-entry.positioned .text {
}
.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: "";
.blog-entry .title {
margin-bottom: 1em;
font-size: 1.1em;
+ line-height: 1.4em;
}
.blog-entry .media img {
border: 1px solid #808080;
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;
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;
+}
@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 {
}
// 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;
}
}
+a.simple {
+ border: none;
+
+}
a.action {
color: @linkColor;
border-bottom: none;
div.header {
position: relative;
- margin-top: 42px;
+ margin-top: 4*@gridGutterWidth;
margin-bottom: @gridGutterWidth;
h1 {
color: @shadowColor;
}
}
.mainlogo {
+ height: 36px;
+
+ h1 { height: 36px }
img {
margin-left: -10px;
}
}
}
+.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;
}
}
}
}
-.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;
}
}
-
-/* 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;
+ }
}
}
}
}
.section {
+
+ em {
+ color: @blue;
+ }
&.positioned {
-
+ margin-bottom: 0;
.content {
.makeColumn(4);
}
}
h3 {
+ font-size: 1.2em;
margin-bottom: @gridGutterWidth;
}
}
}
- margin-bottom: 2em;
+ margin-bottom: 3*@gridGutterWidth;
.left, .right {
width: 50%;
input[readonly=true] {
background-color: #ddd;
- color: darken(#ddd, 70%);
+ color: darken(#ddd, 50%);
}
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 {
min-height: 2*@gridGutterWidth;
position: relative;
&.submit {
- margin-top: @gridGutterWidth;
+ margin-top: 1.5*@gridGutterWidth;
}
.extra-link {
}
+ textarea {
+ height: 200px;
+ width: 350px !important;
+ }
.with-errors {
input, textarea, select {
color: @red;
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;
+ }
}
.messages {
.makeColumn(10);
- margin-bottom: 2em;
+ margin: 2em 0;
background-color: #ddd;
+ margin-left: 0;
li {
padding: 1em;
/*pagination*/
+.pagination {
+ a.page {
+ display: inline !important;
+ }
+}
/*blog styles*/
.blog-entries {
.title {
margin-bottom: 1em;
font-size: 1.1em;
+ line-height: 1.4em;
}
.media {
.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;
}
}
}
+
+.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;
+}
+
+
+
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;
'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 = [
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
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
{% 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 %}">
$("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 %}
{% 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 %}
</form>
{% endif %}
- <div class="section">
+ <div class="extralogin">
{% for o in im_modules %}
<div>
{% if o != 'local' %}
{% 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 %}
--- /dev/null
+# 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
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()}}),
'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,
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
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))
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')
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**
return HttpResponseBadRequest('No such user')
user.is_active = True
+ user.email_verified = True
user.save()
return prepare_response(request, user, next, renew=True)
--- /dev/null
+#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'
+
#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', [])
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'
]