Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 7233d542

History | View | Annotate | Download (37.9 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
            self.username = self.email
502
        
503
        self.validate_unique_email_isactive()
504
        if self.is_active and self.activation_sent:
505
            # reset the activation sent
506
            self.activation_sent = None
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
        q = q.filter(is_active = self.is_active)
557
        if self.id:
558
            q = q.filter(~Q(id = self.id))
559
        if q.count() != 0:
560
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
561

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

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

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

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

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

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

    
611
        return True
612

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

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

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

    
625
    def add_auth_provider(self, provider, **kwargs):
626
        if self.can_add_auth_provider(provider, **kwargs):
627
            self.auth_providers.create(module=provider, active=True, **kwargs)
628
        else:
629
            raise Exception('Cannot add provider')
630

    
631
    def add_pending_auth_provider(self, pending):
632
        """
633
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
634
        the current user.
635
        """
636
        if not isinstance(pending, PendingThirdPartyUser):
637
            pending = PendingThirdPartyUser.objects.get(token=pending)
638

    
639
        provider = self.add_auth_provider(pending.provider,
640
                               identifier=pending.third_party_identifier)
641

    
642
        if email_re.match(pending.email) and pending.email != self.email:
643
            self.additionalmail_set.get_or_create(email=pending.email)
644

    
645
        pending.delete()
646
        return provider
647

    
648
    def remove_auth_provider(self, provider, **kwargs):
649
        self.auth_providers.get(module=provider, **kwargs).delete()
650

    
651
    # user urls
652
    def get_resend_activation_url(self):
653
        return reverse('send_activation', {'user_id': self.pk})
654

    
655
    def get_activation_url(self, nxt=False):
656
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
657
                                 quote(self.auth_token))
658
        if nxt:
659
            url += "&next=%s" % quote(nxt)
660
        return url
661

    
662
    def get_password_reset_url(self, token_generator=default_token_generator):
663
        return reverse('django.contrib.auth.views.password_reset_confirm',
664
                          kwargs={'uidb36':int_to_base36(self.id),
665
                                  'token':token_generator.make_token(self)})
666

    
667
    def get_auth_providers(self):
668
        return self.auth_providers.all()
669

    
670
    def get_available_auth_providers(self):
671
        """
672
        Returns a list of providers available for user to connect to.
673
        """
674
        providers = []
675
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
676
            if self.can_add_auth_provider(module):
677
                providers.append(provider_settings(self))
678

    
679
        return providers
680

    
681
    def get_active_auth_providers(self):
682
        providers = []
683
        for provider in self.auth_providers.active():
684
            if auth_providers.get_provider(provider.module).is_available_for_login():
685
                providers.append(provider)
686
        return providers
687

    
688
    @property
689
    def auth_providers_display(self):
690
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
691

    
692

    
693
class AstakosUserAuthProviderManager(models.Manager):
694

    
695
    def active(self):
696
        return self.filter(active=True)
697

    
698

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

    
715
    objects = AstakosUserAuthProviderManager()
716

    
717
    class Meta:
718
        unique_together = (('identifier', 'module', 'user'), )
719

    
720
    @property
721
    def settings(self):
722
        return auth_providers.get_provider(self.module)
723

    
724
    @property
725
    def details_display(self):
726
        return self.settings.details_tpl % self.__dict__
727

    
728
    def can_remove(self):
729
        return self.user.can_remove_auth_provider(self.module)
730

    
731
    def delete(self, *args, **kwargs):
732
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
733
        if self.module == 'local':
734
            self.user.set_unusable_password()
735
            self.user.save()
736
        return ret
737

    
738
    def __repr__(self):
739
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
740

    
741
    def __unicode__(self):
742
        if self.identifier:
743
            return "%s:%s" % (self.module, self.identifier)
744
        if self.auth_backend:
745
            return "%s:%s" % (self.module, self.auth_backend)
746
        return self.module
747

    
748

    
749

    
750
class Membership(models.Model):
751
    person = models.ForeignKey(AstakosUser)
752
    group = models.ForeignKey(AstakosGroup)
753
    date_requested = models.DateField(default=datetime.now(), blank=True)
754
    date_joined = models.DateField(null=True, db_index=True, blank=True)
755

    
756
    class Meta:
757
        unique_together = ("person", "group")
758

    
759
    def save(self, *args, **kwargs):
760
        if not self.id:
761
            if not self.group.moderation_enabled:
762
                self.date_joined = datetime.now()
763
        super(Membership, self).save(*args, **kwargs)
764

    
765
    @property
766
    def is_approved(self):
767
        if self.date_joined:
768
            return True
769
        return False
770

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

    
781
    def disapprove(self):
782
        self.delete()
783
        quota_disturbed.send(sender=self, users=(self.person,))
784

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

    
812
    update_or_create = _update_or_create
813

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

    
821
    class Meta:
822
        unique_together = ("resource", "group")
823

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

    
831
    class Meta:
832
        unique_together = ("resource", "user")
833

    
834

    
835
class ApprovalTerms(models.Model):
836
    """
837
    Model for approval terms
838
    """
839

    
840
    date = models.DateTimeField(
841
        'Issue date', db_index=True, default=datetime.now())
842
    location = models.CharField('Terms location', max_length=255)
843

    
844

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

    
858
    def __init__(self, *args, **kwargs):
859
        super(Invitation, self).__init__(*args, **kwargs)
860
        if not self.id:
861
            self.code = _generate_invitation_code()
862

    
863
    def consume(self):
864
        self.is_consumed = True
865
        self.consumed = datetime.now()
866
        self.save()
867

    
868
    def __unicode__(self):
869
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
870

    
871

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

879
        If the key is valid and has not expired, return the ``User``
880
        after activating.
881

882
        If the key is not valid or has expired, return ``None``.
883

884
        If the key is valid but the ``User`` is already active,
885
        return ``None``.
886

887
        After successful email change the activation record is deleted.
888

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

    
913

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

    
923
    objects = EmailChangeManager()
924

    
925
    def activation_key_expired(self):
926
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
927
        return self.requested_at + expiration_date < datetime.now()
928

    
929

    
930
class AdditionalMail(models.Model):
931
    """
932
    Model for registring invitations
933
    """
934
    owner = models.ForeignKey(AstakosUser)
935
    email = models.EmailField()
936

    
937

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

    
947

    
948
def get_latest_terms():
949
    try:
950
        term = ApprovalTerms.objects.order_by('-id')[0]
951
        return term
952
    except IndexError:
953
        pass
954
    return None
955

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

    
970
    class Meta:
971
        unique_together = ("provider", "third_party_identifier")
972

    
973
    @property
974
    def realname(self):
975
        return '%s %s' %(self.first_name, self.last_name)
976

    
977
    @realname.setter
978
    def realname(self, value):
979
        parts = value.split(' ')
980
        if len(parts) == 2:
981
            self.first_name = parts[0]
982
            self.last_name = parts[1]
983
        else:
984
            self.last_name = parts[0]
985

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

    
997
    def generate_token(self):
998
        self.password = self.third_party_identifier
999
        self.last_login = datetime.now()
1000
        self.token = default_token_generator.make_token(self)
1001

    
1002
class SessionCatalog(models.Model):
1003
    session_key = models.CharField(_('session key'), max_length=40)
1004
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1005

    
1006

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

    
1017

    
1018
def fix_superusers(sender, **kwargs):
1019
    # Associate superusers with AstakosUser
1020
    admins = User.objects.filter(is_superuser=True)
1021
    for u in admins:
1022
        create_astakos_user(u)
1023

    
1024

    
1025
def user_post_save(sender, instance, created, **kwargs):
1026
    if not created:
1027
        return
1028
    create_astakos_user(instance)
1029

    
1030

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

    
1039

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

    
1055

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

    
1066

    
1067
def resource_post_save(sender, instance, created, **kwargs):
1068
    if not created:
1069
        return
1070
    register_resources((instance,))
1071

    
1072

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

    
1091

    
1092
def on_quota_disturbed(sender, users, **kwargs):
1093
#     print '>>>', locals()
1094
    if not users:
1095
        return
1096
    send_quota(users)
1097

    
1098
def renew_token(sender, instance, **kwargs):
1099
    if not instance.id:
1100
        instance.renew_token()
1101

    
1102
post_syncdb.connect(fix_superusers)
1103
post_save.connect(user_post_save, sender=User)
1104
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1105
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1106
post_save.connect(resource_post_save, sender=Resource)
1107

    
1108
quota_disturbed = Signal(providing_args=["users"])
1109
quota_disturbed.connect(on_quota_disturbed)
1110

    
1111
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1112
post_delete.connect(send_quota_disturbed, sender=Membership)
1113
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1114
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1115
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1116
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1117

    
1118
pre_save.connect(renew_token, sender=AstakosUser)
1119
pre_save.connect(renew_token, sender=Service)