Revision 18ffbee1

b/docs/source/devguide.rst
165 165

  
166 166
::
167 167

  
168
  {"username": "4ad9f34d6e7a4992b34502d40f40cb",
169
  "uniq": "papagian@example.com"
170
  "auth_token": "0000",
171
  "auth_token_expires": "Tue, 11-Sep-2012 09:17:14 ",
172
  "auth_token_created": "Sun, 11-Sep-2011 09:17:14 ",
173
  "has_credits": false,
174
  "has_signed_terms": true}
168
  {"userid": "270d191e09834408b7af65885f46a3",
169
  "email": ["user111@example.com"],
170
  "name": "user1 User1",
171
  "auth_token_created": 1333372365000,
172
  "auth_token_expires": 1335964365000,
173
  "auth_token": "uiWDLAgtJOGW4mI4q9R/8w==",
174
  "has_credits": true}
175 175

  
176 176
|
177 177

  
b/snf-astakos-app/README
83 83
===============  ===========================
84 84
Name             Description
85 85
===============  ===========================
86
activateuser     Activates one or more users
86
addgroup         Add new group
87
addterms         Add new approval terms
87 88
createuser       Create a user
88 89
inviteuser       Invite a user
90
listgroups       List groups
89 91
listinvitations  List invitations
90 92
listusers        List users
91 93
modifyuser       Modify a user's attributes
94
sendactivation   Send activation email
92 95
showinvitation   Show invitation info
93 96
showuser         Show user info
94
addterms         Add new approval terms
95 97
===============  ===========================
98

  
99
To update user credibility from the billing system (Aquarium), enable the queue, install snf-pithos-tools and use ``pithos-dispatcher``::
100

  
101
    pithos-dispatcher --exchange=aquarium --callback=astakos.im.queue.listener.on_creditevent
102

  
103
Load groups:
104
------------
105

  
106
To set the initial user groups load the followind fixture:
107

  
108
    snf-manage loaddata groups
b/snf-astakos-app/astakos/im/activation_backends.py
35 35
from django.core.exceptions import ImproperlyConfigured
36 36
from django.core.mail import send_mail
37 37
from django.template.loader import render_to_string
38
from django.utils.translation import ugettext as _
39 38
from django.contrib.sites.models import Site
40 39
from django.contrib import messages
41
from django.db import transaction
42 40
from django.core.urlresolvers import reverse
41
from django.utils.translation import ugettext as _
42
from django.db import transaction
43 43

  
44 44
from urlparse import urljoin
45 45

  
......
57 57

  
58 58
def get_backend(request):
59 59
    """
60
    Returns an instance of a registration backend,
60
    Returns an instance of an activation backend,
61 61
    according to the INVITATIONS_ENABLED setting
62 62
    (if True returns ``astakos.im.activation_backends.InvitationsBackend`` and if False
63 63
    returns ``astakos.im.activation_backends.SimpleBackend``).
......
71 71
    try:
72 72
        mod = import_module(module)
73 73
    except ImportError, e:
74
        raise ImproperlyConfigured('Error loading registration backend %s: "%s"' % (module, e))
74
        raise ImproperlyConfigured('Error loading activation backend %s: "%s"' % (module, e))
75 75
    try:
76 76
        backend_class = getattr(mod, backend_class_name)
77 77
    except AttributeError:
78
        raise ImproperlyConfigured('Module "%s" does not define a registration backend named "%s"' % (module, attr))
78
        raise ImproperlyConfigured('Module "%s" does not define a activation backend named "%s"' % (module, attr))
79 79
    return backend_class(request)
80 80

  
81 81
class SignupBackend(object):
......
88 88

  
89 89
class InvitationsBackend(SignupBackend):
90 90
    """
91
    A registration backend which implements the following workflow: a user
91
    A activation backend which implements the following workflow: a user
92 92
    supplies the necessary registation information, if the request contains a valid
93 93
    inivation code the user is automatically activated otherwise an inactive user
94 94
    account is created and the user is going to receive an email as soon as an
......
110 110
        invitation = self.invitation
111 111
        initial_data = self.get_signup_initial_data(provider)
112 112
        prefix = 'Invited' if invitation else ''
113
        main = provider.capitalize() if provider == 'local' else 'ThirdParty'
113
        main = provider.capitalize()
114 114
        suffix  = 'UserCreationForm'
115 115
        formclass = '%s%s%s' % (prefix, main, suffix)
116 116
        ip = self.request.META.get('REMOTE_ADDR',
......
119 119

  
120 120
    def get_signup_initial_data(self, provider):
121 121
        """
122
        Returns the necassary registration form depending the user is invited or not
122
        Returns the necassary activation form depending the user is invited or not
123 123

  
124 124
        Throws Invitation.DoesNotExist in case ``code`` is not valid.
125 125
        """
......
131 131
                # create a tmp user with the invitation realname
132 132
                # to extract first and last name
133 133
                u = AstakosUser(realname = invitation.realname)
134
                print '>>>', invitation, invitation.inviter
134 135
                initial_data = {'email':invitation.username,
135 136
                                'inviter':invitation.inviter.realname,
136 137
                                'first_name':u.first_name,
......
155 156
            return True
156 157
        return False
157 158

  
158
    @transaction.commit_manually
159 159
    def handle_activation(self, user, verification_template_name='im/activation_email.txt', greeting_template_name='im/welcome_email.txt', admin_email_template_name='im/admin_notification.txt'):
160 160
        """
161 161
        Initially creates an inactive user account. If the user is preaccepted
......
166 166
        The method uses commit_manually decorator in order to ensure the user
167 167
        will be created only if the procedure has been completed successfully.
168 168
        """
169
        result = None
170 169
        try:
170
            if user.is_active:
171
                return RegistationCompleted()
171 172
            if self._is_preaccepted(user):
172 173
                if user.email_verified:
173 174
                    activate(user, greeting_template_name)
174
                    result = RegistationCompleted()
175
                    return RegistationCompleted()
175 176
                else:
176 177
                    send_verification(user, verification_template_name)
177
                    result = VerificationSent()
178
                    return VerificationSent()
178 179
            else:
179 180
                send_admin_notification(user, admin_email_template_name)
180
                result = NotificationSent()
181
                return NotificationSent()
181 182
        except Invitation.DoesNotExist, e:
182 183
            raise InvitationCodeError()
183
        else:
184
            return result
184
        except BaseException, e:
185
            logger.exception(e)
186
            raise e
185 187

  
186 188
class SimpleBackend(SignupBackend):
187 189
    """
188
    A registration backend which implements the following workflow: a user
190
    A activation backend which implements the following workflow: a user
189 191
    supplies the necessary registation information, an incative user account is
190 192
    created and receives an email in order to activate his/her account.
191 193
    """
......
238 240
        * DEFAULT_CONTACT_EMAIL: service support email
239 241
        * DEFAULT_FROM_EMAIL: from email
240 242
        """
241
        result = None
242
        if not self._is_preaccepted(user):
243
            send_admin_notification(user, admin_email_template_name)
244
            result = NotificationSent()
243
        try:
244
            if user.is_active:
245
                return RegistrationCompeted()
246
            if not self._is_preaccepted(user):
247
                send_admin_notification(user, admin_email_template_name)
248
                return NotificationSent()
249
            else:
250
                send_verification(user, email_template_name)
251
                return VerificationSend()
252
        except SendEmailError, e:
253
            transaction.rollback()
254
            raise e
255
        except BaseException, e:
256
            logger.exception(e)
257
            raise e
245 258
        else:
246
            send_verification(user, email_template_name)
247
            result = VerificationSend()
248
        return result
259
            transaction.commit()
249 260

  
250 261
class ActivationResult(object):
251 262
    def __init__(self, message):
......
266 277
class RegistationCompleted(ActivationResult):
267 278
    def __init__(self):
268 279
        message = _('Registration completed. You can now login.')
269
        super(RegistationCompleted, self).__init__(message)
270

  
271

  
280
        super(RegistationCompleted, self).__init__(message)
b/snf-astakos-app/astakos/im/api.py
33 33

  
34 34
import logging
35 35

  
36
from functools import wraps
36 37
from traceback import format_exc
37 38
from time import time, mktime
38 39
from urllib import quote
......
43 44
from django.utils import simplejson as json
44 45
from django.core.urlresolvers import reverse
45 46

  
46
from astakos.im.faults import BadRequest, Unauthorized, InternalServerError
47
from astakos.im.faults import BadRequest, Unauthorized, InternalServerError, Fault
47 48
from astakos.im.models import AstakosUser
48 49
from astakos.im.settings import CLOUD_SERVICES, INVITATIONS_ENABLED
49
from astakos.im.util import has_signed_terms
50
from astakos.im.util import has_signed_terms, epoch
50 51

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

  
......
62 63
    response['Content-Length'] = len(response.content)
63 64
    return response
64 65

  
65
def authenticate(request):
66
def api_method(http_method=None, token_required=False, perms=[]):
67
    """Decorator function for views that implement an API method."""
68
    
69
    def decorator(func):
70
        @wraps(func)
71
        def wrapper(request, *args, **kwargs):
72
            try:
73
                if http_method and request.method != http_method:
74
                    raise BadRequest('Method not allowed.')
75
                x_auth_token = request.META.get('HTTP_X_AUTH_TOKEN')
76
                if token_required:
77
                    if not x_auth_token:
78
                        raise Unauthorized('Access denied')
79
                    try:
80
                        user = AstakosUser.objects.get(auth_token=x_auth_token)
81
                        if not user.has_perms(perms):
82
                            raise Unauthorized('Unauthorized request')
83
                    except AstakosUser.DoesNotExist, e:
84
                        raise Unauthorized('Invalid X-Auth-Token')
85
                    kwargs['user'] = user
86
                response = func(request, *args, **kwargs)
87
                return response
88
            except Fault, fault:
89
                return render_fault(request, fault)
90
            except BaseException, e:
91
                logger.exception('Unexpected error: %s' % e)
92
                fault = InternalServerError('Unexpected error')
93
                return render_fault(request, fault)
94
        return wrapper
95
    return decorator
96

  
97
@api_method(http_method='GET', token_required=True)
98
def authenticate_old(request, user=None):
66 99
    # Normal Response Codes: 204
67 100
    # Error Response Codes: internalServerError (500)
68 101
    #                       badRequest (400)
69 102
    #                       unauthorised (401)
70
    try:
71
        if request.method != 'GET':
72
            raise BadRequest('Method not allowed.')
73
        x_auth_token = request.META.get('HTTP_X_AUTH_TOKEN')
74
        if not x_auth_token:
75
            return render_fault(request, BadRequest('Missing X-Auth-Token'))
76

  
77
        try:
78
            user = AstakosUser.objects.get(auth_token=x_auth_token)
79
        except AstakosUser.DoesNotExist, e:
80
            return render_fault(request, Unauthorized('Invalid X-Auth-Token'))
81

  
82
        # Check if the is active.
83
        if not user.is_active:
84
            return render_fault(request, Unauthorized('User inactive'))
85

  
86
        # Check if the token has expired.
87
        if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
88
            return render_fault(request, Unauthorized('Authentication expired'))
89
        
90
        if not has_signed_terms(user):
91
            return render_fault(request, Unauthorized('Pending approval terms'))
92
        
93
        response = HttpResponse()
94
        response.status=204
95
        user_info = {'username':user.username,
96
                     'uniq':user.email,
97
                     'auth_token':user.auth_token,
98
                     'auth_token_created':user.auth_token_created.isoformat(),
99
                     'auth_token_expires':user.auth_token_expires.isoformat(),
100
                     'has_credits':user.has_credits,
101
                     'has_signed_terms':has_signed_terms(user)}
102
        response.content = json.dumps(user_info)
103
        response['Content-Type'] = 'application/json; charset=UTF-8'
104
        response['Content-Length'] = len(response.content)
105
        return response
106
    except BaseException, e:
107
        logger.exception(e)
108
        fault = InternalServerError('Unexpected error')
109
        return render_fault(request, fault)
103
    if not user:
104
        raise BadRequest('No user')
105
    
106
    # Check if the is active.
107
    if not user.is_active:
108
        raise Unauthorized('User inactive')
110 109

  
111
def get_services(request):
112
    if request.method != 'GET':
113
        raise BadRequest('Method not allowed.')
110
    # Check if the token has expired.
111
    if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
112
        raise Unauthorized('Authentication expired')
113
    
114
    if not has_signed_terms(user):
115
        raise Unauthorized('Pending approval terms')
116
    
117
    response = HttpResponse()
118
    response.status=204
119
    user_info = {'username':user.username,
120
                 'uniq':user.email,
121
                 'auth_token':user.auth_token,
122
                 'auth_token_created':user.auth_token_created.isoformat(),
123
                 'auth_token_expires':user.auth_token_expires.isoformat(),
124
                 'has_credits':user.has_credits,
125
                 'has_signed_terms':has_signed_terms(user)}
126
    response.content = json.dumps(user_info)
127
    response['Content-Type'] = 'application/json; charset=UTF-8'
128
    response['Content-Length'] = len(response.content)
129
    return response
130

  
131
@api_method(http_method='GET', token_required=True)
132
def authenticate(request, user=None):
133
    # Normal Response Codes: 204
134
    # Error Response Codes: internalServerError (500)
135
    #                       badRequest (400)
136
    #                       unauthorised (401)
137
    if not user:
138
        raise BadRequest('No user')
139
    
140
    # Check if the is active.
141
    if not user.is_active:
142
        raise Unauthorized('User inactive')
143

  
144
    # Check if the token has expired.
145
    if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
146
        raise Unauthorized('Authentication expired')
147
    
148
    if not has_signed_terms(user):
149
        raise Unauthorized('Pending approval terms')
150
    
151
    response = HttpResponse()
152
    response.status=204
153
    user_info = {'userid':user.username,
154
                 'email':[user.email],
155
                 'name':user.realname,
156
                 'auth_token':user.auth_token,
157
                 'auth_token_created':epoch(user.auth_token_created),
158
                 'auth_token_expires':epoch(user.auth_token_expires),
159
                 'has_credits':user.has_credits,
160
                 'is_active':user.is_active,
161
                 'groups':[g.name for g in user.groups.all()]}
162
    response.content = json.dumps(user_info)
163
    response['Content-Type'] = 'application/json; charset=UTF-8'
164
    response['Content-Length'] = len(response.content)
165
    return response
114 166

  
167
@api_method(http_method='GET')
168
def get_services(request):
115 169
    callback = request.GET.get('callback', None)
116 170
    data = json.dumps(CLOUD_SERVICES)
117 171
    mimetype = 'application/json'
......
122 176

  
123 177
    return HttpResponse(content=data, mimetype=mimetype)
124 178

  
179
@api_method()
125 180
def get_menu(request, with_extra_links=False, with_signout=True):
126 181
    location = request.GET.get('location', '')
127 182
    exclude = []
......
144 199
        l.append({ 'url': absolute(reverse('astakos.im.views.edit_profile')),
145 200
                  'name': "My account" })
146 201
        if with_extra_links:
147
            if request.user.password:
202
            if request.user.has_usable_password():
148 203
                l.append({ 'url': absolute(reverse('password_change')),
149 204
                          'name': "Change password" })
150 205
            if INVITATIONS_ENABLED:
......
165 220
        data = '%s(%s)' % (callback, data)
166 221

  
167 222
    return HttpResponse(content=data, mimetype=mimetype)
223

  
224
@api_method(http_method='GET', token_required=True, perms=['astakos.im.can_find_userid'])
225
def find_userid(request):
226
    # Normal Response Codes: 204
227
    # Error Response Codes: internalServerError (500)
228
    #                       badRequest (400)
229
    #                       unauthorised (401)
230
    email = request.GET.get('email')
231
    if not email:
232
        raise BadRequest('Email missing')
233
    try:
234
        user = AstakosUser.objects.get(email = email)
235
    except AstakosUser.DoesNotExist, e:
236
        raise BadRequest('Invalid email')
237
    else:
238
        response = HttpResponse()
239
        response.status=204
240
        user_info = {'userid':user.username}
241
        response.content = json.dumps(user_info)
242
        response['Content-Type'] = 'application/json; charset=UTF-8'
243
        response['Content-Length'] = len(response.content)
244
        return response
245

  
246
@api_method(http_method='GET', token_required=True, perms=['astakos.im.can_find_email'])
247
def find_email(request):
248
    # Normal Response Codes: 204
249
    # Error Response Codes: internalServerError (500)
250
    #                       badRequest (400)
251
    #                       unauthorised (401)
252
    userid = request.GET.get('userid')
253
    if not userid:
254
        raise BadRequest('Userid missing')
255
    try:
256
        user = AstakosUser.objects.get(username = userid)
257
    except AstakosUser.DoesNotExist, e:
258
        raise BadRequest('Invalid userid')
259
    else:
260
        response = HttpResponse()
261
        response.status=204
262
        user_info = {'userid':user.email}
263
        response.content = json.dumps(user_info)
264
        response['Content-Type'] = 'application/json; charset=UTF-8'
265
        response['Content-Length'] = len(response.content)
266
        return response
b/snf-astakos-app/astakos/im/fixtures/groups.json
1
[
2
    {
3
        "model": "auth.group",
4
        "pk": 1,
5
        "fields": {
6
            "name": "default"
7
        }
8
    },
9
    {
10
        "model": "auth.group",
11
        "pk": 2,
12
        "fields": {
13
            "name": "academic"
14
        }
15
    },
16
    {
17
        "model": "auth.group",
18
        "pk": 3,
19
        "fields": {
20
            "name": "shibboleth"
21
        }
22
    }
23
]
b/snf-astakos-app/astakos/im/forms.py
1 1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
# 
2
#
3 3
# Redistribution and use in source and binary forms, with or
4 4
# without modification, are permitted provided that the following
5 5
# conditions are met:
6
# 
6
#
7 7
#   1. Redistributions of source code must retain the above
8 8
#      copyright notice, this list of conditions and the following
9 9
#      disclaimer.
10
# 
10
#
11 11
#   2. Redistributions in binary form must reproduce the above
12 12
#      copyright notice, this list of conditions and the following
13 13
#      disclaimer in the documentation and/or other materials
14 14
#      provided with the distribution.
15
# 
15
#
16 16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
......
25 25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 27
# POSSIBILITY OF SUCH DAMAGE.
28
# 
28
#
29 29
# The views and conclusions contained in the software and
30 30
# documentation are those of the authors and should not be
31 31
# interpreted as representing official policies, either expressed
......
42 42
from django.utils.http import int_to_base36
43 43
from django.core.urlresolvers import reverse
44 44
from django.utils.functional import lazy
45
from django.utils.safestring import mark_safe
45 46

  
46 47
from astakos.im.models import AstakosUser, Invitation
47 48
from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, RECAPTCHA_ENABLED
......
58 59
class LocalUserCreationForm(UserCreationForm):
59 60
    """
60 61
    Extends the built in UserCreationForm in several ways:
61
    
62

  
62 63
    * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
63 64
    * The username field isn't visible and it is assigned a generated id.
64
    * User created is not active. 
65
    * User created is not active.
65 66
    """
66 67
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
67 68
    recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
68
    
69

  
69 70
    class Meta:
70 71
        model = AstakosUser
71 72
        fields = ("email", "first_name", "last_name", "has_signed_terms")
72 73
        widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
73
    
74

  
74 75
    def __init__(self, *args, **kwargs):
75 76
        """
76 77
        Changes the order of fields, and removes the username field.
......
86 87
        if RECAPTCHA_ENABLED:
87 88
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
88 89
                                         'recaptcha_response_field',])
89
    
90

  
91
        if 'has_signed_terms' in self.fields:
92
            # Overriding field label since we need to apply a link
93
            # to the terms within the label
94
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
95
                    % (reverse('latest_terms'), _("the terms"))
96
            self.fields['has_signed_terms'].label = \
97
                    mark_safe("I agree with %s" % terms_link_html)
98

  
90 99
    def clean_email(self):
91 100
        email = self.cleaned_data['email']
92 101
        if not email:
......
96 105
            raise forms.ValidationError(_("This email is already used"))
97 106
        except AstakosUser.DoesNotExist:
98 107
            return email
99
    
108

  
100 109
    def clean_has_signed_terms(self):
101 110
        has_signed_terms = self.cleaned_data['has_signed_terms']
102 111
        if not has_signed_terms:
103 112
            raise forms.ValidationError(_('You have to agree with the terms'))
104 113
        return has_signed_terms
105
    
114

  
106 115
    def clean_recaptcha_response_field(self):
107 116
        if 'recaptcha_challenge_field' in self.cleaned_data:
108 117
            self.validate_captcha()
......
119 128
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
120 129
        if not check.is_valid:
121 130
            raise forms.ValidationError(_('You have not entered the correct words'))
122
    
131

  
123 132
    def save(self, commit=True):
124 133
        """
125 134
        Saves the email, first_name and last_name properties, after the normal
......
127 136
        """
128 137
        user = super(LocalUserCreationForm, self).save(commit=False)
129 138
        user.renew_token()
130
        user.date_signed_terms = datetime.now()
131 139
        if commit:
132 140
            user.save()
133 141
        logger.info('Created user %s', user)
......
137 145
    """
138 146
    Extends the LocalUserCreationForm: adds an inviter readonly field.
139 147
    """
140
    
148

  
141 149
    inviter = forms.CharField(widget=forms.TextInput(), label=_('Inviter Real Name'))
142
    
150

  
143 151
    class Meta:
144 152
        model = AstakosUser
145 153
        fields = ("email", "first_name", "last_name", "has_signed_terms")
146 154
        widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
147
    
155

  
148 156
    def __init__(self, *args, **kwargs):
149 157
        """
150 158
        Changes the order of fields, and removes the username field.
151 159
        """
152 160
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
153
        
161

  
154 162
        #set readonly form fields
155 163
        self.fields['inviter'].widget.attrs['readonly'] = True
156 164
        self.fields['email'].widget.attrs['readonly'] = True
157 165
        self.fields['username'].widget.attrs['readonly'] = True
158
    
166

  
159 167
    def save(self, commit=True):
160 168
        user = super(InvitedLocalUserCreationForm, self).save(commit=False)
161 169
        level = user.invitation.inviter.level + 1
......
166 174
            user.save()
167 175
        return user
168 176

  
169
class ThirdPartyUserCreationForm(UserCreationForm):
177
class ThirdPartyUserCreationForm(forms.ModelForm):
170 178
    class Meta:
171 179
        model = AstakosUser
172
        fields = ("email", "has_signed_terms")
180
        fields = ("email", "first_name", "last_name", "third_party_identifier",
181
                  "has_signed_terms", "provider")
173 182
        widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))}
174 183
    
175 184
    def __init__(self, *args, **kwargs):
......
177 186
        Changes the order of fields, and removes the username field.
178 187
        """
179 188
        if 'ip' in kwargs:
180
            self.ip = kwargs['ip']
181 189
            kwargs.pop('ip')
182 190
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
183
        self.fields.keyOrder = ['email']
191
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
192
                                'provider', 'third_party_identifier']
184 193
        if get_latest_terms():
185 194
            self.fields.keyOrder.append('has_signed_terms')
195
        #set readonly form fields
196
        ro = ["provider", "third_party_identifier", "first_name", "last_name"]
197
        for f in ro:
198
            self.fields[f].widget.attrs['readonly'] = True
199
        
200
        if 'has_signed_terms' in self.fields:
201
            # Overriding field label since we need to apply a link
202
            # to the terms within the label
203
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
204
                    % (reverse('latest_terms'), _("the terms"))
205
            self.fields['has_signed_terms'].label = \
206
                    mark_safe("I agree with %s" % terms_link_html)
207
    
208
    def clean_email(self):
209
        email = self.cleaned_data['email']
210
        if not email:
211
            raise forms.ValidationError(_("This field is required"))
212
        try:
213
            AstakosUser.objects.get(email = email)
214
            raise forms.ValidationError(_("This email is already used"))
215
        except AstakosUser.DoesNotExist:
216
            return email
217
    
218
    def clean_has_signed_terms(self):
219
        has_signed_terms = self.cleaned_data['has_signed_terms']
220
        if not has_signed_terms:
221
            raise forms.ValidationError(_('You have to agree with the terms'))
222
        return has_signed_terms
186 223
    
187 224
    def save(self, commit=True):
188 225
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
189 226
        user.set_unusable_password()
227
        user.renew_token()
190 228
        if commit:
191 229
            user.save()
192 230
        logger.info('Created user %s', user)
......
198 236
        #set readonly form fields
199 237
        self.fields['email'].widget.attrs['readonly'] = True
200 238

  
239
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
240
    def clean_email(self):
241
        email = self.cleaned_data['email']
242
        if not email:
243
            raise forms.ValidationError(_("This field is required"))
244
        try:
245
            user = AstakosUser.objects.get(email = email)
246
            if user.provider == 'local':
247
                self.instance = user
248
                return email
249
            else:
250
                raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
251
        except AstakosUser.DoesNotExist:
252
            return email
253
    
201 254
class LoginForm(AuthenticationForm):
202 255
    username = forms.EmailField(label=_("Email"))
203 256

  
......
205 258
    """
206 259
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
207 260
    Most of the fields are readonly since the user is not allowed to change them.
208
    
261

  
209 262
    The class defines a save method which sets ``is_verified`` to True so as the user
210 263
    during the next login will not to be redirected to profile page.
211 264
    """
212 265
    renew = forms.BooleanField(label='Renew token', required=False)
213
    
266

  
214 267
    class Meta:
215 268
        model = AstakosUser
216
        fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires')
217
    
269
        fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires', 'groups')
270

  
218 271
    def __init__(self, *args, **kwargs):
219 272
        super(ProfileForm, self).__init__(*args, **kwargs)
220 273
        instance = getattr(self, 'instance', None)
221
        ro_fields = ('auth_token', 'auth_token_expires', 'email')
274
        ro_fields = ('auth_token', 'auth_token_expires', 'groups')
222 275
        if instance and instance.id:
223 276
            for field in ro_fields:
224 277
                self.fields[field].widget.attrs['readonly'] = True
225
    
278

  
226 279
    def save(self, commit=True):
227 280
        user = super(ProfileForm, self).save(commit=False)
228 281
        user.is_verified = True
......
244 297
    """
245 298
    Form for sending an invitations
246 299
    """
247
    
300

  
248 301
    email = forms.EmailField(required = True, label = 'Email address')
249 302
    first_name = forms.EmailField(label = 'First name')
250 303
    last_name = forms.EmailField(label = 'Last name')
......
253 306
    """
254 307
    Extends PasswordResetForm by overriding save method:
255 308
    passes a custom from_email in send_mail.
256
    
309

  
257 310
    Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
258 311
    accepts a from_email argument.
259 312
    """
......
263 316
        Generates a one-use only link for resetting password and sends to the user.
264 317
        """
265 318
        for user in self.users_cache:
266
            url = urljoin(BASEURL,
267
                          '/im/local/reset/confirm/%s-%s' %(int_to_base36(user.id),
268
                                                            token_generator.make_token(user)))
319
            url = reverse('django.contrib.auth.views.password_reset_confirm',
320
                          kwargs={'uidb36':int_to_base36(user.id),
321
                                  'token':token_generator.make_token(user)})
322
            url = request.build_absolute_uri(url)
269 323
            t = loader.get_template(email_template_name)
270 324
            c = {
271 325
                'email': user.email,
......
283 337
    class Meta:
284 338
        model = AstakosUser
285 339
        fields = ("has_signed_terms",)
286
    
340

  
287 341
    def __init__(self, *args, **kwargs):
288 342
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
289
    
343

  
290 344
    def clean_has_signed_terms(self):
291 345
        has_signed_terms = self.cleaned_data['has_signed_terms']
292 346
        if not has_signed_terms:
293 347
            raise forms.ValidationError(_('You have to agree with the terms'))
294 348
        return has_signed_terms
295
    
296
    def save(self, commit=True):
297
        """
298
        Updates date_signed_terms & has_signed_terms fields.
299
        """
300
        user = super(SignApprovalTermsForm, self).save(commit=False)
301
        user.date_signed_terms = datetime.now()
302
        if commit:
303
            user.save()
304
        return user
305 349

  
306 350
class InvitationForm(forms.ModelForm):
307 351
    username = forms.EmailField(label=_("Email"))
308 352
    
353
    def __init__(self, *args, **kwargs):
354
        super(InvitationForm, self).__init__(*args, **kwargs)
355
    
309 356
    class Meta:
310 357
        model = Invitation
311 358
        fields = ('username', 'realname')
......
317 364
            raise forms.ValidationError(_('There is already invitation for this email.'))
318 365
        except Invitation.DoesNotExist:
319 366
            pass
320
        return username
367
        return username
b/snf-astakos-app/astakos/im/functions.py
170 170
    
171 171
    Raises SendInvitationError
172 172
    """
173
    invitation.inviter = inviter
174
    invitation.save()
173 175
    send_invitation(invitation, email_template_name)
174 176
    inviter.invitations = max(0, inviter.invitations - 1)
175 177
    inviter.save()
......
183 185
        logger.exception(e)
184 186

  
185 187
class SendMailError(Exception):
186
    def __init__(self, message):
187
        Exception.__init__(self)
188
    pass
188 189

  
189 190
class SendAdminNotificationError(SendMailError):
190 191
    def __init__(self):
191 192
        self.message = _('Failed to send notification')
192
        SendMailError.__init__(self)
193
        super(SendAdminNotificationError, self).__init__()
193 194

  
194
class SendVerificationError(Exception):
195
class SendVerificationError(SendMailError):
195 196
    def __init__(self):
196 197
        self.message = _('Failed to send verification')
197
        SendMailError.__init__(self)
198
        super(SendVerificationError, self).__init__()
198 199

  
199
class SendInvitationError(Exception):
200
class SendInvitationError(SendMailError):
200 201
    def __init__(self):
201 202
        self.message = _('Failed to send invitation')
202
        SendMailError.__init__(self)
203
        super(SendInvitationError, self).__init__()
203 204

  
204
class SendGreetingError(Exception):
205
class SendGreetingError(SendMailError):
205 206
    def __init__(self):
206 207
        self.message = _('Failed to send greeting')
207
        SendMailError.__init__(self)
208
        super(SendGreetingError, self).__init__()
208 209

  
209
class SendFeedbackError(Exception):
210
class SendFeedbackError(SendMailError):
210 211
    def __init__(self):
211 212
        self.message = _('Failed to send feedback')
212
        SendMailError.__init__(self)
213
        super(SendFeedbackError, self).__init__()
b/snf-astakos-app/astakos/im/management/commands/addgroup.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

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

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

  
43
from django.contrib.auth.models import Group
44

  
45
class Command(BaseCommand):
46
    args = "<name>"
47
    help = "Insert group"
48
    
49
    def handle(self, *args, **options):
50
        if len(args) != 1:
51
            raise CommandError("Invalid number of arguments")
52
        
53
        name = args[0].decode('utf8')
54
        
55
        try:
56
            Group.objects.get(name=name)
57
            raise CommandError("A group with this name already exists")
58
        except Group.DoesNotExist, e:
59
            group = Group(name=name)
60
            group.save()
61
        
62
        msg = "Created group id %d" % (group.id,)
63
        self.stdout.write(msg + '\n')
b/snf-astakos-app/astakos/im/management/commands/createuser.py
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.
33 33

  
34
import socket
35

  
34 36
from optparse import make_option
35 37
from random import choice
36 38
from string import digits, lowercase, uppercase
......
97 99
        if options['admin']:
98 100
            user.is_admin = True
99 101
        
100
        user.save()
101
        
102
        msg = "Created user id %d" % (user.id,)
103
        if options['password'] is None:
104
            msg += " with password '%s'" % (password,)
105
        self.stdout.write(msg + '\n')
102
        try:
103
            user.save()
104
        except socket.error, e:
105
            raise CommandError(e)
106
        else:
107
            msg = "Created user id %d" % (user.id,)
108
            if options['password'] is None:
109
                msg += " with password '%s'" % (password,)
110
            self.stdout.write(msg + '\n')
b/snf-astakos-app/astakos/im/management/commands/inviteuser.py
35 35

  
36 36
from django.core.management.base import BaseCommand, CommandError
37 37
from django.db.utils import IntegrityError
38
from django.db import transaction
38 39

  
39 40
from astakos.im.functions import invite, SendMailError
41
from astakos.im.models import Invitation
40 42

  
41 43
from ._common import get_user
42 44

  
43

  
45
@transaction.commit_manually
44 46
class Command(BaseCommand):
45 47
    args = "<inviter id or email> <email> <real name>"
46 48
    help = "Invite a user"
......
62 64
                invite(invitation, inviter)
63 65
                self.stdout.write("Invitation sent to '%s'\n" % (email,))
64 66
            except SendMailError, e:
67
                transaction.rollback()
65 68
                raise CommandError(e.message)
66 69
            except IntegrityError, e:
70
                transaction.rollback()
67 71
                raise CommandError("There is already an invitation for %s" % (email,))
72
            else:
73
                transaction.commit()
68 74
        else:
69 75
            raise CommandError("No invitations left")
b/snf-astakos-app/astakos/im/management/commands/listgroups.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from optparse import make_option
35

  
36
from django.core.management.base import BaseCommand, CommandError
37
from django.contrib.auth.models import Group
38

  
39
from astakos.im.models import AstakosUser
40

  
41
from ._common import format_bool
42

  
43

  
44
class Command(BaseCommand):
45
    help = "List g"
46
    
47
    option_list = BaseCommand.option_list + (
48
        make_option('-c',
49
            action='store_true',
50
            dest='csv',
51
            default=False,
52
            help="Use pipes to separate values"),
53
    )
54
    
55
    def handle(self, *args, **options):
56
        if args:
57
            raise CommandError("Command doesn't accept any arguments")
58
        
59
        groups = Group.objects.all()
60
        
61
        labels = ('id', 'name')
62
        columns = (1, 2)
63
        
64
        if not options['csv']:
65
            line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
66
            self.stdout.write(line + '\n')
67
            sep = '-' * len(line)
68
            self.stdout.write(sep + '\n')
69
        
70
        for group in groups:
71
            fields = (str(group.id), group.name)
72
            
73
            if options['csv']:
74
                line = '|'.join(fields)
75
            else:
76
                line = ' '.join(f.rjust(w) for f, w in zip(fields, columns))
77
            
78
            self.stdout.write(line.encode('utf8') + '\n')
b/snf-astakos-app/astakos/im/management/commands/listinvitations.py
57 57
        
58 58
        invitations = Invitation.objects.all()
59 59
        
60
        labels = ('id', 'email', 'real name', 'code', 'used', 'consumed')
61
        columns = (3, 24, 24, 20, 4, 8)
60
        labels = ('id', 'inviter', 'email', 'real name', 'code', 'used', 'consumed')
61
        columns = (3, 24, 24, 24, 20, 4, 8)
62 62
        
63 63
        if not options['csv']:
64 64
            line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
......
71 71
            code = str(invitation.code)
72 72
            used = format_bool(invitation.is_accepted)
73 73
            consumed = format_bool(invitation.is_consumed)
74
            fields = (id, invitation.username, invitation.realname,
74
            fields = (id, invitation.inviter.email, invitation.username, invitation.realname,
75 75
                      code, used, consumed)
76 76
            
77 77
            if options['csv']:
b/snf-astakos-app/astakos/im/management/commands/listusers.py
64 64
        if options['pending']:
65 65
            users = users.filter(is_active=False)
66 66
        
67
        labels = ('id', 'email', 'real name', 'affiliation', 'active', 'admin')
68
        columns = (3, 24, 24, 12, 6, 5)
67
        labels = ('id', 'email', 'real name', 'affiliation', 'active', 'admin', 'provider')
68
        columns = (3, 24, 24, 12, 6, 5, 12)
69 69
        
70 70
        if not options['csv']:
71 71
            line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
......
78 78
            active = format_bool(user.is_active)
79 79
            admin = format_bool(user.is_superuser)
80 80
            fields = (id, user.email, user.realname, user.affiliation, active,
81
                      admin)
81
                      admin, user.provider)
82 82
            
83 83
            if options['csv']:
84 84
                line = '|'.join(fields)
b/snf-astakos-app/astakos/im/management/commands/modifyuser.py
34 34
from optparse import make_option
35 35

  
36 36
from django.core.management.base import BaseCommand, CommandError
37
from django.contrib.auth.models import Group
37 38

  
38 39
from ._common import get_user
39 40

  
......
80 81
            dest='inactive',
81 82
            default=False,
82 83
            help="Change user's state to inactive"),
84
        make_option('--group',
85
            dest='group',
86
            help="Extend user groups"),
83 87
        )
84 88
    
85 89
    def handle(self, *args, **options):
......
104 108
        if invitations is not None:
105 109
            user.invitations = int(invitations)
106 110
        
111
        groupname = options.get('group')
112
        if groupname is not None:
113
            try:
114
                group = Group.objects.get(name=groupname)
115
                user.groups.add(group)
116
            except Group.DoesNotExist, e:
117
                raise CommandError("Group named %s does not exist." % groupname)
118
        
107 119
        level = options.get('level')
108 120
        if level is not None:
109 121
            user.level = int(level)
b/snf-astakos-app/astakos/im/management/commands/sendactivation.py
32 32
# or implied, of GRNET S.A.
33 33

  
34 34
from django.core.management.base import BaseCommand, CommandError
35
from django.db import transaction
36 35

  
37 36
from astakos.im.functions import send_verification
38 37

  
......
57 56
                self.stderr.write(msg)
58 57
                continue
59 58
            
60
            send_verification(user)
59
            try:
60
                send_verification(user)
61
            except SendMailError, e:
62
                raise CommandError(e.message)
61 63
            
62 64
            self.stdout.write("Activated '%s'\n" % (user.email,))
b/snf-astakos-app/astakos/im/management/commands/showuser.py
75 75
            'verified': format_bool(user.is_verified),
76 76
            'has_credits': format_bool(user.has_credits),
77 77
            'has_signed_terms': format_bool(user.has_signed_terms),
78
            'date_signed_terms': format_date(user.date_signed_terms)
78
            'date_signed_terms': format_date(user.date_signed_terms),
79
            'groups': [elem.name for elem in user.groups.all()],
80
            'third_party_identifier': user.third_party_identifier
79 81
        }
80 82
        
81 83
        for key, val in sorted(kv.items()):
82
            line = '%s: %s\n' % (key.rjust(17), val)
84
            line = '%s: %s\n' % (key.rjust(22), val)
83 85
            self.stdout.write(line.encode('utf8'))
b/snf-astakos-app/astakos/im/models.py
33 33

  
34 34
import hashlib
35 35
import uuid
36
import logging
36 37

  
37 38
from time import asctime
38 39
from datetime import datetime, timedelta
......
41 42
from random import randint
42 43

  
43 44
from django.db import models
44
from django.contrib.auth.models import User, UserManager
45
from django.contrib.auth.models import User, UserManager, Group
45 46

  
46 47
from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION
47 48
from astakos.im.queue.userevent import UserEvent
......
49 50

  
50 51
QUEUE_CLIENT_ID = 3 # Astakos.
51 52

  
53
logger = logging.getLogger(__name__)
54

  
52 55
class AstakosUser(User):
53 56
    """
54 57
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
......
81 84
    has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
82 85
    date_signed_terms = models.DateTimeField('Signed terms date', null=True)
83 86
    
87
    __has_signed_terms = False
88
    __groupnames = []
89
    
90
    def __init__(self, *args, **kwargs):
91
        super(AstakosUser, self).__init__(*args, **kwargs)
92
        self.__has_signed_terms = self.has_signed_terms
93
        if self.id:
94
            self.__groupnames = [g.name for g in self.groups.all()]
95
        else:
96
            self.is_active = False
97
    
84 98
    @property
85 99
    def realname(self):
86 100
        return '%s %s' %(self.first_name, self.last_name)
......
106 120
            if not self.id:
107 121
                self.date_joined = datetime.now()
108 122
            self.updated = datetime.now()
123
        
124
        # update date_signed_terms if necessary
125
        if self.__has_signed_terms != self.has_signed_terms:
126
            self.date_signed_terms = datetime.now()
127
        
109 128
        if not self.id:
110 129
            # set username
111 130
            while not self.username:
......
114 133
                    AstakosUser.objects.get(username = username)
115 134
                except AstakosUser.DoesNotExist, e:
116 135
                    self.username = username
117
            self.is_active = False
118 136
            if not self.provider:
119 137
                self.provider = 'local'
120 138
        report_user_event(self)
121 139
        super(AstakosUser, self).save(**kwargs)
140
        
141
        # set group if does not exist
142
        groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
143
        if groupname not in self.__groupnames:
144
            try:
145
                group = Group.objects.get(name = groupname)
146
                self.groups.add(group)
147
            except Group.DoesNotExist, e:
148
                logger.exception(e)
122 149
    
123 150
    def renew_token(self):
124 151
        md5 = hashlib.md5()
......
159 186
    accepted = models.DateTimeField('Acceptance date', null=True, blank=True)
160 187
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
161 188
    
162
    def save(self, **kwargs):
189
    def __init__(self, *args, **kwargs):
190
        super(Invitation, self).__init__(*args, **kwargs)
163 191
        if not self.id:
164 192
            self.code = _generate_invitation_code()
165
        super(Invitation, self).save(**kwargs)
166 193
    
167 194
    def consume(self):
168 195
        self.is_consumed = True
b/snf-astakos-app/astakos/im/static/im/css/ie7.css
1
.navigation {
2
    min-width: 660px;
3
}
4

  
5
.mainnav.inline {
6
    position: absolute;
7
    right: -10px;
8
}
9

  
10
.mainnav.inline.subnav {
11
    position: relative;
12
    top: 50px;
13
}
b/snf-astakos-app/astakos/im/static/im/css/styles.css
844 844
  background-color: #3582ac;
845 845
}
846 846
.rightcol {
847
  margin-left: 511px;
847
  display: inline;
848
  float: left;
849
  margin-left: 22px;
848 850
  width: 306px;
851
  margin-left: 102px;
849 852
}
850 853
.rightcol.narrow {
851
  margin-left: 593px;
854
  display: inline;
855
  float: left;
856
  margin-left: 22px;
852 857
  width: 224px;
858
  margin-left: 102px;
853 859
}
854 860
.rightcol input[type=text], .rightcol input[type=password] {
855 861
  width: 273px;
......
993 999
}
994 1000
form .form-row.submit {
995 1001
  margin-top: 22px;
996
  z-index: 10;
1002
}
1003
form .form-row.with-checkbox {
1004
  margin-top: 7px;
997 1005
}
998 1006
form .form-row .extra-link {
999 1007
  color: #808080;
......
1013 1021
  color: #aaa;
1014 1022
}
1015 1023
form.innerlabels p {
1024
  display: table;
1016 1025
  position: relative;
1017 1026
}
1018 1027
form textarea,
......
1192 1201
  color: inherit;
1193 1202
  font-weight: bold;
1194 1203
}
1204
form.innerlabels label.checkbox-label {
1205
  position: relative !important;
1206
  margin-left: 10px !important;
1207
  padding-top: 1em !important;
1208
  top: 11px !important;
1209
  left: 10px;
1210
  cursor: pointer;
1211
}
1195 1212
.service-desc {
1196 1213
  margin-top: 4em;
1197 1214
}
......
1481 1498
  margin-bottom: 2em;
1482 1499
  font-size: 0.8em;
1483 1500
}
1484
.initial_hidden {
1501
.initially-hidden {
1485 1502
  display: none;
1486 1503
}
1487 1504
/* recaptcha */
......
1511 1528
  cursor: pointer;
1512 1529
  margin-top: 5.333333333333333px;
1513 1530
}
1531
.textcontent h1, .terms-content h1 {
1532
  font-size: 1.9em;
1533
  margin-bottom: 0.2em;
1534
  margin-top: 1.2em;
1535
  color: #3582ac;
1536
}
1537
.textcontent h1:first-child, .terms-content h1:first-child {
1538
  margin-top: 0;
1539
}
1540
.textcontent h2, .terms-content h2 {
1541
  font-size: 1.6em;
1542
  margin-bottom: 1.1em;
1543
  margin-top: 1.1em;
1544
  color: #3b91bf;
1545
}
1546
.textcontent h2:first-child, .terms-content h2:first-child {
1547
  margin-top: 0;
1548
}
1549
.textcontent h3, .terms-content h3 {
1550
  font-size: 1.3em;
1551
  margin-bottom: 1em;
1552
  margin-top: 1em;
1553
  color: #3b91bf;
1554
}
1555
.textcontent h3:first-child, .terms-content h3:first-child {
1556
  margin-top: 0;
1557
}
1558
.textcontent p, .terms-content p {
1559
  margin-bottom: 1em;
1560
  line-height: 1.5em;
1561
}
1562
.textcontent .date, .terms-content .date {
1563
  margin: 1em 0;
1564
  font-size: 0.9em;
1565
  margin-bottom: 2em;
1566
  color: #808080;
1567
}
b/snf-astakos-app/astakos/im/static/im/css/styles.less
258 258
}
259 259

  
260 260
.rightcol {
261
    .offset(6.5);
262
    .columns(4);
261
    .makeColumn(4);
262
    margin-left: 4*@gridGutterWidth + 14;
263 263

  
264 264
    &.narrow {
265
        .offset(7.5);
266
        .columns(3);    
265
        .makeColumn(3);
266
        margin-left: 4*@gridGutterWidth + 14;
267 267
    }
268

  
268 269
    input[type=text], input[type=password] {
269 270
        width: 3*@gridColumnWidth + 4*@gridGutterWidth + 5;    
270 271
    }
......
428 429
        position: relative;
429 430
        &.submit {
430 431
            margin-top: 1.5*@verticalSpacing;
431
            z-index: 10;
432
        }
433

  
434
        &.with-checkbox {
435
            margin-top: 7px;    
432 436
        }
433 437

  
434 438
        .extra-link {
......
453 457
    }
454 458

  
455 459
    &.innerlabels p {
460
        display: table;
456 461
        position: relative;    
457 462
    }
458 463

  
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff