Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 6b9a334b

History | View | Annotate | Download (38.1 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

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

    
46
from django.db import models, IntegrityError
47
from django.contrib.auth.models import User, UserManager, Group, Permission
48
from django.utils.translation import ugettext as _
49
from django.db import transaction
50
from django.core.exceptions import ValidationError
51
from django.db.models.signals import (
52
    pre_save, post_save, post_syncdb, post_delete
53
)
54
from django.contrib.contenttypes.models import ContentType
55

    
56
from django.dispatch import Signal
57
from django.db.models import Q
58
from django.core.urlresolvers import reverse
59
from django.utils.http import int_to_base36
60
from django.contrib.auth.tokens import default_token_generator
61
from django.conf import settings
62
from django.utils.importlib import import_module
63
from django.core.validators import email_re
64

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

    
76
import astakos.im.messages as astakos_messages
77

    
78
logger = logging.getLogger(__name__)
79

    
80
DEFAULT_CONTENT_TYPE = None
81
try:
82
    content_type = ContentType.objects.get(app_label='im', model='astakosuser')
83
except:
84
    content_type = DEFAULT_CONTENT_TYPE
85

    
86
RESOURCE_SEPARATOR = '.'
87

    
88
inf = float('inf')
89

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

    
100
    def renew_token(self):
101
        md5 = hashlib.md5()
102
        md5.update(self.name.encode('ascii', 'ignore'))
103
        md5.update(self.url.encode('ascii', 'ignore'))
104
        md5.update(asctime())
105

    
106
        self.auth_token = b64encode(md5.digest())
107
        self.auth_token_created = datetime.now()
108
        self.auth_token_expires = self.auth_token_created + \
109
            timedelta(hours=AUTH_TOKEN_DURATION)
110

    
111
    def __str__(self):
112
        return self.name
113

    
114
    @property
115
    def resources(self):
116
        return self.resource_set.all()
117

    
118
    @resources.setter
119
    def resources(self, resources):
120
        for s in resources:
121
            self.resource_set.create(**s)
122

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

    
134

    
135
class ResourceMetadata(models.Model):
136
    key = models.CharField('Name', max_length=255, unique=True, db_index=True)
137
    value = models.CharField('Value', max_length=255)
138

    
139

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

    
148
    def __str__(self):
149
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
150

    
151

    
152
class GroupKind(models.Model):
153
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
154

    
155
    def __str__(self):
156
        return self.name
157

    
158

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

    
199
    @property
200
    def is_disabled(self):
201
        if not self.approval_date:
202
            return True
203
        return False
204

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

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

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

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

    
249
#     def disapprove_member(self, person):
250
#         self.membership_set.remove(person=person)
251

    
252
    @property
253
    def members(self):
254
        q = self.membership_set.select_related().all()
255
        return [m.person for m in q]
256

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

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

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

    
282
    @property
283
    def policies(self):
284
        return self.astakosgroupquota_set.select_related().all()
285

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

    
295
    @property
296
    def owners(self):
297
        return self.owner.all()
298

    
299
    @property
300
    def owner_details(self):
301
        return self.owner.select_related().all()
302

    
303
    @owners.setter
304
    def owners(self, l):
305
        self.owner = l
306
        map(self.approve_member, l)
307

    
308

    
309

    
310
class AstakosUserManager(UserManager):
311

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

    
321
class AstakosUser(User):
322
    """
323
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
324
    """
325
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
326
                                   null=True)
327

    
328
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
329
    #                    AstakosUserProvider model.
330
    provider = models.CharField('Provider', max_length=255, blank=True,
331
                                null=True)
332
    # ex. screen_name for twitter, eppn for shibboleth
333
    third_party_identifier = models.CharField('Third-party identifier',
334
                                              max_length=255, null=True,
335
                                              blank=True)
336

    
337

    
338
    #for invitations
339
    user_level = DEFAULT_USER_LEVEL
340
    level = models.IntegerField('Inviter level', default=user_level)
341
    invitations = models.IntegerField(
342
        'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
343

    
344
    auth_token = models.CharField('Authentication Token', max_length=32,
345
                                  null=True, blank=True)
346
    auth_token_created = models.DateTimeField('Token creation date', null=True)
347
    auth_token_expires = models.DateTimeField(
348
        'Token expiration date', null=True)
349

    
350
    updated = models.DateTimeField('Update date')
351
    is_verified = models.BooleanField('Is verified?', default=False)
352

    
353
    email_verified = models.BooleanField('Email verified?', default=False)
354

    
355
    has_credits = models.BooleanField('Has credits?', default=False)
356
    has_signed_terms = models.BooleanField(
357
        'I agree with the terms', default=False)
358
    date_signed_terms = models.DateTimeField(
359
        'Signed terms date', null=True, blank=True)
360

    
361
    activation_sent = models.DateTimeField(
362
        'Activation sent data', null=True, blank=True)
363

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

    
367
    astakos_groups = models.ManyToManyField(
368
        AstakosGroup, verbose_name=_('agroups'), blank=True,
369
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
370
        through='Membership')
371

    
372
    __has_signed_terms = False
373
    disturbed_quota = models.BooleanField('Needs quotaholder syncing',
374
                                           default=False, db_index=True)
375

    
376
    objects = AstakosUserManager()
377
    
378
    owner = models.ManyToManyField(
379
        AstakosGroup, related_name='owner', null=True)
380

    
381
    class Meta:
382
        unique_together = ("provider", "third_party_identifier")
383

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

    
390
    @property
391
    def realname(self):
392
        return '%s %s' % (self.first_name, self.last_name)
393

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

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

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

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

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

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

    
449
    @property
450
    def policies(self):
451
        return self.astakosuserquota_set.select_related().all()
452

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

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

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

    
478
    @property
479
    def extended_groups(self):
480
        return self.membership_set.select_related().all()
481

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

    
489
    def save(self, update_timestamps=True, **kwargs):
490
        if update_timestamps:
491
            if not self.id:
492
                self.date_joined = datetime.now()
493
            self.updated = datetime.now()
494

    
495
        # update date_signed_terms if necessary
496
        if self.__has_signed_terms != self.has_signed_terms:
497
            self.date_signed_terms = datetime.now()
498

    
499
        if not self.id:
500
            # set username
501
            while not self.username:
502
                username =  self.email
503
                try:
504
                    AstakosUser.objects.get(username=username)
505
                except AstakosUser.DoesNotExist:
506
                    self.username = username
507

    
508
        self.validate_unique_email_isactive()
509
        if self.is_active and self.activation_sent:
510
            # reset the activation sent
511
            self.activation_sent = None
512

    
513
        super(AstakosUser, self).save(**kwargs)
514

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

    
522
        self.auth_token = b64encode(md5.digest())
523
        self.auth_token_created = datetime.now()
524
        self.auth_token_expires = self.auth_token_created + \
525
                                  timedelta(hours=AUTH_TOKEN_DURATION)
526
        if flush_sessions:
527
            self.flush_sessions(current_key)
528
        msg = 'Token renewed for %s' % self.email
529
        logger.log(LOGGING_LEVEL, msg)
530

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

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

    
545
    def __unicode__(self):
546
        return '%s (%s)' % (self.realname, self.email)
547

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

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

    
567
    @property
568
    def signed_terms(self):
569
        term = get_latest_terms()
570
        if not term:
571
            return True
572
        if not self.has_signed_terms:
573
            return False
574
        if not self.date_signed_terms:
575
            return False
576
        if self.date_signed_terms < term.date:
577
            self.has_signed_terms = False
578
            self.date_signed_terms = None
579
            self.save()
580
            return False
581
        return True
582

    
583
    def set_invitations_level(self):
584
        """
585
        Update user invitation level
586
        """
587
        level = self.invitation.inviter.level + 1
588
        self.level = level
589
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
590

    
591
    def can_login_with_auth_provider(self, provider):
592
        if not self.has_auth_provider(provider):
593
            return False
594
        else:
595
            return auth_providers.get_provider(provider).is_available_for_login()
596

    
597
    def can_add_auth_provider(self, provider, **kwargs):
598
        provider_settings = auth_providers.get_provider(provider)
599
        if not provider_settings.is_available_for_login():
600
            return False
601

    
602
        if self.has_auth_provider(provider) and \
603
           provider_settings.one_per_user:
604
            return False
605

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

    
616
        return True
617

    
618
    def can_remove_auth_provider(self, provider):
619
        if len(self.get_active_auth_providers()) <= 1:
620
            return False
621
        return True
622

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

    
626
    def has_auth_provider(self, provider, **kwargs):
627
        return bool(self.auth_providers.filter(module=provider,
628
                                               **kwargs).count())
629

    
630
    def add_auth_provider(self, provider, **kwargs):
631
        if self.can_add_auth_provider(provider, **kwargs):
632
            self.auth_providers.create(module=provider, active=True, **kwargs)
633
        else:
634
            raise Exception('Cannot add provider')
635

    
636
    def add_pending_auth_provider(self, pending):
637
        """
638
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
639
        the current user.
640
        """
641
        if not isinstance(pending, PendingThirdPartyUser):
642
            pending = PendingThirdPartyUser.objects.get(token=pending)
643

    
644
        provider = self.add_auth_provider(pending.provider,
645
                               identifier=pending.third_party_identifier)
646

    
647
        if email_re.match(pending.email) and pending.email != self.email:
648
            self.additionalmail_set.get_or_create(email=pending.email)
649

    
650
        pending.delete()
651
        return provider
652

    
653
    def remove_auth_provider(self, provider, **kwargs):
654
        self.auth_providers.get(module=provider, **kwargs).delete()
655

    
656
    # user urls
657
    def get_resend_activation_url(self):
658
        return reverse('send_activation', {'user_id': self.pk})
659

    
660
    def get_activation_url(self, nxt=False):
661
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
662
                                 quote(self.auth_token))
663
        if nxt:
664
            url += "&next=%s" % quote(nxt)
665
        return url
666

    
667
    def get_password_reset_url(self, token_generator=default_token_generator):
668
        return reverse('django.contrib.auth.views.password_reset_confirm',
669
                          kwargs={'uidb36':int_to_base36(self.id),
670
                                  'token':token_generator.make_token(self)})
671

    
672
    def get_auth_providers(self):
673
        return self.auth_providers.all()
674

    
675
    def get_available_auth_providers(self):
676
        """
677
        Returns a list of providers available for user to connect to.
678
        """
679
        providers = []
680
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
681
            if self.can_add_auth_provider(module):
682
                providers.append(provider_settings(self))
683

    
684
        return providers
685

    
686
    def get_active_auth_providers(self):
687
        providers = []
688
        for provider in self.auth_providers.active():
689
            if auth_providers.get_provider(provider.module).is_available_for_login():
690
                providers.append(provider)
691
        return providers
692

    
693
    @property
694
    def auth_providers_display(self):
695
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
696

    
697

    
698
class AstakosUserAuthProviderManager(models.Manager):
699

    
700
    def active(self):
701
        return self.filter(active=True)
702

    
703

    
704
class AstakosUserAuthProvider(models.Model):
705
    """
706
    Available user authentication methods.
707
    """
708
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
709
                                   null=True, default=None)
710
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
711
    module = models.CharField('Provider', max_length=255, blank=False,
712
                                default='local')
713
    identifier = models.CharField('Third-party identifier',
714
                                              max_length=255, null=True,
715
                                              blank=True)
716
    active = models.BooleanField(default=True)
717
    auth_backend = models.CharField('Backend', max_length=255, blank=False,
718
                                   default='astakos')
719

    
720
    objects = AstakosUserAuthProviderManager()
721

    
722
    class Meta:
723
        unique_together = (('identifier', 'module', 'user'), )
724

    
725
    @property
726
    def settings(self):
727
        return auth_providers.get_provider(self.module)
728

    
729
    @property
730
    def details_display(self):
731
        return self.settings.details_tpl % self.__dict__
732

    
733
    def can_remove(self):
734
        return self.user.can_remove_auth_provider(self.module)
735

    
736
    def delete(self, *args, **kwargs):
737
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
738
        self.user.set_unusable_password()
739
        self.user.save()
740
        return ret
741

    
742
    def __repr__(self):
743
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
744

    
745
    def __unicode__(self):
746
        if self.identifier:
747
            return "%s:%s" % (self.module, self.identifier)
748
        if self.auth_backend:
749
            return "%s:%s" % (self.module, self.auth_backend)
750
        return self.module
751

    
752

    
753

    
754
class Membership(models.Model):
755
    person = models.ForeignKey(AstakosUser)
756
    group = models.ForeignKey(AstakosGroup)
757
    date_requested = models.DateField(default=datetime.now(), blank=True)
758
    date_joined = models.DateField(null=True, db_index=True, blank=True)
759

    
760
    class Meta:
761
        unique_together = ("person", "group")
762

    
763
    def save(self, *args, **kwargs):
764
        if not self.id:
765
            if not self.group.moderation_enabled:
766
                self.date_joined = datetime.now()
767
        super(Membership, self).save(*args, **kwargs)
768

    
769
    @property
770
    def is_approved(self):
771
        if self.date_joined:
772
            return True
773
        return False
774

    
775
    def approve(self):
776
        if self.is_approved:
777
            return
778
        if self.group.max_participants:
779
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
780
            'Maximum participant number has been reached.'
781
        self.date_joined = datetime.now()
782
        self.save()
783
        quota_disturbed.send(sender=self, users=(self.person,))
784

    
785
    def disapprove(self):
786
        self.delete()
787
        quota_disturbed.send(sender=self, users=(self.person,))
788

    
789
class AstakosQuotaManager(models.Manager):
790
    def _update_or_create(self, **kwargs):
791
        assert kwargs, \
792
            'update_or_create() must be passed at least one keyword argument'
793
        obj, created = self.get_or_create(**kwargs)
794
        defaults = kwargs.pop('defaults', {})
795
        if created:
796
            return obj, True, False
797
        else:
798
            try:
799
                params = dict(
800
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
801
                params.update(defaults)
802
                for attr, val in params.items():
803
                    if hasattr(obj, attr):
804
                        setattr(obj, attr, val)
805
                sid = transaction.savepoint()
806
                obj.save(force_update=True)
807
                transaction.savepoint_commit(sid)
808
                return obj, False, True
809
            except IntegrityError, e:
810
                transaction.savepoint_rollback(sid)
811
                try:
812
                    return self.get(**kwargs), False, False
813
                except self.model.DoesNotExist:
814
                    raise e
815

    
816
    update_or_create = _update_or_create
817

    
818
class AstakosGroupQuota(models.Model):
819
    objects = AstakosQuotaManager()
820
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
821
    uplimit = models.BigIntegerField('Up limit', null=True)
822
    resource = models.ForeignKey(Resource)
823
    group = models.ForeignKey(AstakosGroup, blank=True)
824

    
825
    class Meta:
826
        unique_together = ("resource", "group")
827

    
828
class AstakosUserQuota(models.Model):
829
    objects = AstakosQuotaManager()
830
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
831
    uplimit = models.BigIntegerField('Up limit', null=True)
832
    resource = models.ForeignKey(Resource)
833
    user = models.ForeignKey(AstakosUser)
834

    
835
    class Meta:
836
        unique_together = ("resource", "user")
837

    
838

    
839
class ApprovalTerms(models.Model):
840
    """
841
    Model for approval terms
842
    """
843

    
844
    date = models.DateTimeField(
845
        'Issue date', db_index=True, default=datetime.now())
846
    location = models.CharField('Terms location', max_length=255)
847

    
848

    
849
class Invitation(models.Model):
850
    """
851
    Model for registring invitations
852
    """
853
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
854
                                null=True)
855
    realname = models.CharField('Real name', max_length=255)
856
    username = models.CharField('Unique ID', max_length=255, unique=True)
857
    code = models.BigIntegerField('Invitation code', db_index=True)
858
    is_consumed = models.BooleanField('Consumed?', default=False)
859
    created = models.DateTimeField('Creation date', auto_now_add=True)
860
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
861

    
862
    def __init__(self, *args, **kwargs):
863
        super(Invitation, self).__init__(*args, **kwargs)
864
        if not self.id:
865
            self.code = _generate_invitation_code()
866

    
867
    def consume(self):
868
        self.is_consumed = True
869
        self.consumed = datetime.now()
870
        self.save()
871

    
872
    def __unicode__(self):
873
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
874

    
875

    
876
class EmailChangeManager(models.Manager):
877
    @transaction.commit_on_success
878
    def change_email(self, activation_key):
879
        """
880
        Validate an activation key and change the corresponding
881
        ``User`` if valid.
882

883
        If the key is valid and has not expired, return the ``User``
884
        after activating.
885

886
        If the key is not valid or has expired, return ``None``.
887

888
        If the key is valid but the ``User`` is already active,
889
        return ``None``.
890

891
        After successful email change the activation record is deleted.
892

893
        Throws ValueError if there is already
894
        """
895
        try:
896
            email_change = self.model.objects.get(
897
                activation_key=activation_key)
898
            if email_change.activation_key_expired():
899
                email_change.delete()
900
                raise EmailChange.DoesNotExist
901
            # is there an active user with this address?
902
            try:
903
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
904
            except AstakosUser.DoesNotExist:
905
                pass
906
            else:
907
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
908
            # update user
909
            user = AstakosUser.objects.get(pk=email_change.user_id)
910
            user.email = email_change.new_email_address
911
            user.save()
912
            email_change.delete()
913
            return user
914
        except EmailChange.DoesNotExist:
915
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
916

    
917

    
918
class EmailChange(models.Model):
919
    new_email_address = models.EmailField(_(u'new e-mail address'),
920
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
921
    user = models.ForeignKey(
922
        AstakosUser, unique=True, related_name='emailchange_user')
923
    requested_at = models.DateTimeField(default=datetime.now())
924
    activation_key = models.CharField(
925
        max_length=40, unique=True, db_index=True)
926

    
927
    objects = EmailChangeManager()
928

    
929
    def activation_key_expired(self):
930
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
931
        return self.requested_at + expiration_date < datetime.now()
932

    
933

    
934
class AdditionalMail(models.Model):
935
    """
936
    Model for registring invitations
937
    """
938
    owner = models.ForeignKey(AstakosUser)
939
    email = models.EmailField()
940

    
941

    
942
def _generate_invitation_code():
943
    while True:
944
        code = randint(1, 2L ** 63 - 1)
945
        try:
946
            Invitation.objects.get(code=code)
947
            # An invitation with this code already exists, try again
948
        except Invitation.DoesNotExist:
949
            return code
950

    
951

    
952
def get_latest_terms():
953
    try:
954
        term = ApprovalTerms.objects.order_by('-id')[0]
955
        return term
956
    except IndexError:
957
        pass
958
    return None
959

    
960
class PendingThirdPartyUser(models.Model):
961
    """
962
    Model for registring successful third party user authentications
963
    """
964
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
965
    provider = models.CharField('Provider', max_length=255, blank=True)
966
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
967
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
968
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
969
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
970
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
971
    token = models.CharField('Token', max_length=255, null=True, blank=True)
972
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
973

    
974
    class Meta:
975
        unique_together = ("provider", "third_party_identifier")
976

    
977
    @property
978
    def realname(self):
979
        return '%s %s' %(self.first_name, self.last_name)
980

    
981
    @realname.setter
982
    def realname(self, value):
983
        parts = value.split(' ')
984
        if len(parts) == 2:
985
            self.first_name = parts[0]
986
            self.last_name = parts[1]
987
        else:
988
            self.last_name = parts[0]
989

    
990
    def save(self, **kwargs):
991
        if not self.id:
992
            # set username
993
            while not self.username:
994
                username =  uuid.uuid4().hex[:30]
995
                try:
996
                    AstakosUser.objects.get(username = username)
997
                except AstakosUser.DoesNotExist, e:
998
                    self.username = username
999
        super(PendingThirdPartyUser, self).save(**kwargs)
1000

    
1001
    def generate_token(self):
1002
        self.password = self.third_party_identifier
1003
        self.last_login = datetime.now()
1004
        self.token = default_token_generator.make_token(self)
1005

    
1006
class SessionCatalog(models.Model):
1007
    session_key = models.CharField(_('session key'), max_length=40)
1008
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1009

    
1010

    
1011
def create_astakos_user(u):
1012
    try:
1013
        AstakosUser.objects.get(user_ptr=u.pk)
1014
    except AstakosUser.DoesNotExist:
1015
        extended_user = AstakosUser(user_ptr_id=u.pk)
1016
        extended_user.__dict__.update(u.__dict__)
1017
        extended_user.save()
1018
    except BaseException, e:
1019
        logger.exception(e)
1020

    
1021

    
1022
def fix_superusers(sender, **kwargs):
1023
    # Associate superusers with AstakosUser
1024
    admins = User.objects.filter(is_superuser=True)
1025
    for u in admins:
1026
        create_astakos_user(u)
1027

    
1028

    
1029
def user_post_save(sender, instance, created, **kwargs):
1030
    if not created:
1031
        return
1032
    create_astakos_user(instance)
1033

    
1034

    
1035
def set_default_group(user):
1036
    try:
1037
        default = AstakosGroup.objects.get(name='default')
1038
        Membership(
1039
            group=default, person=user, date_joined=datetime.now()).save()
1040
    except AstakosGroup.DoesNotExist, e:
1041
        logger.exception(e)
1042

    
1043

    
1044
def astakosuser_pre_save(sender, instance, **kwargs):
1045
    instance.aquarium_report = False
1046
    instance.new = False
1047
    try:
1048
        db_instance = AstakosUser.objects.get(id=instance.id)
1049
    except AstakosUser.DoesNotExist:
1050
        # create event
1051
        instance.aquarium_report = True
1052
        instance.new = True
1053
    else:
1054
        get = AstakosUser.__getattribute__
1055
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1056
                   BILLING_FIELDS)
1057
        instance.aquarium_report = True if l else False
1058

    
1059

    
1060
def astakosuser_post_save(sender, instance, created, **kwargs):
1061
    if instance.aquarium_report:
1062
        report_user_event(instance, create=instance.new)
1063
    if not created:
1064
        return
1065
    set_default_group(instance)
1066
    # TODO handle socket.error & IOError
1067
    register_users((instance,))
1068
    instance.renew_token()
1069

    
1070

    
1071
def resource_post_save(sender, instance, created, **kwargs):
1072
    if not created:
1073
        return
1074
    register_resources((instance,))
1075

    
1076

    
1077
def send_quota_disturbed(sender, instance, **kwargs):
1078
    users = []
1079
    extend = users.extend
1080
    if sender == Membership:
1081
        if not instance.group.is_enabled:
1082
            return
1083
        extend([instance.person])
1084
    elif sender == AstakosUserQuota:
1085
        extend([instance.user])
1086
    elif sender == AstakosGroupQuota:
1087
        if not instance.group.is_enabled:
1088
            return
1089
        extend(instance.group.astakosuser_set.all())
1090
    elif sender == AstakosGroup:
1091
        if not instance.is_enabled:
1092
            return
1093
    quota_disturbed.send(sender=sender, users=users)
1094

    
1095

    
1096
def on_quota_disturbed(sender, users, **kwargs):
1097
#     print '>>>', locals()
1098
    if not users:
1099
        return
1100
    send_quota(users)
1101

    
1102
def renew_token(sender, instance, **kwargs):
1103
    if not instance.id:
1104
        instance.renew_token()
1105

    
1106
post_syncdb.connect(fix_superusers)
1107
post_save.connect(user_post_save, sender=User)
1108
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1109
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1110
post_save.connect(resource_post_save, sender=Resource)
1111

    
1112
quota_disturbed = Signal(providing_args=["users"])
1113
quota_disturbed.connect(on_quota_disturbed)
1114

    
1115
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1116
post_delete.connect(send_quota_disturbed, sender=Membership)
1117
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1118
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1119
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1120
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1121

    
1122
pre_save.connect(renew_token, sender=AstakosUser)
1123
pre_save.connect(renew_token, sender=Service)