models.py 74.2 KB
Newer Older
1
# Copyright 2011, 2012, 2013 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
import math
39
import copy
40

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

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
68
69
from astakos.im.settings import (
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
70
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
71
    SITENAME, MODERATION_ENABLED,
72
    PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES, PROJECT_ADMINS)
73
from astakos.im import settings as astakos_settings
74
from astakos.im import auth_providers as auth
75

Olga Brani's avatar
Olga Brani committed
76
import astakos.im.messages as astakos_messages
77
from snf_django.lib.db.managers import ForUpdateManager
78
from synnefo.lib.ordereddict import OrderedDict
79

80
from snf_django.lib.db.fields import intDecimalField
81
from synnefo.util.text import uenc, udec
82
from astakos.im import presentation
83

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

Olga Brani's avatar
Olga Brani committed
86
DEFAULT_CONTENT_TYPE = None
87
88
_content_type = None

89

90
91
92
93
94
95
def get_content_type():
    global _content_type
    if _content_type is not None:
        return _content_type

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

103
inf = float('inf')
104

105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

def dict_merge(a, b):
    """
    http://www.xormedia.com/recursively-merge-dictionaries-in-python/
    """
    if not isinstance(b, dict):
        return b
    result = copy.deepcopy(a)
    for k, v in b.iteritems():
        if k in result and isinstance(result[k], dict):
                result[k] = dict_merge(result[k], v)
        else:
            result[k] = copy.deepcopy(v)
    return result


Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
121
class Service(models.Model):
122
123
    name = models.CharField(_('Name'), max_length=255, unique=True,
                            db_index=True)
124
125
    url = models.CharField(_('Service url'), max_length=255, null=True,
                           help_text=_("URL the service is accessible from"))
126
    api_url = models.CharField(_('Service API url'), max_length=255, null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
127
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
128
                                  null=True, blank=True)
129
130
131
132
    auth_token_created = models.DateTimeField(_('Token creation date'),
                                              null=True)
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
                                              null=True)
133

134
    def renew_token(self, expiration_date=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
135
136
        md5 = hashlib.md5()
        md5.update(self.name.encode('ascii', 'ignore'))
137
        md5.update(self.api_url.encode('ascii', 'ignore'))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
138
139
140
141
        md5.update(asctime())

        self.auth_token = b64encode(md5.digest())
        self.auth_token_created = datetime.now()
142
143
144
145
        if expiration_date:
            self.auth_token_expires = expiration_date
        else:
            self.auth_token_expires = None
146

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
147
148
149
    def __str__(self):
        return self.name

150
151
152
153
154
155
156
    @classmethod
    def catalog(cls, orderfor=None):
        catalog = {}
        services = list(cls.objects.all())
        metadata = presentation.SERVICES
        metadata = dict_merge(presentation.SERVICES,
                              astakos_settings.SERVICES_META)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
157

158
        for service in services:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
159
160
161
            d = {'api_url': service.api_url,
                 'url': service.url,
                 'name': service.name}
162
163
            if service.name in metadata:
                metadata[service.name].update(d)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
164
165
            else:
                metadata[service.name] = d
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188

        def service_by_order(s):
            return s[1].get('order')

        def service_by_dashbaord_order(s):
            return s[1].get('dashboard').get('order')

        for service, info in metadata.iteritems():
            default_meta = presentation.service_defaults(service)
            base_meta = metadata.get(service, {})
            settings_meta = astakos_settings.SERVICES_META.get(service, {})
            service_meta = dict_merge(default_meta, base_meta)
            meta = dict_merge(service_meta, settings_meta)
            catalog[service] = meta

        order_key = service_by_order
        if orderfor == 'dashboard':
            order_key = service_by_dashbaord_order

        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
                                             key=order_key))
        return ordered_catalog

189

190
191
192
_presentation_data = {}
def get_presentation(resource):
    global _presentation_data
193
194
195
196
197
198
    resource_presentation = _presentation_data.get(resource, {})
    if not resource_presentation:
        resources_presentation = presentation.RESOURCES.get('resources', {})
        resource_presentation = resources_presentation.get(resource, {})
        _presentation_data[resource] = resource_presentation
    return resource_presentation
199

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
200
class Resource(models.Model):
201
    name = models.CharField(_('Name'), max_length=255, unique=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
202
    desc = models.TextField(_('Description'), null=True)
203
204
205
    service = models.CharField(_('Service identifier'), max_length=255,
                               null=True)
    unit = models.CharField(_('Unit'), null=True, max_length=255)
206
    uplimit = intDecimalField(default=0)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
207

208
209
    objects = ForUpdateManager()

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
210
    def __str__(self):
211
        return self.name
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
212

213
214
215
    def full_name(self):
        return str(self)

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
216
217
218
219
220
221
    def get_info(self):
        return {'service': str(self.service),
                'description': self.desc,
                'unit': self.unit,
                }

222
223
224
225
226
    @property
    def group(self):
        default = self.name
        return get_presentation(str(self)).get('group', default)

227
228
    @property
    def help_text(self):
229
230
        default = "%s resource" % self.name
        return get_presentation(str(self)).get('help_text', default)
231

232
233
    @property
    def help_text_input_each(self):
234
235
        default = "%s resource" % self.name
        return get_presentation(str(self)).get('help_text_input_each', default)
236
237
238
239
240
241
242

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

    @property
    def report_desc(self):
243
244
        default = "%s resource" % self.name
        return get_presentation(str(self)).get('report_desc', default)
245
246
247

    @property
    def placeholder(self):
248
        return get_presentation(str(self)).get('placeholder', self.unit)
249
250
251

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

254
255
256
257
258
259
260
261
262
263
264
265
266
    @property
    def display_name(self):
        name = self.verbose_name
        if self.is_abbreviation:
            name = name.upper()
        return name

    @property
    def pluralized_display_name(self):
        if not self.unit:
            return '%ss' % self.display_name
        return self.display_name

267
268
def get_resource_names():
    _RESOURCE_NAMES = []
269
    resources = Resource.objects.select_related('service').all()
270
271
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
    return _RESOURCE_NAMES
272

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
273

274
class AstakosUserManager(UserManager):
275
276
277
278
279
280
281
282
283
284

    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)

285
286
287
    def get_by_email(self, email):
        return self.get(email=email)

288
289
290
291
292
293
294
295
296
    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)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
297
298
299
300
301
302
303
304
        qextra = Q(**kwargs)
        return self.filter((qemail | qusername) & qextra).exists()

    def verified_user_exists(self, email_or_username):
        return self.user_exists(email_or_username, email_verified=True)

    def verified(self):
        return self.filter(email_verified=True)
305

306
307
308
309
310
311
312
313
314
315
316
317
318
    def uuid_catalog(self, l=None):
        """
        Returns a uuid to username mapping for the uuids appearing in l.
        If l is None returns the mapping for all existing users.
        """
        q = self.filter(uuid__in=l) if l != None else self
        return dict(q.values_list('uuid', 'username'))

    def displayname_catalog(self, l=None):
        """
        Returns a username to uuid mapping for the usernames appearing in l.
        If l is None returns the mapping for all existing users.
        """
319
320
321
322
323
324
325
326
        if l is not None:
            lmap = dict((x.lower(), x) for x in l)
            q = self.filter(username__in=lmap.keys())
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
        else:
            q = self
            values = self.values_list('username', 'uuid')
        return dict(values)
327
328


329

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
330
class AstakosUser(User):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
331
332
333
    """
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
334
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
335
336
337
338
                                   null=True)

    # DEPRECATED FIELDS: provider, third_party_identifier moved in
    #                    AstakosUserProvider model.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
339
    provider = models.CharField(_('Provider'), max_length=255, blank=True,
340
341
                                null=True)
    # ex. screen_name for twitter, eppn for shibboleth
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
342
    third_party_identifier = models.CharField(_('Third-party identifier'),
343
344
345
                                              max_length=255, null=True,
                                              blank=True)

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
346

347
    #for invitations
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
348
    user_level = DEFAULT_USER_LEVEL
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
349
    level = models.IntegerField(_('Inviter level'), default=user_level)
350
    invitations = models.IntegerField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
351
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
352

353
    auth_token = models.CharField(_('Authentication Token'),
Olga Brani's avatar
Olga Brani committed
354
                                  max_length=32,
355
356
                                  null=True,
                                  blank=True,
357
358
359
360
361
                                  help_text = _('Renew your authentication '
                                                'token. Make sure to set the new '
                                                'token in any client you may be '
                                                'using, to preserve its '
                                                'functionality.'))
362
    auth_token_created = models.DateTimeField(_('Token creation date'),
Olga Brani's avatar
Olga Brani committed
363
                                              null=True)
364
    auth_token_expires = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
365
        _('Token expiration date'), null=True)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
366

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
372
    has_credits = models.BooleanField(_('Has credits?'), default=False)
373
    has_signed_terms = models.BooleanField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
374
        _('I agree with the terms'), default=False)
375
    date_signed_terms = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
376
        _('Signed terms date'), null=True, blank=True)
377
378

    activation_sent = models.DateTimeField(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
379
        _('Activation sent data'), null=True, blank=True)
380
381
382
383

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
386
    __has_signed_terms = False
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
387
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
Olga Brani's avatar
Olga Brani committed
388
                                           default=False, db_index=True)
389
390

    objects = AstakosUserManager()
391

392
393
    forupdate = ForUpdateManager()

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
394
395
396
    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
397
        if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
398
            self.is_active = False
399

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
400
401
    @property
    def realname(self):
402
        return '%s %s' % (self.first_name, self.last_name)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
403

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
404
405
406
407
408
409
    @property
    def log_display(self):
        """
        Should be used in all logger.* calls that refer to a user so that
        user display is consistent across log entries.
        """
410
        return '%s::%s' % (self.uuid, self.email)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
411

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
412
413
414
415
416
417
418
419
    @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
420

Olga Brani's avatar
Olga Brani committed
421
422
423
    def add_permission(self, pname):
        if self.has_perm(pname):
            return
424
425
426
427
        p, created = Permission.objects.get_or_create(
                                    codename=pname,
                                    name=pname.capitalize(),
                                    content_type=get_content_type())
Olga Brani's avatar
Olga Brani committed
428
429
430
431
432
433
        self.user_permissions.add(p)

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

437
438
439
    def is_project_admin(self, application_id=None):
        return self.uuid in PROJECT_ADMINS

440
441
442
    @property
    def invitation(self):
        try:
443
            return Invitation.objects.get(username=self.email)
444
445
        except Invitation.DoesNotExist:
            return None
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
446

Olga Brani's avatar
Olga Brani committed
447
448
449
450
451
452
453
    @property
    def policies(self):
        return self.astakosuserquota_set.select_related().all()

    @policies.setter
    def policies(self, policies):
        for p in policies:
454
455
456
457
458
459
            p.setdefault('resource', '')
            p.setdefault('capacity', 0)
            p.setdefault('update', True)
            self.add_resource_policy(**p)

    def add_resource_policy(
460
            self, resource, capacity,
461
            update=True):
Olga Brani's avatar
Olga Brani committed
462
        """Raises ObjectDoesNotExist, IntegrityError"""
463
        resource = Resource.objects.get(name=resource)
Olga Brani's avatar
Olga Brani committed
464
        if update:
465
466
467
            AstakosUserQuota.objects.update_or_create(
                user=self, resource=resource, defaults={
                    'capacity':capacity,
468
                    })
Olga Brani's avatar
Olga Brani committed
469
470
        else:
            q = self.astakosuserquota_set
471
            q.create(
472
                resource=resource, capacity=capacity,
473
                )
Olga Brani's avatar
Olga Brani committed
474

475
    def get_resource_policy(self, resource):
476
        resource = Resource.objects.get(name=resource)
477
        default_capacity = resource.uplimit
478
        try:
479
480
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
            return policy, default_capacity
481
        except AstakosUserQuota.DoesNotExist:
482
            return None, default_capacity
483

484
    def remove_resource_policy(self, service, resource):
Olga Brani's avatar
Olga Brani committed
485
        """Raises ObjectDoesNotExist, IntegrityError"""
486
        resource = Resource.objects.get(name=resource)
Olga Brani's avatar
Olga Brani committed
487
488
        q = self.policies.get(resource=resource).delete()

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
489
490
491
492
493
494
495
496
497
    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

498
499
500
    def save(self, update_timestamps=True, **kwargs):
        if update_timestamps:
            if not self.id:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
501
                self.date_joined = datetime.now()
502
            self.updated = datetime.now()
503

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
504
505
506
        # update date_signed_terms if necessary
        if self.__has_signed_terms != self.has_signed_terms:
            self.date_signed_terms = datetime.now()
507

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
508
509
510
        self.update_uuid()

        if self.username != self.email.lower():
511
            # set username
512
            self.username = self.email.lower()
513

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

516
    def renew_token(self, flush_sessions=False, current_key=None):
517
        md5 = hashlib.md5()
518
        md5.update(settings.SECRET_KEY)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
519
        md5.update(self.username)
520
521
        md5.update(self.realname.encode('ascii', 'ignore'))
        md5.update(asctime())
522

523
524
525
        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
526
                                  timedelta(hours=AUTH_TOKEN_DURATION)
527
528
        if flush_sessions:
            self.flush_sessions(current_key)
529
        msg = 'Token renewed for %s' % self.email
530
        logger.log(LOGGING_LEVEL, msg)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
531

532
533
534
535
    def flush_sessions(self, current_key=None):
        q = self.sessions
        if current_key:
            q = q.exclude(session_key=current_key)
536

537
538
539
        keys = q.values_list('session_key', flat=True)
        if keys:
            msg = 'Flushing sessions: %s' % ','.join(keys)
540
            logger.log(LOGGING_LEVEL, msg, [])
541
542
543
544
545
        engine = import_module(settings.SESSION_ENGINE)
        for k in keys:
            s = engine.SessionStore(k)
            s.flush()

546
    def __unicode__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
547
        return '%s (%s)' % (self.realname, self.email)
548

549
    def conflicting_email(self):
550
        q = AstakosUser.objects.exclude(username=self.username)
551
        q = q.filter(email__iexact=self.email)
552
553
554
        if q.count() != 0:
            return True
        return False
555

556
557
558
    def email_change_is_pending(self):
        return self.emailchanges.count() > 0

559
    @property
560
561
562
563
564
565
566
567
568
569
    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
570
            self.date_signed_terms = None
571
572
573
574
            self.save()
            return False
        return True

575
576
577
578
579
580
581
582
    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)

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

586
587
588
    def can_change_email(self):
        if not self.has_auth_provider('local'):
            return True
589

590
591
        local = self.get_auth_provider('local')._instance
        return local.auth_backend == 'astakos'
592

593
594
595
596
    # Auth providers related methods
    def get_auth_provider(self, module=None, identifier=None, **filters):
        if not module:
            return self.auth_providers.active()[0].settings
597

598
599
600
601
602
        params = {'module': module}
        if identifier:
            params['identifier'] = identifier
        params.update(filters)
        return self.auth_providers.active().get(**params).settings
603

604
605
606
    def has_auth_provider(self, provider, **kwargs):
        return bool(self.auth_providers.active().filter(module=provider,
                                                        **kwargs).count())
607

608
609
    def get_required_providers(self, **kwargs):
        return auth.REQUIRED_PROVIDERS.keys()
610

611
612
613
    def missing_required_providers(self):
        required = self.get_required_providers()
        missing = []
614
615
        for provider in required:
            if not self.has_auth_provider(provider):
616
617
                missing.append(auth.get_provider(provider, self))
        return missing
618

619
    def get_available_auth_providers(self, **filters):
620
        """
621
        Returns a list of providers available for add by the user.
622
        """
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
        modules = astakos_settings.IM_MODULES
        providers = []
        for p in modules:
            providers.append(auth.get_provider(p, self))
        available = []

        for p in providers:
            if p.get_add_policy:
                available.append(p)
        return available

    def get_disabled_auth_providers(self, **filters):
        providers = self.get_auth_providers(**filters)
        disabled = []
        for p in providers:
            if not p.get_login_policy:
                disabled.append(p)
        return disabled

    def get_enabled_auth_providers(self, **filters):
        providers = self.get_auth_providers(**filters)
        enabled = []
        for p in providers:
            if p.get_login_policy:
                enabled.append(p)
        return enabled

    def get_auth_providers(self, **filters):
        providers = []
        for provider in self.auth_providers.active(**filters):
            if provider.settings.module_enabled:
                providers.append(provider.settings)
655

656
        modules = astakos_settings.IM_MODULES
657

658
659
660
661
662
663
664
        def key(p):
            if not p.module in modules:
                return 100
            return modules.index(p.module)

        providers = sorted(providers, key=key)
        return providers
665

666
667
668
669
670
    # URL methods
    @property
    def auth_providers_display(self):
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
                         self.get_enabled_auth_providers()])
671

672
673
674
    def add_auth_provider(self, module='local', identifier=None, **params):
        provider = auth.get_provider(module, self, identifier, **params)
        provider.add_to_user()
675
676

    def get_resend_activation_url(self):
677
678
        return reverse('send_activation', kwargs={'user_id': self.pk})

679
680
681
682
683
684
685
686
687
688
689
690
    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)})

691
692
    def get_inactive_message(self, provider_module, identifier=None):
        provider = self.get_auth_provider(provider_module, identifier)
693

694
695
        msg_extra = ''
        message = ''
696
697
698
699
700
701
702
703

        msg_inactive = provider.get_account_inactive_msg
        msg_pending = provider.get_pending_activation_msg
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
        msg_pending_mod = provider.get_pending_moderation_msg
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)

704
705
        if self.activation_sent:
            if self.email_verified:
706
                message = msg_inactive
707
            else:
708
709
710
711
712
                message = msg_pending
                url = self.get_resend_activation_url()
                msg_extra = msg_pending_help + \
                            u' ' + \
                            '<a href="%s">%s?</a>' % (url, msg_resend)
713
        else:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
714
            if astakos_settings.MODERATION_ENABLED:
715
                message = msg_pending_mod
716
            else:
717
                message = msg_pending
718
                url = self.get_resend_activation_url()
719
720
                msg_extra = '<a href="%s">%s?</a>' % (url, \
                                msg_resend)
721

722
        return mark_safe(message + u' '+ msg_extra)
723

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
724
725
726
    def owns_application(self, application):
        return application.owner == self

727
    def owns_project(self, project):
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
728
        return project.application.owner == self
729

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
730
731
732
733
734
735
736
    def is_associated(self, project):
        try:
            m = ProjectMembership.objects.get(person=self, project=project)
            return m.state in ProjectMembership.ASSOCIATED_STATES
        except ProjectMembership.DoesNotExist:
            return False

737
738
739
740
741
742
743
    def get_membership(self, project):
        try:
            return ProjectMembership.objects.get(
                project=project,
                person=self)
        except ProjectMembership.DoesNotExist:
            return None
744

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
745
746
747
748
749
750
751
    def membership_display(self, project):
        m = self.get_membership(project)
        if m is None:
            return _('Not a member')
        else:
            return m.user_friendly_state_display()

752
    def non_owner_can_view(self, maybe_project):
753
754
        if self.is_project_admin():
            return True
755
756
757
758
759
760
761
762
763
        if maybe_project is None:
            return False
        project = maybe_project
        if self.is_associated(project):
            return True
        if project.is_deactivated():
            return False
        return True

764
765
766
    def settings(self):
        return UserSetting.objects.filter(user=self)

767

768
769
class AstakosUserAuthProviderManager(models.Manager):

770
771
    def active(self, **filters):
        return self.filter(active=True, **filters)
772

773
774
    def remove_unverified_providers(self, provider, **filters):
        try:
775
776
            existing = self.filter(module=provider, user__email_verified=False,
                                   **filters)
777
778
779
780
781
            for p in existing:
                p.user.delete()
        except:
            pass

782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
    def unverified(self, provider, **filters):
        try:
            return self.get(module=provider, user__email_verified=False,
                            **filters).settings
        except AstakosUserAuthProvider.DoesNotExist:
            return None

    def verified(self, provider, **filters):
        try:
            return self.get(module=provider, user__email_verified=True,
                            **filters).settings
        except AstakosUserAuthProvider.DoesNotExist:
            return None


class AuthProviderPolicyProfileManager(models.Manager):

    def active(self):
        return self.filter(active=True)

    def for_user(self, user, provider):
        policies = {}
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
        exclusive_q = exclusive_q1 | exclusive_q2

        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
            policies.update(profile.policies)

        user_groups = user.groups.all().values('pk')
        for profile in self.active().filter(groups__in=user_groups).filter(
                exclusive_q):
            policies.update(profile.policies)
        return policies

    def add_policy(self, name, provider, group_or_user, exclusive=False,
                   **policies):
        is_group = isinstance(group_or_user, Group)
        profile, created = self.get_or_create(name=name, provider=provider,
                                              is_exclusive=exclusive)
        profile.is_exclusive = exclusive
        profile.save()
        if is_group:
            profile.groups.add(group_or_user)
        else:
            profile.users.add(group_or_user)
        profile.set_policies(policies)
        profile.save()
        return profile


class AuthProviderPolicyProfile(models.Model):
    name = models.CharField(_('Name'), max_length=255, blank=False,
                            null=False, db_index=True)
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
                                null=False)

    # apply policies to all providers excluding the one set in provider field
    is_exclusive = models.BooleanField(default=False)

    policy_add = models.NullBooleanField(null=True, default=None)
    policy_remove = models.NullBooleanField(null=True, default=None)
    policy_create = models.NullBooleanField(null=True, default=None)
    policy_login = models.NullBooleanField(null=True, default=None)
    policy_limit = models.IntegerField(null=True, default=None)
    policy_required = models.NullBooleanField(null=True, default=None)
    policy_automoderate = models.NullBooleanField(null=True, default=None)
    policy_switch = models.NullBooleanField(null=True, default=None)

    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
                     'automoderate')

    priority = models.IntegerField(null=False, default=1)
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
    users = models.ManyToManyField(AstakosUser,
                                   related_name='authpolicy_profiles')
    active = models.BooleanField(default=True)

    objects = AuthProviderPolicyProfileManager()

    class Meta:
        ordering = ['priority']

    @property
    def policies(self):
        policies = {}
        for pkey in self.POLICY_FIELDS:
            value = getattr(self, 'policy_%s' % pkey, None)
            if value is None:
                continue
            policies[pkey] = value
        return policies

    def set_policies(self, policies_dict):
        for key, value in policies_dict.iteritems():
            if key in self.POLICY_FIELDS:
                setattr(self, 'policy_%s' % key, value)
        return self.policies
880

881
882
883
884
885

class AstakosUserAuthProvider(models.Model):
    """
    Available user authentication methods.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
886
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
887
888
                                   null=True, default=None)
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
889
    module = models.CharField(_('Provider'), max_length=255, blank=False,
890
                                default='local')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
891
    identifier = models.CharField(_('Third-party identifier'),
892
893
894
                                              max_length=255, null=True,
                                              blank=True)
    active = models.BooleanField(default=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
895
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
896
                                   default='astakos')
897
    info_data = models.TextField(default="", null=True, blank=True)
898
    created = models.DateTimeField('Creation date', auto_now_add=True)
899
900
901
902
903

    objects = AstakosUserAuthProviderManager()

    class Meta:
        unique_together = (('identifier', 'module', 'user'), )
904
        ordering = ('module', 'created')
905

906
907
908
909
    def __init__(self, *args, **kwargs):
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
        try:
            self.info = json.loads(self.info_data)
910
911
912
            if not self.info:
                self.info = {}
        except Exception, e:
913
            self.info = {}
914

915
916
917
        for key,value in self.info.iteritems():
            setattr(self, 'info_%s' % key, value)

918
919
    @property
    def settings(self):
920
        extra_data = {}
921

922
923
924
        info_data = {}
        if self.info_data:
            info_data = json.loads(self.info_data)
925

926
        extra_data['info'] = info_data
927

928
929
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
            extra_data[key] = getattr(self, key)
930

931
932
933
        extra_data['instance'] = self
        return auth.get_provider(self.module, self.user,
                                           self.identifier, **extra_data)
934

935
936
937
    def __repr__(self):
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)

938
939
940
941
942
943
944
    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

945
946
947
    def save(self, *args, **kwargs):
        self.info_data = json.dumps(self.info)
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
948

949

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
950
class ExtendedManager(models.Manager):
Olga Brani's avatar
Olga Brani committed
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
    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
978

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
979
980

class AstakosUserQuota(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
981
    objects = ExtendedManager()
982
    capacity = intDecimalField()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
983
984
    resource = models.ForeignKey(Resource)
    user = models.ForeignKey(AstakosUser)
985

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
986
987
    class Meta:
        unique_together = ("resource", "user")
988

989

990
991
992
993
class ApprovalTerms(models.Model):
    """
    Model for approval terms
    """
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
994

995
    date = models.DateTimeField(
996
        _('Issue date'), db_index=True, auto_now_add=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
997
    location = models.CharField(_('Terms location'), max_length=255)
998

999

1000
class Invitation(models.Model):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1001
1002
1003
    """
    Model for registring invitations
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1004
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1005
                                null=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1006
1007
1008
1009
1010
1011
    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)
1012

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
1013
1014
    def __init__(self, *args, **kwargs):
        super(Invitation, self).__init__(*args, **kwargs)
1015
1016
        if not self.id:
            self.code = _generate_invitation_code()
1017

1018
1019
1020
1021
    def consume(self):
        self.is_consumed = True
        self.consumed = datetime.now()
        self.save()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
1022

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