models.py 62.5 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
69
from astakos.im.settings import (
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
    AUTH_TOKEN_DURATION, BILLING_FIELDS,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
70
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
71
    SITENAME, SERVICES, MODERATION_ENABLED)
72
from astakos.im import settings as astakos_settings
73
from astakos.im.endpoints.qh import (
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
74
    register_users, register_resources, qh_add_quota, QuotaLimits,
75
    qh_query_serials, qh_ack_serials)
76
from astakos.im import auth_providers
77
#from astakos.im.endpoints.aquarium.producer import report_user_event
root's avatar
root committed
78
#from astakos.im.tasks import propagate_groupmembers_quota
79

Olga Brani's avatar
Olga Brani committed
80
import astakos.im.messages as astakos_messages
81
from .managers import ForUpdateManager
82

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
83
84
logger = logging.getLogger(__name__)

Olga Brani's avatar
Olga Brani committed
85
DEFAULT_CONTENT_TYPE = None
86
87
88
89
90
91
92
93
94
95
96
97
98
_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
99
100
101

RESOURCE_SEPARATOR = '.'

102
inf = float('inf')
103

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

114
    def renew_token(self, expiration_date=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
115
116
117
118
119
120
121
        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()
122
123
124
125
        if expiration_date:
            self.auth_token_expires = expiration_date
        else:
            self.auth_token_expires = None
126

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
127
128
129
    def __str__(self):
        return self.name

Olga Brani's avatar
Olga Brani committed
130
131
132
133
134
135
136
137
    @property
    def resources(self):
        return self.resource_set.all()

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

Olga Brani's avatar
Olga Brani committed
139
140
141
142
143
144
145
146
147
148
149
    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)

150

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
151
class ResourceMetadata(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
152
153
    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
154

155

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
156
class Resource(models.Model):
157
    name = models.CharField(_('Name'), max_length=255)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
158
159
    meta = models.ManyToManyField(ResourceMetadata)
    service = models.ForeignKey(Service)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
160
161
162
    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
163

164
165
    class Meta:
        unique_together = ("name", "service")
166

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
167
    def __str__(self):
Olga Brani's avatar
Olga Brani committed
168
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
169

170
171
172
173
174
175
176
177
178
179
180
181
182
_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
183

184
class AstakosUserManager(UserManager):
185
186
187
188
189
190
191
192
193
194

    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)

195
196
197
    def get_by_email(self, email):
        return self.get(email=email)

198
199
200
201
202
203
204
205
206
207
208
209
    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
210
class AstakosUser(User):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
211
212
213
    """
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
214
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
215
216
217
218
                                   null=True)

    # DEPRECATED FIELDS: provider, third_party_identifier moved in
    #                    AstakosUserProvider model.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
219
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
220
221
                                null=True)
    # ex. screen_name for twitter, eppn for shibboleth
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
222
    third_party_identifier = models.CharField(_('Third-party identifier'),
223
224
225
                                              max_length=255, null=True,
                                              blank=True)

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
226

227
    #for invitations
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
228
    user_level = DEFAULT_USER_LEVEL
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
229
    level = models.IntegerField(_('Inviter level'), default=user_level)
230
    invitations = models.IntegerField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
231
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
232

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
233
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
234
                                  null=True, blank=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
235
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
236
    auth_token_expires = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
237
        _('Token expiration date'), null=True)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
238

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
244
    has_credits = models.BooleanField(_('Has credits?'), default=False)
245
    has_signed_terms = models.BooleanField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
246
        _('I agree with the terms'), default=False)
247
    date_signed_terms = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
248
        _('Signed terms date'), null=True, blank=True)
249
250

    activation_sent = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
251
        _('Activation sent data'), null=True, blank=True)
252
253
254
255

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
258
    __has_signed_terms = False
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
259
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
Olga Brani's avatar
Olga Brani committed
260
                                           default=False, db_index=True)
261
262

    objects = AstakosUserManager()
263

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
264
265
266
    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
267
        if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
268
            self.is_active = False
269

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
270
271
    @property
    def realname(self):
272
        return '%s %s' % (self.first_name, self.last_name)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
273

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
274
275
276
277
278
279
280
281
    @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
282

Olga Brani's avatar
Olga Brani committed
283
284
285
    def add_permission(self, pname):
        if self.has_perm(pname):
            return
286
287
288
289
        p, created = Permission.objects.get_or_create(
                                    codename=pname,
                                    name=pname.capitalize(),
                                    content_type=get_content_type())
Olga Brani's avatar
Olga Brani committed
290
291
292
293
294
295
        self.user_permissions.add(p)

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

299
300
301
    @property
    def invitation(self):
        try:
302
            return Invitation.objects.get(username=self.email)
303
304
        except Invitation.DoesNotExist:
            return None
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
305

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
306
307
    @property
    def quota(self):
Olga Brani's avatar
Olga Brani committed
308
        """Returns a dict with the sum of quota limits per resource"""
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
309
        d = defaultdict(int)
310
311
        default_quota = get_default_quota()
        d.update(default_quota)
Olga Brani's avatar
Olga Brani committed
312
        for q in self.policies:
313
            d[q.resource] += q.uplimit or inf
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
314
315
        for m in self.projectmembership_set.select_related().all():
            if not m.acceptance_date:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
316
                continue
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
317
318
            p = m.project
            if not p.is_active:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
319
                continue
320
            grants = p.application.projectresourcegrant_set.all()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
321
            for g in grants:
322
                d[str(g.resource)] += g.member_capacity or inf
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
323
324
        # TODO set default for remaining
        return d
325

Olga Brani's avatar
Olga Brani committed
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
    @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
355
356
357
358
359
360
361
362
363
    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
364
365
366
367
    @property
    def extended_groups(self):
        return self.membership_set.select_related().all()

368
369
370
    def save(self, update_timestamps=True, **kwargs):
        if update_timestamps:
            if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
371
                self.date_joined = datetime.now()
372
            self.updated = datetime.now()
373

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
374
375
376
        # update date_signed_terms if necessary
        if self.__has_signed_terms != self.has_signed_terms:
            self.date_signed_terms = datetime.now()
377

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
378
379
380
        self.update_uuid()

        if self.username != self.email.lower():
381
            # set username
382
            self.username = self.email.lower()
383

384
        self.validate_unique_email_isactive()
385

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

388
    def renew_token(self, flush_sessions=False, current_key=None):
389
        md5 = hashlib.md5()
390
        md5.update(settings.SECRET_KEY)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
391
        md5.update(self.username)
392
393
        md5.update(self.realname.encode('ascii', 'ignore'))
        md5.update(asctime())
394

395
396
397
        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
398
                                  timedelta(hours=AUTH_TOKEN_DURATION)
399
400
        if flush_sessions:
            self.flush_sessions(current_key)
401
        msg = 'Token renewed for %s' % self.email
402
        logger.log(LOGGING_LEVEL, msg)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
403

404
405
406
407
    def flush_sessions(self, current_key=None):
        q = self.sessions
        if current_key:
            q = q.exclude(session_key=current_key)
408

409
410
411
        keys = q.values_list('session_key', flat=True)
        if keys:
            msg = 'Flushing sessions: %s' % ','.join(keys)
412
            logger.log(LOGGING_LEVEL, msg, [])
413
414
415
416
417
        engine = import_module(settings.SESSION_ENGINE)
        for k in keys:
            s = engine.SessionStore(k)
            s.flush()

418
    def __unicode__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
419
        return '%s (%s)' % (self.realname, self.email)
420

421
    def conflicting_email(self):
422
        q = AstakosUser.objects.exclude(username=self.username)
423
        q = q.filter(email__iexact=self.email)
424
425
426
        if q.count() != 0:
            return True
        return False
427

428
    def validate_unique_email_isactive(self):
429
430
431
        """
        Implements a unique_together constraint for email and is_active fields.
        """
432
        q = AstakosUser.objects.all()
433
        q = q.filter(email = self.email)
434
435
        if self.id:
            q = q.filter(~Q(id = self.id))
436
        if q.count() != 0:
437
            m = 'Another account with the same email = %(email)s & \
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
438
                is_active = %(is_active)s found.' % self.__dict__
439
            raise ValidationError(m)
440

441
442
443
    def email_change_is_pending(self):
        return self.emailchanges.count() > 0

444
445
446
    def email_change_is_pending(self):
        return self.emailchanges.count() > 0

447
    @property
448
449
450
451
452
453
454
455
456
457
    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
458
            self.date_signed_terms = None
459
460
461
462
            self.save()
            return False
        return True

463
464
465
466
467
468
469
470
471
472
473
474
475
476
    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()

477
    def can_add_auth_provider(self, provider, **kwargs):
478
        provider_settings = auth_providers.get_provider(provider)
479
480

        if not provider_settings.is_available_for_add():
481
            return False
482

483
484
485
        if self.has_auth_provider(provider) and \
           provider_settings.one_per_user:
            return False
486

487
488
489
        if 'provider_info' in kwargs:
            kwargs.pop('provider_info')

490
491
492
493
494
495
496
497
498
499
        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

500
501
        return True

502
503
504
505
506
507
508
509
510
    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():
511
            return False
512

513
514
515
516
517
        return True

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

518
519
520
521
522
523
524
    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

525
526
527
528
529
    def has_auth_provider(self, provider, **kwargs):
        return bool(self.auth_providers.filter(module=provider,
                                               **kwargs).count())

    def add_auth_provider(self, provider, **kwargs):
530
531
        info_data = ''
        if 'provider_info' in kwargs:
532
533
534
            info_data = kwargs.pop('provider_info')
            if isinstance(info_data, dict):
                info_data = json.dumps(info_data)
535

536
        if self.can_add_auth_provider(provider, **kwargs):
537
538
539
            self.auth_providers.create(module=provider, active=True,
                                       info_data=info_data,
                                       **kwargs)
540
541
        else:
            raise Exception('Cannot add provider')
542
543
544
545
546
547
548
549
550
551

    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,
552
553
554
                               identifier=pending.third_party_identifier,
                                affiliation=pending.affiliation,
                                          provider_info=pending.info)
555

556
        if email_re.match(pending.email or '') and pending.email != self.email:
557
558
559
560
561
562
563
564
565
566
            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):
567
568
569
570
571
        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})
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593

    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():
594
            if self.can_add_auth_provider(module):
595
596
597
598
                providers.append(provider_settings(self))

        return providers

599
    def get_active_auth_providers(self, **filters):
600
        providers = []
601
        for provider in self.auth_providers.active(**filters):
602
603
604
605
            if auth_providers.get_provider(provider.module).is_available_for_login():
                providers.append(provider)
        return providers

606
607
608
609
    @property
    def auth_providers_display(self):
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))

610
611
612
613
614
615
616
617
    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
618
                if astakos_settings.MODERATION_ENABLED:
619
620
621
622
                    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) + \
623
                                u' ' + \
624
625
626
                                _('<a href="%s">%s?</a>') % (url,
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
        else:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
627
            if astakos_settings.MODERATION_ENABLED:
628
629
630
631
632
633
634
                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)))

635
        return mark_safe(message + u' '+ msg_extra)
636

637
638
639

class AstakosUserAuthProviderManager(models.Manager):

640
641
    def active(self, **filters):
        return self.filter(active=True, **filters)
642
643
644
645
646
647


class AstakosUserAuthProvider(models.Model):
    """
    Available user authentication methods.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
648
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
649
650
                                   null=True, default=None)
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
651
    module = models.CharField(_('Provider'), max_length=255, blank=False,
652
                                default='local')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
653
    identifier = models.CharField(_('Third-party identifier'),
654
655
656
                                              max_length=255, null=True,
                                              blank=True)
    active = models.BooleanField(default=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
657
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
658
                                   default='astakos')
659
    info_data = models.TextField(default="", null=True, blank=True)
660
    created = models.DateTimeField('Creation date', auto_now_add=True)
661
662
663
664
665

    objects = AstakosUserAuthProviderManager()

    class Meta:
        unique_together = (('identifier', 'module', 'user'), )
666
        ordering = ('module', 'created')
667

668
669
670
671
    def __init__(self, *args, **kwargs):
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
        try:
            self.info = json.loads(self.info_data)
672
673
674
            if not self.info:
                self.info = {}
        except Exception, e:
675
            self.info = {}
676

677
678
679
        for key,value in self.info.iteritems():
            setattr(self, 'info_%s' % key, value)

680
681
682
683
684
685
686

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

    @property
    def details_display(self):
687
688
689
690
        try:
          return self.settings.get_details_tpl_display % self.__dict__
        except:
          return ''
691
692
693
694
695
696
697
698
699

    @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
700
701
702
703
        try:
          return title_tpl % self.__dict__
        except:
          return self.settings.get_title_display % self.__dict__
704
705
706
707
708
709

    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)
710
711
712
        if self.module == 'local':
            self.user.set_unusable_password()
            self.user.save()
713
714
        return ret

715
716
717
    def __repr__(self):
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)

718
719
720
721
722
723
724
    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

725
726
727
    def save(self, *args, **kwargs):
        self.info_data = json.dumps(self.info)
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
728

729

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
730
class ExtendedManager(models.Manager):
Olga Brani's avatar
Olga Brani committed
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
    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
758

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
759
760

class AstakosUserQuota(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
761
762
763
    objects = ExtendedManager()
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
764
765
    resource = models.ForeignKey(Resource)
    user = models.ForeignKey(AstakosUser)
766

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
767
768
    class Meta:
        unique_together = ("resource", "user")
769

770

771
772
773
774
class ApprovalTerms(models.Model):
    """
    Model for approval terms
    """
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
775

776
    date = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
777
778
        _('Issue date'), db_index=True, default=datetime.now())
    location = models.CharField(_('Terms location'), max_length=255)
779

780

781
class Invitation(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
782
783
784
    """
    Model for registring invitations
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
785
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
786
                                null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
787
788
789
790
791
792
    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)
793

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
794
795
    def __init__(self, *args, **kwargs):
        super(Invitation, self).__init__(*args, **kwargs)
796
797
        if not self.id:
            self.code = _generate_invitation_code()
798

799
800
801
802
    def consume(self):
        self.is_consumed = True
        self.consumed = datetime.now()
        self.save()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
803

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

807
808

class EmailChangeManager(models.Manager):
809

810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
    @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:
829
830
            email_change = self.model.objects.get(
                activation_key=activation_key)
831
832
833
834
835
            if email_change.activation_key_expired():
                email_change.delete()
                raise EmailChange.DoesNotExist
            # is there an active user with this address?
            try:
836
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
837
838
839
            except AstakosUser.DoesNotExist:
                pass
            else:
840
                raise ValueError(_('The new email address is reserved.'))
841
842
            # update user
            user = AstakosUser.objects.get(pk=email_change.user_id)
843
            old_email = user.email
844
845
846
            user.email = email_change.new_email_address
            user.save()
            email_change.delete()
847
848
849
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
                                                          user.email)
            logger.log(LOGGING_LEVEL, msg)
850
851
            return user
        except EmailChange.DoesNotExist:
852
            raise ValueError(_('Invalid activation key.'))
853
854
855


class EmailChange(models.Model):
856
857
858
    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.'))
859
    user = models.ForeignKey(
860
        AstakosUser, unique=True, related_name='emailchanges')
861
    requested_at = models.DateTimeField(default=datetime.now())
862
863
    activation_key = models.CharField(
        max_length=40, unique=True, db_index=True)
864
865
866

    objects = EmailChangeManager()

867
868
869
870
    def get_url(self):
        return reverse('email_change_confirm',
                      kwargs={'activation_key': self.activation_key})

871
872
    def activation_key_expired(self):
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
873
874
        return self.requested_at + expiration_date < datetime.now()

875

876
877
878
879
880
class AdditionalMail(models.Model):
    """
    Model for registring invitations
    """
    owner = models.ForeignKey(AstakosUser)
881
    email = models.EmailField()
882

883

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
884
885
def _generate_invitation_code():
    while True:
886
        code = randint(1, 2L ** 63 - 1)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
887
888
889
890
891
892
        try:
            Invitation.objects.get(code=code)
            # An invitation with this code already exists, try again
        except Invitation.DoesNotExist:
            return code

893

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
894
895
896
897
898
899
900
901
def get_latest_terms():
    try:
        term = ApprovalTerms.objects.order_by('-id')[0]
        return term
    except IndexError:
        pass
    return None

902
903
904
905
class PendingThirdPartyUser(models.Model):
    """
    Model for registring successful third party user authentications
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
906
907
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
908
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
909
910
911
912
    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
913
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
914
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
915
    info = models.TextField(default="", null=True, blank=True)
916

917
918
    class Meta:
        unique_together = ("provider", "third_party_identifier")
919

920
921
922
923
924
925
926
927
928
929
930
    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

931
932
933
934
935
936
937
938
939
940
941
942
    @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]
943

944
945
946
947
948
949
950
951
952
953
954
    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)

955
956
957
958
959
    def generate_token(self):
        self.password = self.third_party_identifier
        self.last_login = datetime.now()
        self.token = default_token_generator.make_token(self)

960
961
962
963
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
964
965
966
967

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
968
class MemberJoinPolicy(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
969
970
971
972
973
974
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
    description = models.CharField(_('Description'), max_length=80)

    def __str__(self):
        return self.policy

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
975
class MemberLeavePolicy(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
976
977
978
979
980
981
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
    description = models.CharField(_('Description'), max_length=80)

    def __str__(self):
        return self.policy

982
def synced_model_metaclass(class_name, class_parents, class_attributes):
983

984
985
    new_attributes = {}
    sync_attributes = {}
986

987
988
989
990
991
992
    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
993

994
995
996
997
    if 'prefix' not in sync_attributes:
        m = ("you did not specify a 'sync_prefix' attribute "
             "in class '%s'" % (class_name,))
        raise ValueError(m)
998

999
1000
    prefix = sync_attributes.pop('prefix')
    class_name = sync_attributes.pop('classname', prefix + '_model')
1001

1002
1003
1004
1005
1006
1007
1008
    for name, value in sync_attributes.iteritems():
        newname = prefix + '_' + name
        if newname in new_attributes:
            m = ("class '%s' was specified with prefix '%s' "
                 "but it already has an attribute named '%s'"
                 % (class_name, prefix, newname))
            raise ValueError(m)
1009

1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
        new_attributes[newname] = value

    newclass = type(class_name, class_parents, new_attributes)
    return newclass


def make_synced(prefix=