Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 3b258643

History | View | Annotate | Download (41.4 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
                                u' ' + \
722
                                _('<a href="%s">%s?</a>') % (url,
723
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
724
        else:
725
            if astakos_settings.MODERATION_ENABLED:
726
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
727
            else:
728
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
729
                url = self.get_resend_activation_url()
730
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
731
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
732

    
733
        return mark_safe(message + u' '+ msg_extra)
734

    
735

    
736
class AstakosUserAuthProviderManager(models.Manager):
737

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

    
741

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

    
760
    objects = AstakosUserAuthProviderManager()
761

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

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

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

    
778

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

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

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

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

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

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

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

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

    
827

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

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

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

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

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

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

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

    
890
    update_or_create = _update_or_create
891

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

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

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

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

    
912

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

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

    
922

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

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

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

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

    
949

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

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

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

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

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

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

    
991

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

    
1001
    objects = EmailChangeManager()
1002

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

    
1007

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

    
1015

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

    
1025

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

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

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

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

    
1061
        return user
1062

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

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

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

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

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

    
1096

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

    
1109

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

    
1116

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

    
1122

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

    
1131

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

    
1147

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

    
1157

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

    
1163

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

    
1182

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

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

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

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

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

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