Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ e5966bd9

History | View | Annotate | Download (41.8 kB)

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

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

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

    
47
from django.db import models, IntegrityError
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
54
)
55
from django.contrib.contenttypes.models import ContentType
56

    
57
from django.dispatch import Signal
58
from django.db.models import Q
59
from django.core.urlresolvers import reverse
60
from django.utils.http import int_to_base36
61
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

    
67
from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
68
                                 AUTH_TOKEN_DURATION, BILLING_FIELDS,
69
                                 EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
70
from astakos.im import settings as astakos_settings
71
from astakos.im.endpoints.qh import (
72
    register_users, send_quota, register_resources
73
)
74
from astakos.im import auth_providers
75
from astakos.im.endpoints.aquarium.producer import report_user_event
76
from astakos.im.functions import send_invitation
77
#from astakos.im.tasks import propagate_groupmembers_quota
78

    
79
import astakos.im.messages as astakos_messages
80

    
81
logger = logging.getLogger(__name__)
82

    
83
DEFAULT_CONTENT_TYPE = None
84
try:
85
    content_type = ContentType.objects.get(app_label='im', model='astakosuser')
86
except:
87
    content_type = DEFAULT_CONTENT_TYPE
88

    
89
RESOURCE_SEPARATOR = '.'
90

    
91
inf = float('inf')
92

    
93
class Service(models.Model):
94
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
95
    url = models.FilePathField()
96
    icon = models.FilePathField(blank=True)
97
    auth_token = models.CharField('Authentication Token', max_length=32,
98
                                  null=True, blank=True)
99
    auth_token_created = models.DateTimeField('Token creation date', null=True)
100
    auth_token_expires = models.DateTimeField(
101
        'Token expiration date', null=True)
102

    
103
    def renew_token(self):
104
        md5 = hashlib.md5()
105
        md5.update(self.name.encode('ascii', 'ignore'))
106
        md5.update(self.url.encode('ascii', 'ignore'))
107
        md5.update(asctime())
108

    
109
        self.auth_token = b64encode(md5.digest())
110
        self.auth_token_created = datetime.now()
111
        self.auth_token_expires = self.auth_token_created + \
112
            timedelta(hours=AUTH_TOKEN_DURATION)
113

    
114
    def __str__(self):
115
        return self.name
116

    
117
    @property
118
    def resources(self):
119
        return self.resource_set.all()
120

    
121
    @resources.setter
122
    def resources(self, resources):
123
        for s in resources:
124
            self.resource_set.create(**s)
125

    
126
    def add_resource(self, service, resource, uplimit, update=True):
127
        """Raises ObjectDoesNotExist, IntegrityError"""
128
        resource = Resource.objects.get(service__name=service, name=resource)
129
        if update:
130
            AstakosUserQuota.objects.update_or_create(user=self,
131
                                                      resource=resource,
132
                                                      defaults={'uplimit': uplimit})
133
        else:
134
            q = self.astakosuserquota_set
135
            q.create(resource=resource, uplimit=uplimit)
136

    
137

    
138
class ResourceMetadata(models.Model):
139
    key = models.CharField('Name', max_length=255, unique=True, db_index=True)
140
    value = models.CharField('Value', max_length=255)
141

    
142

    
143
class Resource(models.Model):
144
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
145
    meta = models.ManyToManyField(ResourceMetadata)
146
    service = models.ForeignKey(Service)
147
    desc = models.TextField('Description', null=True)
148
    unit = models.CharField('Name', null=True, max_length=255)
149
    group = models.CharField('Group', null=True, max_length=255)
150

    
151
    def __str__(self):
152
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
153

    
154

    
155
class GroupKind(models.Model):
156
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
157

    
158
    def __str__(self):
159
        return self.name
160

    
161

    
162
class AstakosGroup(Group):
163
    kind = models.ForeignKey(GroupKind)
164
    homepage = models.URLField(
165
        'Homepage Url', max_length=255, null=True, blank=True)
166
    desc = models.TextField('Description', null=True)
167
    policy = models.ManyToManyField(
168
        Resource,
169
        null=True,
170
        blank=True,
171
        through='AstakosGroupQuota'
172
    )
173
    creation_date = models.DateTimeField(
174
        'Creation date',
175
        default=datetime.now()
176
    )
177
    issue_date = models.DateTimeField('Start date', null=True)
178
    expiration_date = models.DateTimeField(
179
        'End date',
180
         null=True
181
    )
182
    moderation_enabled = models.BooleanField(
183
        'Moderated membership?',
184
        default=True
185
    )
186
    approval_date = models.DateTimeField(
187
        'Activation date',
188
        null=True,
189
        blank=True
190
    )
191
    estimated_participants = models.PositiveIntegerField(
192
        'Estimated #members',
193
        null=True,
194
        blank=True,
195
    )
196
    max_participants = models.PositiveIntegerField(
197
        'Maximum numder of participants',
198
        null=True,
199
        blank=True
200
    )
201

    
202
    @property
203
    def is_disabled(self):
204
        if not self.approval_date:
205
            return True
206
        return False
207

    
208
    @property
209
    def is_enabled(self):
210
        if self.is_disabled:
211
            return False
212
        if not self.issue_date:
213
            return False
214
        if not self.expiration_date:
215
            return True
216
        now = datetime.now()
217
        if self.issue_date > now:
218
            return False
219
        if now >= self.expiration_date:
220
            return False
221
        return True
222

    
223
    def enable(self):
224
        if self.is_enabled:
225
            return
226
        self.approval_date = datetime.now()
227
        self.save()
228
        quota_disturbed.send(sender=self, users=self.approved_members)
229
        #propagate_groupmembers_quota.apply_async(
230
        #    args=[self], eta=self.issue_date)
231
        #propagate_groupmembers_quota.apply_async(
232
        #    args=[self], eta=self.expiration_date)
233

    
234
    def disable(self):
235
        if self.is_disabled:
236
            return
237
        self.approval_date = None
238
        self.save()
239
        quota_disturbed.send(sender=self, users=self.approved_members)
240

    
241
    @transaction.commit_manually
242
    def approve_member(self, person):
243
        m, created = self.membership_set.get_or_create(person=person)
244
        try:
245
            m.approve()
246
        except:
247
            transaction.rollback()
248
            raise
249
        else:
250
            transaction.commit()
251

    
252
#     def disapprove_member(self, person):
253
#         self.membership_set.remove(person=person)
254

    
255
    @property
256
    def members(self):
257
        q = self.membership_set.select_related().all()
258
        return [m.person for m in q]
259

    
260
    @property
261
    def approved_members(self):
262
        q = self.membership_set.select_related().all()
263
        return [m.person for m in q if m.is_approved]
264

    
265
    @property
266
    def quota(self):
267
        d = defaultdict(int)
268
        for q in self.astakosgroupquota_set.select_related().all():
269
            d[q.resource] += q.uplimit or inf
270
        return d
271

    
272
    def add_policy(self, service, resource, uplimit, update=True):
273
        """Raises ObjectDoesNotExist, IntegrityError"""
274
        resource = Resource.objects.get(service__name=service, name=resource)
275
        if update:
276
            AstakosGroupQuota.objects.update_or_create(
277
                group=self,
278
                resource=resource,
279
                defaults={'uplimit': uplimit}
280
            )
281
        else:
282
            q = self.astakosgroupquota_set
283
            q.create(resource=resource, uplimit=uplimit)
284

    
285
    @property
286
    def policies(self):
287
        return self.astakosgroupquota_set.select_related().all()
288

    
289
    @policies.setter
290
    def policies(self, policies):
291
        for p in policies:
292
            service = p.get('service', None)
293
            resource = p.get('resource', None)
294
            uplimit = p.get('uplimit', 0)
295
            update = p.get('update', True)
296
            self.add_policy(service, resource, uplimit, update)
297

    
298
    @property
299
    def owners(self):
300
        return self.owner.all()
301

    
302
    @property
303
    def owner_details(self):
304
        return self.owner.select_related().all()
305

    
306
    @owners.setter
307
    def owners(self, l):
308
        self.owner = l
309
        map(self.approve_member, l)
310

    
311

    
312

    
313
class AstakosUserManager(UserManager):
314

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

    
324
    def get_by_email(self, email):
325
        return self.get(email=email)
326

    
327
    def get_by_identifier(self, email_or_username, **kwargs):
328
        try:
329
            return self.get(email__iexact=email_or_username, **kwargs)
330
        except AstakosUser.DoesNotExist:
331
            return self.get(username__iexact=email_or_username, **kwargs)
332

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

    
338

    
339
class AstakosUser(User):
340
    """
341
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
342
    """
343
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
344
                                   null=True)
345

    
346
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
347
    #                    AstakosUserProvider model.
348
    provider = models.CharField('Provider', max_length=255, blank=True,
349
                                null=True)
350
    # ex. screen_name for twitter, eppn for shibboleth
351
    third_party_identifier = models.CharField('Third-party identifier',
352
                                              max_length=255, null=True,
353
                                              blank=True)
354

    
355

    
356
    #for invitations
357
    user_level = DEFAULT_USER_LEVEL
358
    level = models.IntegerField('Inviter level', default=user_level)
359
    invitations = models.IntegerField(
360
        'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
361

    
362
    auth_token = models.CharField('Authentication Token', max_length=32,
363
                                  null=True, blank=True)
364
    auth_token_created = models.DateTimeField('Token creation date', null=True)
365
    auth_token_expires = models.DateTimeField(
366
        'Token expiration date', null=True)
367

    
368
    updated = models.DateTimeField('Update date')
369
    is_verified = models.BooleanField('Is verified?', default=False)
370

    
371
    email_verified = models.BooleanField('Email verified?', default=False)
372

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

    
379
    activation_sent = models.DateTimeField(
380
        'Activation sent data', null=True, blank=True)
381

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

    
385
    astakos_groups = models.ManyToManyField(
386
        AstakosGroup, verbose_name=_('agroups'), blank=True,
387
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
388
        through='Membership')
389

    
390
    __has_signed_terms = False
391
    disturbed_quota = models.BooleanField('Needs quotaholder syncing',
392
                                           default=False, db_index=True)
393

    
394
    objects = AstakosUserManager()
395

    
396
    owner = models.ManyToManyField(
397
        AstakosGroup, related_name='owner', null=True)
398

    
399
    def __init__(self, *args, **kwargs):
400
        super(AstakosUser, self).__init__(*args, **kwargs)
401
        self.__has_signed_terms = self.has_signed_terms
402
        if not self.id:
403
            self.is_active = False
404

    
405
    @property
406
    def realname(self):
407
        return '%s %s' % (self.first_name, self.last_name)
408

    
409
    @realname.setter
410
    def realname(self, value):
411
        parts = value.split(' ')
412
        if len(parts) == 2:
413
            self.first_name = parts[0]
414
            self.last_name = parts[1]
415
        else:
416
            self.last_name = parts[0]
417

    
418
    def add_permission(self, pname):
419
        if self.has_perm(pname):
420
            return
421
        p, created = Permission.objects.get_or_create(codename=pname,
422
                                                      name=pname.capitalize(),
423
                                                      content_type=content_type)
424
        self.user_permissions.add(p)
425

    
426
    def remove_permission(self, pname):
427
        if self.has_perm(pname):
428
            return
429
        p = Permission.objects.get(codename=pname,
430
                                   content_type=content_type)
431
        self.user_permissions.remove(p)
432

    
433
    @property
434
    def invitation(self):
435
        try:
436
            return Invitation.objects.get(username=self.email)
437
        except Invitation.DoesNotExist:
438
            return None
439

    
440
    def invite(self, email, realname):
441
        inv = Invitation(inviter=self, username=email, realname=realname)
442
        inv.save()
443
        send_invitation(inv)
444
        self.invitations = max(0, self.invitations - 1)
445
        self.save()
446

    
447
    @property
448
    def quota(self):
449
        """Returns a dict with the sum of quota limits per resource"""
450
        d = defaultdict(int)
451
        for q in self.policies:
452
            d[q.resource] += q.uplimit or inf
453
        for m in self.extended_groups:
454
            if not m.is_approved:
455
                continue
456
            g = m.group
457
            if not g.is_enabled:
458
                continue
459
            for r, uplimit in g.quota.iteritems():
460
                d[r] += uplimit or inf
461
        # TODO set default for remaining
462
        return d
463

    
464
    @property
465
    def policies(self):
466
        return self.astakosuserquota_set.select_related().all()
467

    
468
    @policies.setter
469
    def policies(self, policies):
470
        for p in policies:
471
            service = policies.get('service', None)
472
            resource = policies.get('resource', None)
473
            uplimit = policies.get('uplimit', 0)
474
            update = policies.get('update', True)
475
            self.add_policy(service, resource, uplimit, update)
476

    
477
    def add_policy(self, service, resource, uplimit, update=True):
478
        """Raises ObjectDoesNotExist, IntegrityError"""
479
        resource = Resource.objects.get(service__name=service, name=resource)
480
        if update:
481
            AstakosUserQuota.objects.update_or_create(user=self,
482
                                                      resource=resource,
483
                                                      defaults={'uplimit': uplimit})
484
        else:
485
            q = self.astakosuserquota_set
486
            q.create(resource=resource, uplimit=uplimit)
487

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

    
493
    @property
494
    def extended_groups(self):
495
        return self.membership_set.select_related().all()
496

    
497
    @extended_groups.setter
498
    def extended_groups(self, groups):
499
        #TODO exceptions
500
        for name in (groups or ()):
501
            group = AstakosGroup.objects.get(name=name)
502
            self.membership_set.create(group=group)
503

    
504
    def save(self, update_timestamps=True, **kwargs):
505
        if update_timestamps:
506
            if not self.id:
507
                self.date_joined = datetime.now()
508
            self.updated = datetime.now()
509

    
510
        # update date_signed_terms if necessary
511
        if self.__has_signed_terms != self.has_signed_terms:
512
            self.date_signed_terms = datetime.now()
513

    
514
        if not self.id:
515
            # set username
516
            self.username = self.email.lower()
517

    
518
        self.validate_unique_email_isactive()
519

    
520
        super(AstakosUser, self).save(**kwargs)
521

    
522
    def renew_token(self, flush_sessions=False, current_key=None):
523
        md5 = hashlib.md5()
524
        md5.update(settings.SECRET_KEY)
525
        md5.update(self.username)
526
        md5.update(self.realname.encode('ascii', 'ignore'))
527
        md5.update(asctime())
528

    
529
        self.auth_token = b64encode(md5.digest())
530
        self.auth_token_created = datetime.now()
531
        self.auth_token_expires = self.auth_token_created + \
532
                                  timedelta(hours=AUTH_TOKEN_DURATION)
533
        if flush_sessions:
534
            self.flush_sessions(current_key)
535
        msg = 'Token renewed for %s' % self.email
536
        logger.log(LOGGING_LEVEL, msg)
537

    
538
    def flush_sessions(self, current_key=None):
539
        q = self.sessions
540
        if current_key:
541
            q = q.exclude(session_key=current_key)
542

    
543
        keys = q.values_list('session_key', flat=True)
544
        if keys:
545
            msg = 'Flushing sessions: %s' % ','.join(keys)
546
            logger.log(LOGGING_LEVEL, msg, [])
547
        engine = import_module(settings.SESSION_ENGINE)
548
        for k in keys:
549
            s = engine.SessionStore(k)
550
            s.flush()
551

    
552
    def __unicode__(self):
553
        return '%s (%s)' % (self.realname, self.email)
554

    
555
    def conflicting_email(self):
556
        q = AstakosUser.objects.exclude(username=self.username)
557
        q = q.filter(email__iexact=self.email)
558
        if q.count() != 0:
559
            return True
560
        return False
561

    
562
    def validate_unique_email_isactive(self):
563
        """
564
        Implements a unique_together constraint for email and is_active fields.
565
        """
566
        q = AstakosUser.objects.all()
567
        q = q.filter(email = self.email)
568
        if self.id:
569
            q = q.filter(~Q(id = self.id))
570
        if q.count() != 0:
571
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
572

    
573
    @property
574
    def signed_terms(self):
575
        term = get_latest_terms()
576
        if not term:
577
            return True
578
        if not self.has_signed_terms:
579
            return False
580
        if not self.date_signed_terms:
581
            return False
582
        if self.date_signed_terms < term.date:
583
            self.has_signed_terms = False
584
            self.date_signed_terms = None
585
            self.save()
586
            return False
587
        return True
588

    
589
    def set_invitations_level(self):
590
        """
591
        Update user invitation level
592
        """
593
        level = self.invitation.inviter.level + 1
594
        self.level = level
595
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
596

    
597
    def can_login_with_auth_provider(self, provider):
598
        if not self.has_auth_provider(provider):
599
            return False
600
        else:
601
            return auth_providers.get_provider(provider).is_available_for_login()
602

    
603
    def can_add_auth_provider(self, provider, **kwargs):
604
        provider_settings = auth_providers.get_provider(provider)
605
        if not provider_settings.is_available_for_login():
606
            return False
607

    
608
        if self.has_auth_provider(provider) and \
609
           provider_settings.one_per_user:
610
            return False
611

    
612
        if 'provider_info' in kwargs:
613
            kwargs.pop('provider_info')
614

    
615
        if 'identifier' in kwargs:
616
            try:
617
                # provider with specified params already exist
618
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
619
                                                                   **kwargs)
620
            except AstakosUser.DoesNotExist:
621
                return True
622
            else:
623
                return False
624

    
625
        return True
626

    
627
    def can_remove_auth_provider(self, provider):
628
        if len(self.get_active_auth_providers()) <= 1:
629
            return False
630
        return True
631

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

    
635
    def has_auth_provider(self, provider, **kwargs):
636
        return bool(self.auth_providers.filter(module=provider,
637
                                               **kwargs).count())
638

    
639
    def add_auth_provider(self, provider, **kwargs):
640
        info_data = ''
641
        if 'provider_info' in kwargs:
642
            info_data = kwargs.pop('provider_info')
643
            if isinstance(info_data, dict):
644
                info_data = json.dumps(info_data)
645

    
646
        if self.can_add_auth_provider(provider, **kwargs):
647
            self.auth_providers.create(module=provider, active=True,
648
                                       info_data=info_data,
649
                                       **kwargs)
650
        else:
651
            raise Exception('Cannot add provider')
652

    
653
    def add_pending_auth_provider(self, pending):
654
        """
655
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
656
        the current user.
657
        """
658
        if not isinstance(pending, PendingThirdPartyUser):
659
            pending = PendingThirdPartyUser.objects.get(token=pending)
660

    
661
        provider = self.add_auth_provider(pending.provider,
662
                               identifier=pending.third_party_identifier,
663
                                affiliation=pending.affiliation,
664
                                          provider_info=pending.info)
665

    
666
        if email_re.match(pending.email or '') and pending.email != self.email:
667
            self.additionalmail_set.get_or_create(email=pending.email)
668

    
669
        pending.delete()
670
        return provider
671

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

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

    
679
    def get_provider_remove_url(self, module, **kwargs):
680
        return reverse('remove_auth_provider', kwargs={
681
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
682

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

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

    
695
    def get_auth_providers(self):
696
        return self.auth_providers.all()
697

    
698
    def get_available_auth_providers(self):
699
        """
700
        Returns a list of providers available for user to connect to.
701
        """
702
        providers = []
703
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
704
            if self.can_add_auth_provider(module):
705
                providers.append(provider_settings(self))
706

    
707
        return providers
708

    
709
    def get_active_auth_providers(self):
710
        providers = []
711
        for provider in self.auth_providers.active():
712
            if auth_providers.get_provider(provider.module).is_available_for_login():
713
                providers.append(provider)
714
        return providers
715

    
716
    @property
717
    def auth_providers_display(self):
718
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
719

    
720
    def get_inactive_message(self):
721
        msg_extra = ''
722
        message = ''
723
        if self.activation_sent:
724
            if self.email_verified:
725
                message = _(astakos_messages.ACCOUNT_INACTIVE)
726
            else:
727
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
728
                if astakos_settings.MODERATION_ENABLED:
729
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
730
                else:
731
                    url = self.get_resend_activation_url()
732
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
733
                                u' ' + \
734
                                _('<a href="%s">%s?</a>') % (url,
735
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
736
        else:
737
            if astakos_settings.MODERATION_ENABLED:
738
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
739
            else:
740
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
741
                url = self.get_resend_activation_url()
742
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
743
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
744

    
745
        return mark_safe(message + u' '+ msg_extra)
746

    
747

    
748
class AstakosUserAuthProviderManager(models.Manager):
749

    
750
    def active(self):
751
        return self.filter(active=True)
752

    
753

    
754
class AstakosUserAuthProvider(models.Model):
755
    """
756
    Available user authentication methods.
757
    """
758
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
759
                                   null=True, default=None)
760
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
761
    module = models.CharField('Provider', max_length=255, blank=False,
762
                                default='local')
763
    identifier = models.CharField('Third-party identifier',
764
                                              max_length=255, null=True,
765
                                              blank=True)
766
    active = models.BooleanField(default=True)
767
    auth_backend = models.CharField('Backend', max_length=255, blank=False,
768
                                   default='astakos')
769
    info_data = models.TextField(default="", null=True, blank=True)
770
    created = models.DateTimeField('Creation date', auto_now_add=True)
771

    
772
    objects = AstakosUserAuthProviderManager()
773

    
774
    class Meta:
775
        unique_together = (('identifier', 'module', 'user'), )
776
        ordering = ('module', 'created')
777

    
778
    def __init__(self, *args, **kwargs):
779
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
780
        try:
781
            self.info = json.loads(self.info_data)
782
            if not self.info:
783
                self.info = {}
784
        except Exception, e:
785
            self.info = {}
786

    
787
        for key,value in self.info.iteritems():
788
            setattr(self, 'info_%s' % key, value)
789

    
790

    
791
    @property
792
    def settings(self):
793
        return auth_providers.get_provider(self.module)
794

    
795
    @property
796
    def details_display(self):
797
        try:
798
          return self.settings.get_details_tpl_display % self.__dict__
799
        except:
800
          return ''
801

    
802
    @property
803
    def title_display(self):
804
        title_tpl = self.settings.get_title_display
805
        try:
806
            if self.settings.get_user_title_display:
807
                title_tpl = self.settings.get_user_title_display
808
        except Exception, e:
809
            pass
810
        try:
811
          return title_tpl % self.__dict__
812
        except:
813
          return self.settings.get_title_display % self.__dict__
814

    
815
    def can_remove(self):
816
        return self.user.can_remove_auth_provider(self.module)
817

    
818
    def delete(self, *args, **kwargs):
819
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
820
        if self.module == 'local':
821
            self.user.set_unusable_password()
822
            self.user.save()
823
        return ret
824

    
825
    def __repr__(self):
826
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
827

    
828
    def __unicode__(self):
829
        if self.identifier:
830
            return "%s:%s" % (self.module, self.identifier)
831
        if self.auth_backend:
832
            return "%s:%s" % (self.module, self.auth_backend)
833
        return self.module
834

    
835
    def save(self, *args, **kwargs):
836
        self.info_data = json.dumps(self.info)
837
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
838

    
839

    
840
class Membership(models.Model):
841
    person = models.ForeignKey(AstakosUser)
842
    group = models.ForeignKey(AstakosGroup)
843
    date_requested = models.DateField(default=datetime.now(), blank=True)
844
    date_joined = models.DateField(null=True, db_index=True, blank=True)
845

    
846
    class Meta:
847
        unique_together = ("person", "group")
848

    
849
    def save(self, *args, **kwargs):
850
        if not self.id:
851
            if not self.group.moderation_enabled:
852
                self.date_joined = datetime.now()
853
        super(Membership, self).save(*args, **kwargs)
854

    
855
    @property
856
    def is_approved(self):
857
        if self.date_joined:
858
            return True
859
        return False
860

    
861
    def approve(self):
862
        if self.is_approved:
863
            return
864
        if self.group.max_participants:
865
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
866
            'Maximum participant number has been reached.'
867
        self.date_joined = datetime.now()
868
        self.save()
869
        quota_disturbed.send(sender=self, users=(self.person,))
870

    
871
    def disapprove(self):
872
        self.delete()
873
        quota_disturbed.send(sender=self, users=(self.person,))
874

    
875
class AstakosQuotaManager(models.Manager):
876
    def _update_or_create(self, **kwargs):
877
        assert kwargs, \
878
            'update_or_create() must be passed at least one keyword argument'
879
        obj, created = self.get_or_create(**kwargs)
880
        defaults = kwargs.pop('defaults', {})
881
        if created:
882
            return obj, True, False
883
        else:
884
            try:
885
                params = dict(
886
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
887
                params.update(defaults)
888
                for attr, val in params.items():
889
                    if hasattr(obj, attr):
890
                        setattr(obj, attr, val)
891
                sid = transaction.savepoint()
892
                obj.save(force_update=True)
893
                transaction.savepoint_commit(sid)
894
                return obj, False, True
895
            except IntegrityError, e:
896
                transaction.savepoint_rollback(sid)
897
                try:
898
                    return self.get(**kwargs), False, False
899
                except self.model.DoesNotExist:
900
                    raise e
901

    
902
    update_or_create = _update_or_create
903

    
904
class AstakosGroupQuota(models.Model):
905
    objects = AstakosQuotaManager()
906
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
907
    uplimit = models.BigIntegerField('Up limit', null=True)
908
    resource = models.ForeignKey(Resource)
909
    group = models.ForeignKey(AstakosGroup, blank=True)
910

    
911
    class Meta:
912
        unique_together = ("resource", "group")
913

    
914
class AstakosUserQuota(models.Model):
915
    objects = AstakosQuotaManager()
916
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
917
    uplimit = models.BigIntegerField('Up limit', null=True)
918
    resource = models.ForeignKey(Resource)
919
    user = models.ForeignKey(AstakosUser)
920

    
921
    class Meta:
922
        unique_together = ("resource", "user")
923

    
924

    
925
class ApprovalTerms(models.Model):
926
    """
927
    Model for approval terms
928
    """
929

    
930
    date = models.DateTimeField(
931
        'Issue date', db_index=True, default=datetime.now())
932
    location = models.CharField('Terms location', max_length=255)
933

    
934

    
935
class Invitation(models.Model):
936
    """
937
    Model for registring invitations
938
    """
939
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
940
                                null=True)
941
    realname = models.CharField('Real name', max_length=255)
942
    username = models.CharField('Unique ID', max_length=255, unique=True)
943
    code = models.BigIntegerField('Invitation code', db_index=True)
944
    is_consumed = models.BooleanField('Consumed?', default=False)
945
    created = models.DateTimeField('Creation date', auto_now_add=True)
946
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
947

    
948
    def __init__(self, *args, **kwargs):
949
        super(Invitation, self).__init__(*args, **kwargs)
950
        if not self.id:
951
            self.code = _generate_invitation_code()
952

    
953
    def consume(self):
954
        self.is_consumed = True
955
        self.consumed = datetime.now()
956
        self.save()
957

    
958
    def __unicode__(self):
959
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
960

    
961

    
962
class EmailChangeManager(models.Manager):
963
    @transaction.commit_on_success
964
    def change_email(self, activation_key):
965
        """
966
        Validate an activation key and change the corresponding
967
        ``User`` if valid.
968

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

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

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

977
        After successful email change the activation record is deleted.
978

979
        Throws ValueError if there is already
980
        """
981
        try:
982
            email_change = self.model.objects.get(
983
                activation_key=activation_key)
984
            if email_change.activation_key_expired():
985
                email_change.delete()
986
                raise EmailChange.DoesNotExist
987
            # is there an active user with this address?
988
            try:
989
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
990
            except AstakosUser.DoesNotExist:
991
                pass
992
            else:
993
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
994
            # update user
995
            user = AstakosUser.objects.get(pk=email_change.user_id)
996
            user.email = email_change.new_email_address
997
            user.save()
998
            email_change.delete()
999
            return user
1000
        except EmailChange.DoesNotExist:
1001
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
1002

    
1003

    
1004
class EmailChange(models.Model):
1005
    new_email_address = models.EmailField(_(u'new e-mail address'),
1006
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
1007
    user = models.ForeignKey(
1008
        AstakosUser, unique=True, related_name='emailchange_user')
1009
    requested_at = models.DateTimeField(default=datetime.now())
1010
    activation_key = models.CharField(
1011
        max_length=40, unique=True, db_index=True)
1012

    
1013
    objects = EmailChangeManager()
1014

    
1015
    def activation_key_expired(self):
1016
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1017
        return self.requested_at + expiration_date < datetime.now()
1018

    
1019

    
1020
class AdditionalMail(models.Model):
1021
    """
1022
    Model for registring invitations
1023
    """
1024
    owner = models.ForeignKey(AstakosUser)
1025
    email = models.EmailField()
1026

    
1027

    
1028
def _generate_invitation_code():
1029
    while True:
1030
        code = randint(1, 2L ** 63 - 1)
1031
        try:
1032
            Invitation.objects.get(code=code)
1033
            # An invitation with this code already exists, try again
1034
        except Invitation.DoesNotExist:
1035
            return code
1036

    
1037

    
1038
def get_latest_terms():
1039
    try:
1040
        term = ApprovalTerms.objects.order_by('-id')[0]
1041
        return term
1042
    except IndexError:
1043
        pass
1044
    return None
1045

    
1046
class PendingThirdPartyUser(models.Model):
1047
    """
1048
    Model for registring successful third party user authentications
1049
    """
1050
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
1051
    provider = models.CharField('Provider', max_length=255, blank=True)
1052
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1053
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
1054
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
1055
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
1056
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1057
    token = models.CharField('Token', max_length=255, null=True, blank=True)
1058
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1059
    info = models.TextField(default="", null=True, blank=True)
1060

    
1061
    class Meta:
1062
        unique_together = ("provider", "third_party_identifier")
1063

    
1064
    def get_user_instance(self):
1065
        d = self.__dict__
1066
        d.pop('_state', None)
1067
        d.pop('id', None)
1068
        d.pop('token', None)
1069
        d.pop('created', None)
1070
        d.pop('info', None)
1071
        user = AstakosUser(**d)
1072

    
1073
        return user
1074

    
1075
    @property
1076
    def realname(self):
1077
        return '%s %s' %(self.first_name, self.last_name)
1078

    
1079
    @realname.setter
1080
    def realname(self, value):
1081
        parts = value.split(' ')
1082
        if len(parts) == 2:
1083
            self.first_name = parts[0]
1084
            self.last_name = parts[1]
1085
        else:
1086
            self.last_name = parts[0]
1087

    
1088
    def save(self, **kwargs):
1089
        if not self.id:
1090
            # set username
1091
            while not self.username:
1092
                username =  uuid.uuid4().hex[:30]
1093
                try:
1094
                    AstakosUser.objects.get(username = username)
1095
                except AstakosUser.DoesNotExist, e:
1096
                    self.username = username
1097
        super(PendingThirdPartyUser, self).save(**kwargs)
1098

    
1099
    def generate_token(self):
1100
        self.password = self.third_party_identifier
1101
        self.last_login = datetime.now()
1102
        self.token = default_token_generator.make_token(self)
1103

    
1104
class SessionCatalog(models.Model):
1105
    session_key = models.CharField(_('session key'), max_length=40)
1106
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1107

    
1108

    
1109
def create_astakos_user(u):
1110
    try:
1111
        AstakosUser.objects.get(user_ptr=u.pk)
1112
    except AstakosUser.DoesNotExist:
1113
        extended_user = AstakosUser(user_ptr_id=u.pk)
1114
        extended_user.__dict__.update(u.__dict__)
1115
        extended_user.save()
1116
        if not extended_user.has_auth_provider('local'):
1117
            extended_user.add_auth_provider('local')
1118
    except BaseException, e:
1119
        logger.exception(e)
1120

    
1121

    
1122
def fix_superusers(sender, **kwargs):
1123
    # Associate superusers with AstakosUser
1124
    admins = User.objects.filter(is_superuser=True)
1125
    for u in admins:
1126
        create_astakos_user(u)
1127

    
1128

    
1129
def user_post_save(sender, instance, created, **kwargs):
1130
    if not created:
1131
        return
1132
    create_astakos_user(instance)
1133

    
1134

    
1135
def set_default_group(user):
1136
    try:
1137
        default = AstakosGroup.objects.get(name='default')
1138
        Membership(
1139
            group=default, person=user, date_joined=datetime.now()).save()
1140
    except AstakosGroup.DoesNotExist, e:
1141
        logger.exception(e)
1142

    
1143

    
1144
def astakosuser_pre_save(sender, instance, **kwargs):
1145
    instance.aquarium_report = False
1146
    instance.new = False
1147
    try:
1148
        db_instance = AstakosUser.objects.get(id=instance.id)
1149
    except AstakosUser.DoesNotExist:
1150
        # create event
1151
        instance.aquarium_report = True
1152
        instance.new = True
1153
    else:
1154
        get = AstakosUser.__getattribute__
1155
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1156
                   BILLING_FIELDS)
1157
        instance.aquarium_report = True if l else False
1158

    
1159

    
1160
def astakosuser_post_save(sender, instance, created, **kwargs):
1161
    if instance.aquarium_report:
1162
        report_user_event(instance, create=instance.new)
1163
    if not created:
1164
        return
1165
    set_default_group(instance)
1166
    # TODO handle socket.error & IOError
1167
    register_users((instance,))
1168

    
1169

    
1170
def resource_post_save(sender, instance, created, **kwargs):
1171
    if not created:
1172
        return
1173
    register_resources((instance,))
1174

    
1175

    
1176
def send_quota_disturbed(sender, instance, **kwargs):
1177
    users = []
1178
    extend = users.extend
1179
    if sender == Membership:
1180
        if not instance.group.is_enabled:
1181
            return
1182
        extend([instance.person])
1183
    elif sender == AstakosUserQuota:
1184
        extend([instance.user])
1185
    elif sender == AstakosGroupQuota:
1186
        if not instance.group.is_enabled:
1187
            return
1188
        extend(instance.group.astakosuser_set.all())
1189
    elif sender == AstakosGroup:
1190
        if not instance.is_enabled:
1191
            return
1192
    quota_disturbed.send(sender=sender, users=users)
1193

    
1194

    
1195
def on_quota_disturbed(sender, users, **kwargs):
1196
#     print '>>>', locals()
1197
    if not users:
1198
        return
1199
    send_quota(users)
1200

    
1201
def renew_token(sender, instance, **kwargs):
1202
    if not instance.auth_token:
1203
        instance.renew_token()
1204

    
1205
post_syncdb.connect(fix_superusers)
1206
post_save.connect(user_post_save, sender=User)
1207
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1208
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1209
post_save.connect(resource_post_save, sender=Resource)
1210

    
1211
quota_disturbed = Signal(providing_args=["users"])
1212
quota_disturbed.connect(on_quota_disturbed)
1213

    
1214
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1215
post_delete.connect(send_quota_disturbed, sender=Membership)
1216
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1217
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1218
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1219
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1220

    
1221
pre_save.connect(renew_token, sender=AstakosUser)
1222
pre_save.connect(renew_token, sender=Service)