models.py 64.6 KB
Newer Older
Antony Chazapis's avatar
Antony Chazapis committed
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
2
#
3
4
5
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
6
#
7
8
9
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
10
#
11
12
13
14
#   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.
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
15
#
16
17
18
19
20
21
22
23
24
25
26
27
# 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.
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
28
#
29
30
31
32
33
34
# 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.

import hashlib
35
import uuid
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
36
import logging
37
import json
38

39
from time import asctime, sleep
40
41
from datetime import datetime, timedelta
from base64 import b64encode
42
from urlparse import urlparse
43
from urllib import quote
44
from random import randint
45
from collections import defaultdict, namedtuple
46

47
from django.db import models, IntegrityError, transaction, connection
Olga Brani's avatar
Olga Brani committed
48
from django.contrib.auth.models import User, UserManager, Group, Permission
49
from django.utils.translation import ugettext as _
50
from django.db import transaction
51
from django.core.exceptions import ValidationError
52
from django.db.models.signals import (
53
    pre_save, post_save, post_syncdb, post_delete)
Olga Brani's avatar
Olga Brani committed
54
55
from django.contrib.contenttypes.models import ContentType

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
56
from django.dispatch import Signal
57
from django.db.models import Q
58
59
60
from django.core.urlresolvers import reverse
from django.utils.http import int_to_base36
from django.contrib.auth.tokens import default_token_generator
61
from django.conf import settings
62
from django.utils.importlib import import_module
63
from django.utils.safestring import mark_safe
64
from django.core.validators import email_re
65
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
66

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
67
68
from astakos.im.settings import (
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
69
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
70
    SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA)
71
from astakos.im import settings as astakos_settings
72
from astakos.im.endpoints.qh import (
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
73
    register_users, register_resources, qh_add_quota, QuotaLimits,
74
    qh_query_serials, qh_ack_serials)
75
from astakos.im import auth_providers
76

Olga Brani's avatar
Olga Brani committed
77
import astakos.im.messages as astakos_messages
78
from .managers import ForUpdateManager
79

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
80
81
logger = logging.getLogger(__name__)

Olga Brani's avatar
Olga Brani committed
82
DEFAULT_CONTENT_TYPE = None
83
84
85
86
87
88
89
90
91
92
93
94
95
_content_type = None

def get_content_type():
    global _content_type
    if _content_type is not None:
        return _content_type

    try:
        content_type = ContentType.objects.get(app_label='im', model='astakosuser')
    except:
        content_type = DEFAULT_CONTENT_TYPE
    _content_type = content_type
    return content_type
Olga Brani's avatar
Olga Brani committed
96
97
98

RESOURCE_SEPARATOR = '.'

99
inf = float('inf')
100

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
101
class Service(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
102
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
103
104
    url = models.FilePathField()
    icon = models.FilePathField(blank=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
105
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
106
                                  null=True, blank=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
107
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
108
    auth_token_expires = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
109
        _('Token expiration date'), null=True)
110

111
    def renew_token(self, expiration_date=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
112
113
114
115
116
117
118
        md5 = hashlib.md5()
        md5.update(self.name.encode('ascii', 'ignore'))
        md5.update(self.url.encode('ascii', 'ignore'))
        md5.update(asctime())

        self.auth_token = b64encode(md5.digest())
        self.auth_token_created = datetime.now()
119
120
121
122
        if expiration_date:
            self.auth_token_expires = expiration_date
        else:
            self.auth_token_expires = None
123

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
124
125
126
    def __str__(self):
        return self.name

Olga Brani's avatar
Olga Brani committed
127
128
129
130
131
132
133
134
    @property
    def resources(self):
        return self.resource_set.all()

    @resources.setter
    def resources(self, resources):
        for s in resources:
            self.resource_set.create(**s)
135

Olga Brani's avatar
Olga Brani committed
136
137
138
139
140
141
142
143
144
145
146
    def add_resource(self, service, resource, uplimit, update=True):
        """Raises ObjectDoesNotExist, IntegrityError"""
        resource = Resource.objects.get(service__name=service, name=resource)
        if update:
            AstakosUserQuota.objects.update_or_create(user=self,
                                                      resource=resource,
                                                      defaults={'uplimit': uplimit})
        else:
            q = self.astakosuserquota_set
            q.create(resource=resource, uplimit=uplimit)

147

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
148
class ResourceMetadata(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
149
150
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
    value = models.CharField(_('Value'), max_length=255)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
151

152
153
154
155
156
157
158
159
_presentation_data = {}
def get_presentation(resource):
    global _presentation_data
    presentation = _presentation_data.get(resource, {})
    if not presentation:
        resource_presentation = RESOURCES_PRESENTATION_DATA.get('resources', {})
        presentation = resource_presentation.get(resource, {})
        _presentation_data[resource] = presentation
160
    return presentation
161

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
162
class Resource(models.Model):
163
    name = models.CharField(_('Name'), max_length=255)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
164
165
    meta = models.ManyToManyField(ResourceMetadata)
    service = models.ForeignKey(Service)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
166
167
168
    desc = models.TextField(_('Description'), null=True)
    unit = models.CharField(_('Name'), null=True, max_length=255)
    group = models.CharField(_('Group'), null=True, max_length=255)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
169

170
171
    class Meta:
        unique_together = ("name", "service")
172

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
173
    def __str__(self):
Olga Brani's avatar
Olga Brani committed
174
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
175

176
177
178
    @property
    def help_text(self):
        return get_presentation(str(self)).get('help_text', '')
179

180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
    @property
    def help_text_input_each(self):
        return get_presentation(str(self)).get('help_text_input_each', '')

    @property
    def is_abbreviation(self):
        return get_presentation(str(self)).get('is_abbreviation', False)

    @property
    def report_desc(self):
        return get_presentation(str(self)).get('report_desc', '')

    @property
    def placeholder(self):
        return get_presentation(str(self)).get('placeholder', '')

    @property
    def verbose_name(self):
        return get_presentation(str(self)).get('verbose_name', '')


201
202
203
204
205
206
207
208
209
210
211
212
213
_default_quota = {}
def get_default_quota():
    global _default_quota
    if _default_quota:
        return _default_quota
    for s, data in SERVICES.iteritems():
        map(
            lambda d:_default_quota.update(
                {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
            ),
            data.get('resources', {})
        )
    return _default_quota
214

215
class AstakosUserManager(UserManager):
216
217
218
219
220
221
222
223
224
225

    def get_auth_provider_user(self, provider, **kwargs):
        """
        Retrieve AstakosUser instance associated with the specified third party
        id.
        """
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
                          kwargs.iteritems()))
        return self.get(auth_providers__module=provider, **kwargs)

226
227
228
    def get_by_email(self, email):
        return self.get(email=email)

229
230
231
232
233
234
235
236
237
238
239
240
    def get_by_identifier(self, email_or_username, **kwargs):
        try:
            return self.get(email__iexact=email_or_username, **kwargs)
        except AstakosUser.DoesNotExist:
            return self.get(username__iexact=email_or_username, **kwargs)

    def user_exists(self, email_or_username, **kwargs):
        qemail = Q(email__iexact=email_or_username)
        qusername = Q(username__iexact=email_or_username)
        return self.filter(qemail | qusername).exists()


Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
241
class AstakosUser(User):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
242
243
244
    """
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
245
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
246
247
248
249
                                   null=True)

    # DEPRECATED FIELDS: provider, third_party_identifier moved in
    #                    AstakosUserProvider model.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
250
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
251
252
                                null=True)
    # ex. screen_name for twitter, eppn for shibboleth
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
253
    third_party_identifier = models.CharField(_('Third-party identifier'),
254
255
256
                                              max_length=255, null=True,
                                              blank=True)

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
257

258
    #for invitations
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
259
    user_level = DEFAULT_USER_LEVEL
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
260
    level = models.IntegerField(_('Inviter level'), default=user_level)
261
    invitations = models.IntegerField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
262
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
263

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
264
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
265
                                  null=True, blank=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
266
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
267
    auth_token_expires = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
268
        _('Token expiration date'), null=True)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
269

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
270
271
    updated = models.DateTimeField(_('Update date'))
    is_verified = models.BooleanField(_('Is verified?'), default=False)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
272

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
273
    email_verified = models.BooleanField(_('Email verified?'), default=False)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
274

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
275
    has_credits = models.BooleanField(_('Has credits?'), default=False)
276
    has_signed_terms = models.BooleanField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
277
        _('I agree with the terms'), default=False)
278
    date_signed_terms = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
279
        _('Signed terms date'), null=True, blank=True)
280
281

    activation_sent = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
282
        _('Activation sent data'), null=True, blank=True)
283
284
285
286

    policy = models.ManyToManyField(
        Resource, null=True, through='AstakosUserQuota')

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
287
288
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
289
    __has_signed_terms = False
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
290
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
Olga Brani's avatar
Olga Brani committed
291
                                           default=False, db_index=True)
292
293

    objects = AstakosUserManager()
294

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
295
296
297
    def __init__(self, *args, **kwargs):
        super(AstakosUser, self).__init__(*args, **kwargs)
        self.__has_signed_terms = self.has_signed_terms
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
298
        if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
299
            self.is_active = False
300

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
301
302
    @property
    def realname(self):
303
        return '%s %s' % (self.first_name, self.last_name)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
304

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
305
306
307
308
309
310
311
312
    @realname.setter
    def realname(self, value):
        parts = value.split(' ')
        if len(parts) == 2:
            self.first_name = parts[0]
            self.last_name = parts[1]
        else:
            self.last_name = parts[0]
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
313

Olga Brani's avatar
Olga Brani committed
314
315
316
    def add_permission(self, pname):
        if self.has_perm(pname):
            return
317
318
319
320
        p, created = Permission.objects.get_or_create(
                                    codename=pname,
                                    name=pname.capitalize(),
                                    content_type=get_content_type())
Olga Brani's avatar
Olga Brani committed
321
322
323
324
325
326
        self.user_permissions.add(p)

    def remove_permission(self, pname):
        if self.has_perm(pname):
            return
        p = Permission.objects.get(codename=pname,
327
                                   content_type=get_content_type())
Olga Brani's avatar
Olga Brani committed
328
329
        self.user_permissions.remove(p)

330
331
332
    @property
    def invitation(self):
        try:
333
            return Invitation.objects.get(username=self.email)
334
335
        except Invitation.DoesNotExist:
            return None
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
336

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
337
338
    @property
    def quota(self):
Olga Brani's avatar
Olga Brani committed
339
        """Returns a dict with the sum of quota limits per resource"""
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
340
        d = defaultdict(int)
341
342
        default_quota = get_default_quota()
        d.update(default_quota)
Olga Brani's avatar
Olga Brani committed
343
        for q in self.policies:
344
            d[q.resource] += q.uplimit or inf
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
345
346
        for m in self.projectmembership_set.select_related().all():
            if not m.acceptance_date:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
347
                continue
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
348
            p = m.project
349
            if not p.is_active():
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
350
                continue
351
            grants = p.application.projectresourcegrant_set.all()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
352
            for g in grants:
353
                d[str(g.resource)] += g.member_capacity or inf
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
354
        return d
355

Olga Brani's avatar
Olga Brani committed
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
    @property
    def policies(self):
        return self.astakosuserquota_set.select_related().all()

    @policies.setter
    def policies(self, policies):
        for p in policies:
            service = policies.get('service', None)
            resource = policies.get('resource', None)
            uplimit = policies.get('uplimit', 0)
            update = policies.get('update', True)
            self.add_policy(service, resource, uplimit, update)

    def add_policy(self, service, resource, uplimit, update=True):
        """Raises ObjectDoesNotExist, IntegrityError"""
        resource = Resource.objects.get(service__name=service, name=resource)
        if update:
            AstakosUserQuota.objects.update_or_create(user=self,
                                                      resource=resource,
                                                      defaults={'uplimit': uplimit})
        else:
            q = self.astakosuserquota_set
            q.create(resource=resource, uplimit=uplimit)

    def remove_policy(self, service, resource):
        """Raises ObjectDoesNotExist, IntegrityError"""
        resource = Resource.objects.get(service__name=service, name=resource)
        q = self.policies.get(resource=resource).delete()

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
385
386
387
388
389
390
391
392
393
    def update_uuid(self):
        while not self.uuid:
            uuid_val =  str(uuid.uuid4())
            try:
                AstakosUser.objects.get(uuid=uuid_val)
            except AstakosUser.DoesNotExist, e:
                self.uuid = uuid_val
        return self.uuid

Olga Brani's avatar
Olga Brani committed
394
395
396
397
    @property
    def extended_groups(self):
        return self.membership_set.select_related().all()

398
399
400
    def save(self, update_timestamps=True, **kwargs):
        if update_timestamps:
            if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
401
                self.date_joined = datetime.now()
402
            self.updated = datetime.now()
403

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
404
405
406
        # update date_signed_terms if necessary
        if self.__has_signed_terms != self.has_signed_terms:
            self.date_signed_terms = datetime.now()
407

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
408
409
410
        self.update_uuid()

        if self.username != self.email.lower():
411
            # set username
412
            self.username = self.email.lower()
413

414
        self.validate_unique_email_isactive()
415

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
416
        super(AstakosUser, self).save(**kwargs)
417

418
    def renew_token(self, flush_sessions=False, current_key=None):
419
        md5 = hashlib.md5()
420
        md5.update(settings.SECRET_KEY)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
421
        md5.update(self.username)
422
423
        md5.update(self.realname.encode('ascii', 'ignore'))
        md5.update(asctime())
424

425
426
427
        self.auth_token = b64encode(md5.digest())
        self.auth_token_created = datetime.now()
        self.auth_token_expires = self.auth_token_created + \
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
428
                                  timedelta(hours=AUTH_TOKEN_DURATION)
429
430
        if flush_sessions:
            self.flush_sessions(current_key)
431
        msg = 'Token renewed for %s' % self.email
432
        logger.log(LOGGING_LEVEL, msg)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
433

434
435
436
437
    def flush_sessions(self, current_key=None):
        q = self.sessions
        if current_key:
            q = q.exclude(session_key=current_key)
438

439
440
441
        keys = q.values_list('session_key', flat=True)
        if keys:
            msg = 'Flushing sessions: %s' % ','.join(keys)
442
            logger.log(LOGGING_LEVEL, msg, [])
443
444
445
446
447
        engine = import_module(settings.SESSION_ENGINE)
        for k in keys:
            s = engine.SessionStore(k)
            s.flush()

448
    def __unicode__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
449
        return '%s (%s)' % (self.realname, self.email)
450

451
    def conflicting_email(self):
452
        q = AstakosUser.objects.exclude(username=self.username)
453
        q = q.filter(email__iexact=self.email)
454
455
456
        if q.count() != 0:
            return True
        return False
457

458
    def validate_unique_email_isactive(self):
459
460
461
        """
        Implements a unique_together constraint for email and is_active fields.
        """
462
        q = AstakosUser.objects.all()
463
        q = q.filter(email = self.email)
464
465
        if self.id:
            q = q.filter(~Q(id = self.id))
466
        if q.count() != 0:
467
            m = 'Another account with the same email = %(email)s & \
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
468
                is_active = %(is_active)s found.' % self.__dict__
469
            raise ValidationError(m)
470

471
472
473
    def email_change_is_pending(self):
        return self.emailchanges.count() > 0

474
475
476
    def email_change_is_pending(self):
        return self.emailchanges.count() > 0

477
    @property
478
479
480
481
482
483
484
485
486
487
    def signed_terms(self):
        term = get_latest_terms()
        if not term:
            return True
        if not self.has_signed_terms:
            return False
        if not self.date_signed_terms:
            return False
        if self.date_signed_terms < term.date:
            self.has_signed_terms = False
488
            self.date_signed_terms = None
489
490
491
492
            self.save()
            return False
        return True

493
494
495
496
497
498
499
500
501
502
503
504
505
506
    def set_invitations_level(self):
        """
        Update user invitation level
        """
        level = self.invitation.inviter.level + 1
        self.level = level
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)

    def can_login_with_auth_provider(self, provider):
        if not self.has_auth_provider(provider):
            return False
        else:
            return auth_providers.get_provider(provider).is_available_for_login()

507
    def can_add_auth_provider(self, provider, **kwargs):
508
        provider_settings = auth_providers.get_provider(provider)
509
510

        if not provider_settings.is_available_for_add():
511
            return False
512

513
514
515
        if self.has_auth_provider(provider) and \
           provider_settings.one_per_user:
            return False
516

517
518
519
        if 'provider_info' in kwargs:
            kwargs.pop('provider_info')

520
521
522
523
524
525
526
527
528
529
        if 'identifier' in kwargs:
            try:
                # provider with specified params already exist
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
                                                                   **kwargs)
            except AstakosUser.DoesNotExist:
                return True
            else:
                return False

530
531
        return True

532
533
534
535
536
537
538
539
540
    def can_remove_auth_provider(self, module):
        provider = auth_providers.get_provider(module)
        existing = self.get_active_auth_providers()
        existing_for_provider = self.get_active_auth_providers(module=module)

        if len(existing) <= 1:
            return False

        if len(existing_for_provider) == 1 and provider.is_required():
541
            return False
542

543
544
545
546
547
        return True

    def can_change_password(self):
        return self.has_auth_provider('local', auth_backend='astakos')

548
549
550
551
552
553
554
    def has_required_auth_providers(self):
        required = auth_providers.REQUIRED_PROVIDERS
        for provider in required:
            if not self.has_auth_provider(provider):
                return False
        return True

555
556
557
558
559
    def has_auth_provider(self, provider, **kwargs):
        return bool(self.auth_providers.filter(module=provider,
                                               **kwargs).count())

    def add_auth_provider(self, provider, **kwargs):
560
561
        info_data = ''
        if 'provider_info' in kwargs:
562
563
564
            info_data = kwargs.pop('provider_info')
            if isinstance(info_data, dict):
                info_data = json.dumps(info_data)
565

566
        if self.can_add_auth_provider(provider, **kwargs):
567
568
569
            self.auth_providers.create(module=provider, active=True,
                                       info_data=info_data,
                                       **kwargs)
570
571
        else:
            raise Exception('Cannot add provider')
572
573
574
575
576
577
578
579
580
581

    def add_pending_auth_provider(self, pending):
        """
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
        the current user.
        """
        if not isinstance(pending, PendingThirdPartyUser):
            pending = PendingThirdPartyUser.objects.get(token=pending)

        provider = self.add_auth_provider(pending.provider,
582
583
584
                               identifier=pending.third_party_identifier,
                                affiliation=pending.affiliation,
                                          provider_info=pending.info)
585

586
        if email_re.match(pending.email or '') and pending.email != self.email:
587
588
589
590
591
592
593
594
595
596
            self.additionalmail_set.get_or_create(email=pending.email)

        pending.delete()
        return provider

    def remove_auth_provider(self, provider, **kwargs):
        self.auth_providers.get(module=provider, **kwargs).delete()

    # user urls
    def get_resend_activation_url(self):
597
598
599
600
601
        return reverse('send_activation', kwargs={'user_id': self.pk})

    def get_provider_remove_url(self, module, **kwargs):
        return reverse('remove_auth_provider', kwargs={
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623

    def get_activation_url(self, nxt=False):
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
                                 quote(self.auth_token))
        if nxt:
            url += "&next=%s" % quote(nxt)
        return url

    def get_password_reset_url(self, token_generator=default_token_generator):
        return reverse('django.contrib.auth.views.password_reset_confirm',
                          kwargs={'uidb36':int_to_base36(self.id),
                                  'token':token_generator.make_token(self)})

    def get_auth_providers(self):
        return self.auth_providers.all()

    def get_available_auth_providers(self):
        """
        Returns a list of providers available for user to connect to.
        """
        providers = []
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
624
            if self.can_add_auth_provider(module):
625
626
627
628
                providers.append(provider_settings(self))

        return providers

629
    def get_active_auth_providers(self, **filters):
630
        providers = []
631
        for provider in self.auth_providers.active(**filters):
632
633
634
635
            if auth_providers.get_provider(provider.module).is_available_for_login():
                providers.append(provider)
        return providers

636
637
638
639
    @property
    def auth_providers_display(self):
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))

640
641
642
643
644
645
646
647
    def get_inactive_message(self):
        msg_extra = ''
        message = ''
        if self.activation_sent:
            if self.email_verified:
                message = _(astakos_messages.ACCOUNT_INACTIVE)
            else:
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
648
                if astakos_settings.MODERATION_ENABLED:
649
650
651
652
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
                else:
                    url = self.get_resend_activation_url()
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
653
                                u' ' + \
654
655
656
                                _('<a href="%s">%s?</a>') % (url,
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
        else:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
657
            if astakos_settings.MODERATION_ENABLED:
658
659
660
661
662
663
664
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
            else:
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
                url = self.get_resend_activation_url()
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))

665
        return mark_safe(message + u' '+ msg_extra)
666

667
668
669
670
671
672
    def owns_project(self, project):
        return project.user_status(self) == 100

    def is_project_member(self, project):
        return project.user_status(self) in [0,1,2,3]

673
674
675
    def is_project_accepted_member(self, project):
        return project.user_status(self) == 2

676
677
678

class AstakosUserAuthProviderManager(models.Manager):

679
680
    def active(self, **filters):
        return self.filter(active=True, **filters)
681
682
683
684
685
686


class AstakosUserAuthProvider(models.Model):
    """
    Available user authentication methods.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
687
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
688
689
                                   null=True, default=None)
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
690
    module = models.CharField(_('Provider'), max_length=255, blank=False,
691
                                default='local')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
692
    identifier = models.CharField(_('Third-party identifier'),
693
694
695
                                              max_length=255, null=True,
                                              blank=True)
    active = models.BooleanField(default=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
696
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
697
                                   default='astakos')
698
    info_data = models.TextField(default="", null=True, blank=True)
699
    created = models.DateTimeField('Creation date', auto_now_add=True)
700
701
702
703
704

    objects = AstakosUserAuthProviderManager()

    class Meta:
        unique_together = (('identifier', 'module', 'user'), )
705
        ordering = ('module', 'created')
706

707
708
709
710
    def __init__(self, *args, **kwargs):
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
        try:
            self.info = json.loads(self.info_data)
711
712
713
            if not self.info:
                self.info = {}
        except Exception, e:
714
            self.info = {}
715

716
717
718
        for key,value in self.info.iteritems():
            setattr(self, 'info_%s' % key, value)

719
720
721
722
723
724
725

    @property
    def settings(self):
        return auth_providers.get_provider(self.module)

    @property
    def details_display(self):
726
727
728
729
        try:
          return self.settings.get_details_tpl_display % self.__dict__
        except:
          return ''
730
731
732
733
734
735
736
737
738

    @property
    def title_display(self):
        title_tpl = self.settings.get_title_display
        try:
            if self.settings.get_user_title_display:
                title_tpl = self.settings.get_user_title_display
        except Exception, e:
            pass
739
740
741
742
        try:
          return title_tpl % self.__dict__
        except:
          return self.settings.get_title_display % self.__dict__
743
744
745
746
747
748

    def can_remove(self):
        return self.user.can_remove_auth_provider(self.module)

    def delete(self, *args, **kwargs):
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
749
750
751
        if self.module == 'local':
            self.user.set_unusable_password()
            self.user.save()
752
753
        return ret

754
755
756
    def __repr__(self):
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)

757
758
759
760
761
762
763
    def __unicode__(self):
        if self.identifier:
            return "%s:%s" % (self.module, self.identifier)
        if self.auth_backend:
            return "%s:%s" % (self.module, self.auth_backend)
        return self.module

764
765
766
    def save(self, *args, **kwargs):
        self.info_data = json.dumps(self.info)
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
767

768

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
769
class ExtendedManager(models.Manager):
Olga Brani's avatar
Olga Brani committed
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
    def _update_or_create(self, **kwargs):
        assert kwargs, \
            'update_or_create() must be passed at least one keyword argument'
        obj, created = self.get_or_create(**kwargs)
        defaults = kwargs.pop('defaults', {})
        if created:
            return obj, True, False
        else:
            try:
                params = dict(
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
                params.update(defaults)
                for attr, val in params.items():
                    if hasattr(obj, attr):
                        setattr(obj, attr, val)
                sid = transaction.savepoint()
                obj.save(force_update=True)
                transaction.savepoint_commit(sid)
                return obj, False, True
            except IntegrityError, e:
                transaction.savepoint_rollback(sid)
                try:
                    return self.get(**kwargs), False, False
                except self.model.DoesNotExist:
                    raise e

    update_or_create = _update_or_create
797

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
798
799

class AstakosUserQuota(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
800
801
802
    objects = ExtendedManager()
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
803
804
    resource = models.ForeignKey(Resource)
    user = models.ForeignKey(AstakosUser)
805

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
806
807
    class Meta:
        unique_together = ("resource", "user")
808

809

810
811
812
813
class ApprovalTerms(models.Model):
    """
    Model for approval terms
    """
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
814

815
    date = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
816
817
        _('Issue date'), db_index=True, default=datetime.now())
    location = models.CharField(_('Terms location'), max_length=255)
818

819

820
class Invitation(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
821
822
823
    """
    Model for registring invitations
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
824
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
825
                                null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
826
827
828
829
830
831
    realname = models.CharField(_('Real name'), max_length=255)
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
832

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
833
834
    def __init__(self, *args, **kwargs):
        super(Invitation, self).__init__(*args, **kwargs)
835
836
        if not self.id:
            self.code = _generate_invitation_code()
837

838
839
840
841
    def consume(self):
        self.is_consumed = True
        self.consumed = datetime.now()
        self.save()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
842

843
    def __unicode__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
844
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
845

846
847

class EmailChangeManager(models.Manager):
848

849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
    @transaction.commit_on_success
    def change_email(self, activation_key):
        """
        Validate an activation key and change the corresponding
        ``User`` if valid.

        If the key is valid and has not expired, return the ``User``
        after activating.

        If the key is not valid or has expired, return ``None``.

        If the key is valid but the ``User`` is already active,
        return ``None``.

        After successful email change the activation record is deleted.

        Throws ValueError if there is already
        """
        try:
868
869
            email_change = self.model.objects.get(
                activation_key=activation_key)
870
871
872
873
874
            if email_change.activation_key_expired():
                email_change.delete()
                raise EmailChange.DoesNotExist
            # is there an active user with this address?
            try:
875
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
876
877
878
            except AstakosUser.DoesNotExist:
                pass
            else:
879
                raise ValueError(_('The new email address is reserved.'))
880
881
            # update user
            user = AstakosUser.objects.get(pk=email_change.user_id)
882
            old_email = user.email
883
884
885
            user.email = email_change.new_email_address
            user.save()
            email_change.delete()
886
887
888
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
                                                          user.email)
            logger.log(LOGGING_LEVEL, msg)
889
890
            return user
        except EmailChange.DoesNotExist:
891
            raise ValueError(_('Invalid activation key.'))
892
893
894


class EmailChange(models.Model):
895
896
897
    new_email_address = models.EmailField(
        _(u'new e-mail address'),
        help_text=_('Your old email address will be used until you verify your new one.'))
898
    user = models.ForeignKey(
899
        AstakosUser, unique=True, related_name='emailchanges')
900
    requested_at = models.DateTimeField(default=datetime.now())
901
902
    activation_key = models.CharField(
        max_length=40, unique=True, db_index=True)
903
904
905

    objects = EmailChangeManager()

906
907
908
909
    def get_url(self):
        return reverse('email_change_confirm',
                      kwargs={'activation_key': self.activation_key})

910
911
    def activation_key_expired(self):
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
912
913
        return self.requested_at + expiration_date < datetime.now()

914

915
916
917
918
919
class AdditionalMail(models.Model):
    """
    Model for registring invitations
    """
    owner = models.ForeignKey(AstakosUser)
920
    email = models.EmailField()
921

922

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
923
924
def _generate_invitation_code():
    while True:
925
        code = randint(1, 2L ** 63 - 1)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
926
927
928
929
930
931
        try:
            Invitation.objects.get(code=code)
            # An invitation with this code already exists, try again
        except Invitation.DoesNotExist:
            return code

932

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
933
934
935
936
937
938
939
940
def get_latest_terms():
    try:
        term = ApprovalTerms.objects.order_by('-id')[0]
        return term
    except IndexError:
        pass
    return None

941
942
943
944
class PendingThirdPartyUser(models.Model):
    """
    Model for registring successful third party user authentications
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
945
946
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
947
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
948
949
950
951
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
952
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
953
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
954
    info = models.TextField(default="", null=True, blank=True)
955

956
957
    class Meta:
        unique_together = ("provider", "third_party_identifier")
958

959
960
961
962
963
964
965
966
967
968
969
    def get_user_instance(self):
        d = self.__dict__
        d.pop('_state', None)
        d.pop('id', None)
        d.pop('token', None)
        d.pop('created', None)
        d.pop('info', None)
        user = AstakosUser(**d)

        return user

970
971
972
973
974
975
976
977
978
979
980
981
    @property
    def realname(self):
        return '%s %s' %(self.first_name, self.last_name)

    @realname.setter
    def realname(self, value):
        parts = value.split(' ')
        if len(parts) == 2:
            self.first_name = parts[0]
            self.last_name = parts[1]
        else:
            self.last_name = parts[0]
982

983
984
985
986
987
988
989
990
991
992
993
    def save(self, **kwargs):
        if not self.id:
            # set username
            while not self.username:
                username =  uuid.uuid4().hex[:30]
                try:
                    AstakosUser.objects.get(username = username)
                except AstakosUser.DoesNotExist, e:
                    self.username = username
        super(PendingThirdPartyUser, self).save(**kwargs)

994
995
996
997
998
    def generate_token(self):
        self.password = self.third_party_identifier
        self.last_login = datetime.now()
        self.token = default_token_generator.make_token(self)

999
1000
1001
1002
class SessionCatalog(models.Model):
    session_key = models.CharField(_('session key'), max_length=40)
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1003
1004
1005
1006

### PROJECTS ###
################

1007
def synced_model_metaclass(class_name, class_parents, class_attributes):
1008

1009
1010
    new_attributes = {}
    sync_attributes = {}
1011

1012
1013
1014
1015
1016
1017
    for name, value in class_attributes.iteritems():
        sync, underscore, rest = name.partition('_')
        if sync == 'sync' and underscore == '_':
            sync_attributes[rest] = value
        else:
            new_attributes[name] = value
1018

1019
1020
1021
1022
    if 'prefix' not in sync_attributes:
        m = ("you did not specify a 'sync_prefix' attribute "
             "in class '%s'" % (class_name,))
        raise ValueError(m)
1023

1024
1025
    prefix = sync_attributes.pop('prefix')
    class_name = sync_attributes.pop('classname', prefix + '_model')
1026

1027