Statistics
| Branch: | Tag: | Revision:

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

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(models.Manager):
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
    # Use UserManager to get the create_user method, etc.
326
    objects = UserManager()
327

    
328
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
329
                                   null=True)
330

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

    
340

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

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

    
353
    updated = models.DateTimeField('Update date')
354
    is_verified = models.BooleanField('Is verified?', default=False)
355

    
356
    email_verified = models.BooleanField('Email verified?', default=False)
357

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

    
364
    activation_sent = models.DateTimeField(
365
        'Activation sent data', null=True, blank=True)
366

    
367
    policy = models.ManyToManyField(
368
        Resource, null=True, through='AstakosUserQuota')
369

    
370
    astakos_groups = models.ManyToManyField(
371
        AstakosGroup, verbose_name=_('agroups'), blank=True,
372
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
373
        through='Membership')
374

    
375
    __has_signed_terms = False
376
    disturbed_quota = models.BooleanField('Needs quotaholder syncing',
377
                                           default=False, db_index=True)
378

    
379
    objects = AstakosUserManager()
380
    owner = models.ManyToManyField(
381
        AstakosGroup, related_name='owner', null=True)
382

    
383
    class Meta:
384
        unique_together = ("provider", "third_party_identifier")
385

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
515
        super(AstakosUser, self).save(**kwargs)
516

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

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

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

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

    
547
    def __unicode__(self):
548
        return '%s (%s)' % (self.realname, self.email)
549

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

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

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

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

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

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

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

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

    
618
        return True
619

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

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

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

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

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

    
646
        provider = self.add_auth_provider(pending.provider,
647
                               identifier=pending.third_party_identifier)
648

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

    
652
        pending.delete()
653
        return provider
654

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

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

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

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

    
674
    def get_auth_providers(self):
675
        return self.auth_providers.all()
676

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

    
686
        return providers
687

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

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

    
699

    
700
class AstakosUserAuthProviderManager(models.Manager):
701

    
702
    def active(self):
703
        return self.filter(active=True)
704

    
705

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

    
722
    objects = AstakosUserAuthProviderManager()
723

    
724
    class Meta:
725
        unique_together = (('identifier', 'module', 'user'), )
726

    
727
    @property
728
    def settings(self):
729
        return auth_providers.get_provider(self.module)
730

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

    
735
    def can_remove(self):
736
        return self.user.can_remove_auth_provider(self.module)
737

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

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

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

    
754

    
755

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

    
762
    class Meta:
763
        unique_together = ("person", "group")
764

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

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

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

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

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

    
818
    update_or_create = _update_or_create
819

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

    
827
    class Meta:
828
        unique_together = ("resource", "group")
829

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

    
837
    class Meta:
838
        unique_together = ("resource", "user")
839

    
840

    
841
class ApprovalTerms(models.Model):
842
    """
843
    Model for approval terms
844
    """
845

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

    
850

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

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

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

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

    
877

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

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

888
        If the key is not valid or has expired, return ``None``.
889

890
        If the key is valid but the ``User`` is already active,
891
        return ``None``.
892

893
        After successful email change the activation record is deleted.
894

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

    
919

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

    
929
    objects = EmailChangeManager()
930

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

    
935

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

    
943

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

    
953

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

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

    
976
    class Meta:
977
        unique_together = ("provider", "third_party_identifier")
978

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

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

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

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

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

    
1012

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

    
1023

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

    
1030

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

    
1036

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

    
1045

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

    
1061

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

    
1072

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

    
1078

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

    
1097

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

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

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

    
1114
quota_disturbed = Signal(providing_args=["users"])
1115
quota_disturbed.connect(on_quota_disturbed)
1116

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

    
1124
pre_save.connect(renew_token, sender=AstakosUser)
1125
pre_save.connect(renew_token, sender=Service)