Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 4bdd7e3d

History | View | Annotate | Download (41.3 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
class AstakosUser(User):
328
    """
329
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
330
    """
331
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
332
                                   null=True)
333

    
334
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
335
    #                    AstakosUserProvider model.
336
    provider = models.CharField('Provider', max_length=255, blank=True,
337
                                null=True)
338
    # ex. screen_name for twitter, eppn for shibboleth
339
    third_party_identifier = models.CharField('Third-party identifier',
340
                                              max_length=255, null=True,
341
                                              blank=True)
342

    
343

    
344
    #for invitations
345
    user_level = DEFAULT_USER_LEVEL
346
    level = models.IntegerField('Inviter level', default=user_level)
347
    invitations = models.IntegerField(
348
        'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
349

    
350
    auth_token = models.CharField('Authentication Token', max_length=32,
351
                                  null=True, blank=True)
352
    auth_token_created = models.DateTimeField('Token creation date', null=True)
353
    auth_token_expires = models.DateTimeField(
354
        'Token expiration date', null=True)
355

    
356
    updated = models.DateTimeField('Update date')
357
    is_verified = models.BooleanField('Is verified?', default=False)
358

    
359
    email_verified = models.BooleanField('Email verified?', default=False)
360

    
361
    has_credits = models.BooleanField('Has credits?', default=False)
362
    has_signed_terms = models.BooleanField(
363
        'I agree with the terms', default=False)
364
    date_signed_terms = models.DateTimeField(
365
        'Signed terms date', null=True, blank=True)
366

    
367
    activation_sent = models.DateTimeField(
368
        'Activation sent data', null=True, blank=True)
369

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

    
373
    astakos_groups = models.ManyToManyField(
374
        AstakosGroup, verbose_name=_('agroups'), blank=True,
375
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
376
        through='Membership')
377

    
378
    __has_signed_terms = False
379
    disturbed_quota = models.BooleanField('Needs quotaholder syncing',
380
                                           default=False, db_index=True)
381

    
382
    objects = AstakosUserManager()
383

    
384
    owner = models.ManyToManyField(
385
        AstakosGroup, related_name='owner', null=True)
386

    
387
    def __init__(self, *args, **kwargs):
388
        super(AstakosUser, self).__init__(*args, **kwargs)
389
        self.__has_signed_terms = self.has_signed_terms
390
        if not self.id:
391
            self.is_active = False
392

    
393
    @property
394
    def realname(self):
395
        return '%s %s' % (self.first_name, self.last_name)
396

    
397
    @realname.setter
398
    def realname(self, value):
399
        parts = value.split(' ')
400
        if len(parts) == 2:
401
            self.first_name = parts[0]
402
            self.last_name = parts[1]
403
        else:
404
            self.last_name = parts[0]
405

    
406
    def add_permission(self, pname):
407
        if self.has_perm(pname):
408
            return
409
        p, created = Permission.objects.get_or_create(codename=pname,
410
                                                      name=pname.capitalize(),
411
                                                      content_type=content_type)
412
        self.user_permissions.add(p)
413

    
414
    def remove_permission(self, pname):
415
        if self.has_perm(pname):
416
            return
417
        p = Permission.objects.get(codename=pname,
418
                                   content_type=content_type)
419
        self.user_permissions.remove(p)
420

    
421
    @property
422
    def invitation(self):
423
        try:
424
            return Invitation.objects.get(username=self.email)
425
        except Invitation.DoesNotExist:
426
            return None
427

    
428
    def invite(self, email, realname):
429
        inv = Invitation(inviter=self, username=email, realname=realname)
430
        inv.save()
431
        send_invitation(inv)
432
        self.invitations = max(0, self.invitations - 1)
433
        self.save()
434

    
435
    @property
436
    def quota(self):
437
        """Returns a dict with the sum of quota limits per resource"""
438
        d = defaultdict(int)
439
        for q in self.policies:
440
            d[q.resource] += q.uplimit or inf
441
        for m in self.extended_groups:
442
            if not m.is_approved:
443
                continue
444
            g = m.group
445
            if not g.is_enabled:
446
                continue
447
            for r, uplimit in g.quota.iteritems():
448
                d[r] += uplimit or inf
449
        # TODO set default for remaining
450
        return d
451

    
452
    @property
453
    def policies(self):
454
        return self.astakosuserquota_set.select_related().all()
455

    
456
    @policies.setter
457
    def policies(self, policies):
458
        for p in policies:
459
            service = policies.get('service', None)
460
            resource = policies.get('resource', None)
461
            uplimit = policies.get('uplimit', 0)
462
            update = policies.get('update', True)
463
            self.add_policy(service, resource, uplimit, update)
464

    
465
    def add_policy(self, service, resource, uplimit, update=True):
466
        """Raises ObjectDoesNotExist, IntegrityError"""
467
        resource = Resource.objects.get(service__name=service, name=resource)
468
        if update:
469
            AstakosUserQuota.objects.update_or_create(user=self,
470
                                                      resource=resource,
471
                                                      defaults={'uplimit': uplimit})
472
        else:
473
            q = self.astakosuserquota_set
474
            q.create(resource=resource, uplimit=uplimit)
475

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

    
481
    @property
482
    def extended_groups(self):
483
        return self.membership_set.select_related().all()
484

    
485
    @extended_groups.setter
486
    def extended_groups(self, groups):
487
        #TODO exceptions
488
        for name in (groups or ()):
489
            group = AstakosGroup.objects.get(name=name)
490
            self.membership_set.create(group=group)
491

    
492
    def save(self, update_timestamps=True, **kwargs):
493
        if update_timestamps:
494
            if not self.id:
495
                self.date_joined = datetime.now()
496
            self.updated = datetime.now()
497

    
498
        # update date_signed_terms if necessary
499
        if self.__has_signed_terms != self.has_signed_terms:
500
            self.date_signed_terms = datetime.now()
501

    
502
        if not self.id:
503
            # set username
504
            self.username = self.email
505

    
506
        self.validate_unique_email_isactive()
507

    
508
        super(AstakosUser, self).save(**kwargs)
509

    
510
    def renew_token(self, flush_sessions=False, current_key=None):
511
        md5 = hashlib.md5()
512
        md5.update(settings.SECRET_KEY)
513
        md5.update(self.username)
514
        md5.update(self.realname.encode('ascii', 'ignore'))
515
        md5.update(asctime())
516

    
517
        self.auth_token = b64encode(md5.digest())
518
        self.auth_token_created = datetime.now()
519
        self.auth_token_expires = self.auth_token_created + \
520
                                  timedelta(hours=AUTH_TOKEN_DURATION)
521
        if flush_sessions:
522
            self.flush_sessions(current_key)
523
        msg = 'Token renewed for %s' % self.email
524
        logger.log(LOGGING_LEVEL, msg)
525

    
526
    def flush_sessions(self, current_key=None):
527
        q = self.sessions
528
        if current_key:
529
            q = q.exclude(session_key=current_key)
530

    
531
        keys = q.values_list('session_key', flat=True)
532
        if keys:
533
            msg = 'Flushing sessions: %s' % ','.join(keys)
534
            logger.log(LOGGING_LEVEL, msg, [])
535
        engine = import_module(settings.SESSION_ENGINE)
536
        for k in keys:
537
            s = engine.SessionStore(k)
538
            s.flush()
539

    
540
    def __unicode__(self):
541
        return '%s (%s)' % (self.realname, self.email)
542

    
543
    def conflicting_email(self):
544
        q = AstakosUser.objects.exclude(username=self.username)
545
        q = q.filter(email__iexact=self.email)
546
        if q.count() != 0:
547
            return True
548
        return False
549

    
550
    def validate_unique_email_isactive(self):
551
        """
552
        Implements a unique_together constraint for email and is_active fields.
553
        """
554
        q = AstakosUser.objects.all()
555
        q = q.filter(email = self.email)
556
        if self.id:
557
            q = q.filter(~Q(id = self.id))
558
        if q.count() != 0:
559
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
560

    
561
    @property
562
    def signed_terms(self):
563
        term = get_latest_terms()
564
        if not term:
565
            return True
566
        if not self.has_signed_terms:
567
            return False
568
        if not self.date_signed_terms:
569
            return False
570
        if self.date_signed_terms < term.date:
571
            self.has_signed_terms = False
572
            self.date_signed_terms = None
573
            self.save()
574
            return False
575
        return True
576

    
577
    def set_invitations_level(self):
578
        """
579
        Update user invitation level
580
        """
581
        level = self.invitation.inviter.level + 1
582
        self.level = level
583
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
584

    
585
    def can_login_with_auth_provider(self, provider):
586
        if not self.has_auth_provider(provider):
587
            return False
588
        else:
589
            return auth_providers.get_provider(provider).is_available_for_login()
590

    
591
    def can_add_auth_provider(self, provider, **kwargs):
592
        provider_settings = auth_providers.get_provider(provider)
593
        if not provider_settings.is_available_for_login():
594
            return False
595

    
596
        if self.has_auth_provider(provider) and \
597
           provider_settings.one_per_user:
598
            return False
599

    
600
        if 'provider_info' in kwargs:
601
            kwargs.pop('provider_info')
602

    
603
        if 'identifier' in kwargs:
604
            try:
605
                # provider with specified params already exist
606
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
607
                                                                   **kwargs)
608
            except AstakosUser.DoesNotExist:
609
                return True
610
            else:
611
                return False
612

    
613
        return True
614

    
615
    def can_remove_auth_provider(self, provider):
616
        if len(self.get_active_auth_providers()) <= 1:
617
            return False
618
        return True
619

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

    
623
    def has_auth_provider(self, provider, **kwargs):
624
        return bool(self.auth_providers.filter(module=provider,
625
                                               **kwargs).count())
626

    
627
    def add_auth_provider(self, provider, **kwargs):
628
        info_data = ''
629
        if 'provider_info' in kwargs:
630
            info_data = kwargs.pop('provider_info')
631
            if isinstance(info_data, dict):
632
                info_data = json.dumps(info_data)
633

    
634
        if self.can_add_auth_provider(provider, **kwargs):
635
            self.auth_providers.create(module=provider, active=True,
636
                                       info_data=info_data,
637
                                       **kwargs)
638
        else:
639
            raise Exception('Cannot add provider')
640

    
641
    def add_pending_auth_provider(self, pending):
642
        """
643
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
644
        the current user.
645
        """
646
        if not isinstance(pending, PendingThirdPartyUser):
647
            pending = PendingThirdPartyUser.objects.get(token=pending)
648

    
649
        provider = self.add_auth_provider(pending.provider,
650
                               identifier=pending.third_party_identifier,
651
                                affiliation=pending.affiliation,
652
                                          provider_info=pending.info)
653

    
654
        if email_re.match(pending.email or '') and pending.email != self.email:
655
            self.additionalmail_set.get_or_create(email=pending.email)
656

    
657
        pending.delete()
658
        return provider
659

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

    
663
    # user urls
664
    def get_resend_activation_url(self):
665
        return reverse('send_activation', kwargs={'user_id': self.pk})
666

    
667
    def get_provider_remove_url(self, module, **kwargs):
668
        return reverse('remove_auth_provider', kwargs={
669
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
670

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

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

    
683
    def get_auth_providers(self):
684
        return self.auth_providers.all()
685

    
686
    def get_available_auth_providers(self):
687
        """
688
        Returns a list of providers available for user to connect to.
689
        """
690
        providers = []
691
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
692
            if self.can_add_auth_provider(module):
693
                providers.append(provider_settings(self))
694

    
695
        return providers
696

    
697
    def get_active_auth_providers(self):
698
        providers = []
699
        for provider in self.auth_providers.active():
700
            if auth_providers.get_provider(provider.module).is_available_for_login():
701
                providers.append(provider)
702
        return providers
703

    
704
    @property
705
    def auth_providers_display(self):
706
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
707

    
708
    def get_inactive_message(self):
709
        msg_extra = ''
710
        message = ''
711
        if self.activation_sent:
712
            if self.email_verified:
713
                message = _(astakos_messages.ACCOUNT_INACTIVE)
714
            else:
715
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
716
                if astakos_settings.MODERATION_ENABLED:
717
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
718
                else:
719
                    url = self.get_resend_activation_url()
720
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
721
                                _('<a href="%s">%s?</a>') % (url,
722
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
723
        else:
724
            if astakos_settings.MODERATION_ENABLED:
725
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
726
            else:
727
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
728
                url = self.get_resend_activation_url()
729
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
730
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
731

    
732
        return mark_safe(message + msg_extra)
733

    
734

    
735
class AstakosUserAuthProviderManager(models.Manager):
736

    
737
    def active(self):
738
        return self.filter(active=True)
739

    
740

    
741
class AstakosUserAuthProvider(models.Model):
742
    """
743
    Available user authentication methods.
744
    """
745
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
746
                                   null=True, default=None)
747
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
748
    module = models.CharField('Provider', max_length=255, blank=False,
749
                                default='local')
750
    identifier = models.CharField('Third-party identifier',
751
                                              max_length=255, null=True,
752
                                              blank=True)
753
    active = models.BooleanField(default=True)
754
    auth_backend = models.CharField('Backend', max_length=255, blank=False,
755
                                   default='astakos')
756
    info_data = models.TextField(default="", null=True, blank=True)
757
    created = models.DateTimeField('Creation date', auto_now_add=True)
758

    
759
    objects = AstakosUserAuthProviderManager()
760

    
761
    class Meta:
762
        unique_together = (('identifier', 'module', 'user'), )
763
        ordering = ('module', 'created')
764

    
765
    def __init__(self, *args, **kwargs):
766
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
767
        try:
768
            self.info = json.loads(self.info_data)
769
            if not self.info:
770
                self.info = {}
771
        except Exception, e:
772
            self.info = {}
773

    
774
        for key,value in self.info.iteritems():
775
            setattr(self, 'info_%s' % key, value)
776

    
777

    
778
    @property
779
    def settings(self):
780
        return auth_providers.get_provider(self.module)
781

    
782
    @property
783
    def details_display(self):
784
        try:
785
          return self.settings.get_details_tpl_display % self.__dict__
786
        except:
787
          return ''
788

    
789
    @property
790
    def title_display(self):
791
        title_tpl = self.settings.get_title_display
792
        try:
793
            if self.settings.get_user_title_display:
794
                title_tpl = self.settings.get_user_title_display
795
        except Exception, e:
796
            pass
797
        try:
798
          return title_tpl % self.__dict__
799
        except:
800
          return self.settings.get_title_display % self.__dict__
801

    
802
    def can_remove(self):
803
        return self.user.can_remove_auth_provider(self.module)
804

    
805
    def delete(self, *args, **kwargs):
806
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
807
        if self.module == 'local':
808
            self.user.set_unusable_password()
809
            self.user.save()
810
        return ret
811

    
812
    def __repr__(self):
813
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
814

    
815
    def __unicode__(self):
816
        if self.identifier:
817
            return "%s:%s" % (self.module, self.identifier)
818
        if self.auth_backend:
819
            return "%s:%s" % (self.module, self.auth_backend)
820
        return self.module
821

    
822
    def save(self, *args, **kwargs):
823
        self.info_data = json.dumps(self.info)
824
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
825

    
826

    
827
class Membership(models.Model):
828
    person = models.ForeignKey(AstakosUser)
829
    group = models.ForeignKey(AstakosGroup)
830
    date_requested = models.DateField(default=datetime.now(), blank=True)
831
    date_joined = models.DateField(null=True, db_index=True, blank=True)
832

    
833
    class Meta:
834
        unique_together = ("person", "group")
835

    
836
    def save(self, *args, **kwargs):
837
        if not self.id:
838
            if not self.group.moderation_enabled:
839
                self.date_joined = datetime.now()
840
        super(Membership, self).save(*args, **kwargs)
841

    
842
    @property
843
    def is_approved(self):
844
        if self.date_joined:
845
            return True
846
        return False
847

    
848
    def approve(self):
849
        if self.is_approved:
850
            return
851
        if self.group.max_participants:
852
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
853
            'Maximum participant number has been reached.'
854
        self.date_joined = datetime.now()
855
        self.save()
856
        quota_disturbed.send(sender=self, users=(self.person,))
857

    
858
    def disapprove(self):
859
        self.delete()
860
        quota_disturbed.send(sender=self, users=(self.person,))
861

    
862
class AstakosQuotaManager(models.Manager):
863
    def _update_or_create(self, **kwargs):
864
        assert kwargs, \
865
            'update_or_create() must be passed at least one keyword argument'
866
        obj, created = self.get_or_create(**kwargs)
867
        defaults = kwargs.pop('defaults', {})
868
        if created:
869
            return obj, True, False
870
        else:
871
            try:
872
                params = dict(
873
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
874
                params.update(defaults)
875
                for attr, val in params.items():
876
                    if hasattr(obj, attr):
877
                        setattr(obj, attr, val)
878
                sid = transaction.savepoint()
879
                obj.save(force_update=True)
880
                transaction.savepoint_commit(sid)
881
                return obj, False, True
882
            except IntegrityError, e:
883
                transaction.savepoint_rollback(sid)
884
                try:
885
                    return self.get(**kwargs), False, False
886
                except self.model.DoesNotExist:
887
                    raise e
888

    
889
    update_or_create = _update_or_create
890

    
891
class AstakosGroupQuota(models.Model):
892
    objects = AstakosQuotaManager()
893
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
894
    uplimit = models.BigIntegerField('Up limit', null=True)
895
    resource = models.ForeignKey(Resource)
896
    group = models.ForeignKey(AstakosGroup, blank=True)
897

    
898
    class Meta:
899
        unique_together = ("resource", "group")
900

    
901
class AstakosUserQuota(models.Model):
902
    objects = AstakosQuotaManager()
903
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
904
    uplimit = models.BigIntegerField('Up limit', null=True)
905
    resource = models.ForeignKey(Resource)
906
    user = models.ForeignKey(AstakosUser)
907

    
908
    class Meta:
909
        unique_together = ("resource", "user")
910

    
911

    
912
class ApprovalTerms(models.Model):
913
    """
914
    Model for approval terms
915
    """
916

    
917
    date = models.DateTimeField(
918
        'Issue date', db_index=True, default=datetime.now())
919
    location = models.CharField('Terms location', max_length=255)
920

    
921

    
922
class Invitation(models.Model):
923
    """
924
    Model for registring invitations
925
    """
926
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
927
                                null=True)
928
    realname = models.CharField('Real name', max_length=255)
929
    username = models.CharField('Unique ID', max_length=255, unique=True)
930
    code = models.BigIntegerField('Invitation code', db_index=True)
931
    is_consumed = models.BooleanField('Consumed?', default=False)
932
    created = models.DateTimeField('Creation date', auto_now_add=True)
933
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
934

    
935
    def __init__(self, *args, **kwargs):
936
        super(Invitation, self).__init__(*args, **kwargs)
937
        if not self.id:
938
            self.code = _generate_invitation_code()
939

    
940
    def consume(self):
941
        self.is_consumed = True
942
        self.consumed = datetime.now()
943
        self.save()
944

    
945
    def __unicode__(self):
946
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
947

    
948

    
949
class EmailChangeManager(models.Manager):
950
    @transaction.commit_on_success
951
    def change_email(self, activation_key):
952
        """
953
        Validate an activation key and change the corresponding
954
        ``User`` if valid.
955

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

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

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

964
        After successful email change the activation record is deleted.
965

966
        Throws ValueError if there is already
967
        """
968
        try:
969
            email_change = self.model.objects.get(
970
                activation_key=activation_key)
971
            if email_change.activation_key_expired():
972
                email_change.delete()
973
                raise EmailChange.DoesNotExist
974
            # is there an active user with this address?
975
            try:
976
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
977
            except AstakosUser.DoesNotExist:
978
                pass
979
            else:
980
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
981
            # update user
982
            user = AstakosUser.objects.get(pk=email_change.user_id)
983
            user.email = email_change.new_email_address
984
            user.save()
985
            email_change.delete()
986
            return user
987
        except EmailChange.DoesNotExist:
988
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
989

    
990

    
991
class EmailChange(models.Model):
992
    new_email_address = models.EmailField(_(u'new e-mail address'),
993
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
994
    user = models.ForeignKey(
995
        AstakosUser, unique=True, related_name='emailchange_user')
996
    requested_at = models.DateTimeField(default=datetime.now())
997
    activation_key = models.CharField(
998
        max_length=40, unique=True, db_index=True)
999

    
1000
    objects = EmailChangeManager()
1001

    
1002
    def activation_key_expired(self):
1003
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1004
        return self.requested_at + expiration_date < datetime.now()
1005

    
1006

    
1007
class AdditionalMail(models.Model):
1008
    """
1009
    Model for registring invitations
1010
    """
1011
    owner = models.ForeignKey(AstakosUser)
1012
    email = models.EmailField()
1013

    
1014

    
1015
def _generate_invitation_code():
1016
    while True:
1017
        code = randint(1, 2L ** 63 - 1)
1018
        try:
1019
            Invitation.objects.get(code=code)
1020
            # An invitation with this code already exists, try again
1021
        except Invitation.DoesNotExist:
1022
            return code
1023

    
1024

    
1025
def get_latest_terms():
1026
    try:
1027
        term = ApprovalTerms.objects.order_by('-id')[0]
1028
        return term
1029
    except IndexError:
1030
        pass
1031
    return None
1032

    
1033
class PendingThirdPartyUser(models.Model):
1034
    """
1035
    Model for registring successful third party user authentications
1036
    """
1037
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
1038
    provider = models.CharField('Provider', max_length=255, blank=True)
1039
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1040
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
1041
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
1042
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
1043
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1044
    token = models.CharField('Token', max_length=255, null=True, blank=True)
1045
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1046
    info = models.TextField(default="", null=True, blank=True)
1047

    
1048
    class Meta:
1049
        unique_together = ("provider", "third_party_identifier")
1050

    
1051
    def get_user_instance(self):
1052
        d = self.__dict__
1053
        d.pop('_state', None)
1054
        d.pop('id', None)
1055
        d.pop('token', None)
1056
        d.pop('created', None)
1057
        d.pop('info', None)
1058
        user = AstakosUser(**d)
1059

    
1060
        return user
1061

    
1062
    @property
1063
    def realname(self):
1064
        return '%s %s' %(self.first_name, self.last_name)
1065

    
1066
    @realname.setter
1067
    def realname(self, value):
1068
        parts = value.split(' ')
1069
        if len(parts) == 2:
1070
            self.first_name = parts[0]
1071
            self.last_name = parts[1]
1072
        else:
1073
            self.last_name = parts[0]
1074

    
1075
    def save(self, **kwargs):
1076
        if not self.id:
1077
            # set username
1078
            while not self.username:
1079
                username =  uuid.uuid4().hex[:30]
1080
                try:
1081
                    AstakosUser.objects.get(username = username)
1082
                except AstakosUser.DoesNotExist, e:
1083
                    self.username = username
1084
        super(PendingThirdPartyUser, self).save(**kwargs)
1085

    
1086
    def generate_token(self):
1087
        self.password = self.third_party_identifier
1088
        self.last_login = datetime.now()
1089
        self.token = default_token_generator.make_token(self)
1090

    
1091
class SessionCatalog(models.Model):
1092
    session_key = models.CharField(_('session key'), max_length=40)
1093
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1094

    
1095

    
1096
def create_astakos_user(u):
1097
    try:
1098
        AstakosUser.objects.get(user_ptr=u.pk)
1099
    except AstakosUser.DoesNotExist:
1100
        extended_user = AstakosUser(user_ptr_id=u.pk)
1101
        extended_user.__dict__.update(u.__dict__)
1102
        extended_user.save()
1103
        if not extended_user.has_auth_provider('local'):
1104
            extended_user.add_auth_provider('local')
1105
    except BaseException, e:
1106
        logger.exception(e)
1107

    
1108

    
1109
def fix_superusers(sender, **kwargs):
1110
    # Associate superusers with AstakosUser
1111
    admins = User.objects.filter(is_superuser=True)
1112
    for u in admins:
1113
        create_astakos_user(u)
1114

    
1115

    
1116
def user_post_save(sender, instance, created, **kwargs):
1117
    if not created:
1118
        return
1119
    create_astakos_user(instance)
1120

    
1121

    
1122
def set_default_group(user):
1123
    try:
1124
        default = AstakosGroup.objects.get(name='default')
1125
        Membership(
1126
            group=default, person=user, date_joined=datetime.now()).save()
1127
    except AstakosGroup.DoesNotExist, e:
1128
        logger.exception(e)
1129

    
1130

    
1131
def astakosuser_pre_save(sender, instance, **kwargs):
1132
    instance.aquarium_report = False
1133
    instance.new = False
1134
    try:
1135
        db_instance = AstakosUser.objects.get(id=instance.id)
1136
    except AstakosUser.DoesNotExist:
1137
        # create event
1138
        instance.aquarium_report = True
1139
        instance.new = True
1140
    else:
1141
        get = AstakosUser.__getattribute__
1142
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1143
                   BILLING_FIELDS)
1144
        instance.aquarium_report = True if l else False
1145

    
1146

    
1147
def astakosuser_post_save(sender, instance, created, **kwargs):
1148
    if instance.aquarium_report:
1149
        report_user_event(instance, create=instance.new)
1150
    if not created:
1151
        return
1152
    set_default_group(instance)
1153
    # TODO handle socket.error & IOError
1154
    register_users((instance,))
1155

    
1156

    
1157
def resource_post_save(sender, instance, created, **kwargs):
1158
    if not created:
1159
        return
1160
    register_resources((instance,))
1161

    
1162

    
1163
def send_quota_disturbed(sender, instance, **kwargs):
1164
    users = []
1165
    extend = users.extend
1166
    if sender == Membership:
1167
        if not instance.group.is_enabled:
1168
            return
1169
        extend([instance.person])
1170
    elif sender == AstakosUserQuota:
1171
        extend([instance.user])
1172
    elif sender == AstakosGroupQuota:
1173
        if not instance.group.is_enabled:
1174
            return
1175
        extend(instance.group.astakosuser_set.all())
1176
    elif sender == AstakosGroup:
1177
        if not instance.is_enabled:
1178
            return
1179
    quota_disturbed.send(sender=sender, users=users)
1180

    
1181

    
1182
def on_quota_disturbed(sender, users, **kwargs):
1183
#     print '>>>', locals()
1184
    if not users:
1185
        return
1186
    send_quota(users)
1187

    
1188
def renew_token(sender, instance, **kwargs):
1189
    if not instance.auth_token:
1190
        instance.renew_token()
1191

    
1192
post_syncdb.connect(fix_superusers)
1193
post_save.connect(user_post_save, sender=User)
1194
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1195
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1196
post_save.connect(resource_post_save, sender=Resource)
1197

    
1198
quota_disturbed = Signal(providing_args=["users"])
1199
quota_disturbed.connect(on_quota_disturbed)
1200

    
1201
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1202
post_delete.connect(send_quota_disturbed, sender=Membership)
1203
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1204
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1205
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1206
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1207

    
1208
pre_save.connect(renew_token, sender=AstakosUser)
1209
pre_save.connect(renew_token, sender=Service)