Statistics
| Branch: | Tag: | Revision:

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

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

    
696
class AstakosUserAuthProviderManager(models.Manager):
697

    
698
    def active(self):
699
        return self.filter(active=True)
700

    
701

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

    
718
    objects = AstakosUserAuthProviderManager()
719

    
720
    class Meta:
721
        unique_together = (('identifier', 'module', 'user'), )
722

    
723
    @property
724
    def settings(self):
725
        return auth_providers.get_provider(self.module)
726

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

    
731
    def can_remove(self):
732
        return self.user.can_remove_auth_provider(self.module)
733

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

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

    
743

    
744
class Membership(models.Model):
745
    person = models.ForeignKey(AstakosUser)
746
    group = models.ForeignKey(AstakosGroup)
747
    date_requested = models.DateField(default=datetime.now(), blank=True)
748
    date_joined = models.DateField(null=True, db_index=True, blank=True)
749

    
750
    class Meta:
751
        unique_together = ("person", "group")
752

    
753
    def save(self, *args, **kwargs):
754
        if not self.id:
755
            if not self.group.moderation_enabled:
756
                self.date_joined = datetime.now()
757
        super(Membership, self).save(*args, **kwargs)
758

    
759
    @property
760
    def is_approved(self):
761
        if self.date_joined:
762
            return True
763
        return False
764

    
765
    def approve(self):
766
        if self.is_approved:
767
            return
768
        if self.group.max_participants:
769
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
770
            'Maximum participant number has been reached.'
771
        self.date_joined = datetime.now()
772
        self.save()
773
        quota_disturbed.send(sender=self, users=(self.person,))
774

    
775
    def disapprove(self):
776
        self.delete()
777
        quota_disturbed.send(sender=self, users=(self.person,))
778

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

    
806
    update_or_create = _update_or_create
807

    
808
class AstakosGroupQuota(models.Model):
809
    objects = AstakosQuotaManager()
810
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
811
    uplimit = models.BigIntegerField('Up limit', null=True)
812
    resource = models.ForeignKey(Resource)
813
    group = models.ForeignKey(AstakosGroup, blank=True)
814

    
815
    class Meta:
816
        unique_together = ("resource", "group")
817

    
818
class AstakosUserQuota(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
    user = models.ForeignKey(AstakosUser)
824

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

    
828

    
829
class ApprovalTerms(models.Model):
830
    """
831
    Model for approval terms
832
    """
833

    
834
    date = models.DateTimeField(
835
        'Issue date', db_index=True, default=datetime.now())
836
    location = models.CharField('Terms location', max_length=255)
837

    
838

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

    
852
    def __init__(self, *args, **kwargs):
853
        super(Invitation, self).__init__(*args, **kwargs)
854
        if not self.id:
855
            self.code = _generate_invitation_code()
856

    
857
    def consume(self):
858
        self.is_consumed = True
859
        self.consumed = datetime.now()
860
        self.save()
861

    
862
    def __unicode__(self):
863
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
864

    
865

    
866
class EmailChangeManager(models.Manager):
867
    @transaction.commit_on_success
868
    def change_email(self, activation_key):
869
        """
870
        Validate an activation key and change the corresponding
871
        ``User`` if valid.
872

873
        If the key is valid and has not expired, return the ``User``
874
        after activating.
875

876
        If the key is not valid or has expired, return ``None``.
877

878
        If the key is valid but the ``User`` is already active,
879
        return ``None``.
880

881
        After successful email change the activation record is deleted.
882

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

    
907

    
908
class EmailChange(models.Model):
909
    new_email_address = models.EmailField(_(u'new e-mail address'),
910
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
911
    user = models.ForeignKey(
912
        AstakosUser, unique=True, related_name='emailchange_user')
913
    requested_at = models.DateTimeField(default=datetime.now())
914
    activation_key = models.CharField(
915
        max_length=40, unique=True, db_index=True)
916

    
917
    objects = EmailChangeManager()
918

    
919
    def activation_key_expired(self):
920
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
921
        return self.requested_at + expiration_date < datetime.now()
922

    
923

    
924
class AdditionalMail(models.Model):
925
    """
926
    Model for registring invitations
927
    """
928
    owner = models.ForeignKey(AstakosUser)
929
    email = models.EmailField()
930

    
931

    
932
def _generate_invitation_code():
933
    while True:
934
        code = randint(1, 2L ** 63 - 1)
935
        try:
936
            Invitation.objects.get(code=code)
937
            # An invitation with this code already exists, try again
938
        except Invitation.DoesNotExist:
939
            return code
940

    
941

    
942
def get_latest_terms():
943
    try:
944
        term = ApprovalTerms.objects.order_by('-id')[0]
945
        return term
946
    except IndexError:
947
        pass
948
    return None
949

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

    
964
    class Meta:
965
        unique_together = ("provider", "third_party_identifier")
966

    
967
    @property
968
    def realname(self):
969
        return '%s %s' %(self.first_name, self.last_name)
970

    
971
    @realname.setter
972
    def realname(self, value):
973
        parts = value.split(' ')
974
        if len(parts) == 2:
975
            self.first_name = parts[0]
976
            self.last_name = parts[1]
977
        else:
978
            self.last_name = parts[0]
979

    
980
    def save(self, **kwargs):
981
        if not self.id:
982
            # set username
983
            while not self.username:
984
                username =  uuid.uuid4().hex[:30]
985
                try:
986
                    AstakosUser.objects.get(username = username)
987
                except AstakosUser.DoesNotExist, e:
988
                    self.username = username
989
        super(PendingThirdPartyUser, self).save(**kwargs)
990

    
991
    def generate_token(self):
992
        self.password = self.third_party_identifier
993
        self.last_login = datetime.now()
994
        self.token = default_token_generator.make_token(self)
995

    
996
class SessionCatalog(models.Model):
997
    session_key = models.CharField(_('session key'), max_length=40)
998
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
999

    
1000

    
1001
def create_astakos_user(u):
1002
    try:
1003
        AstakosUser.objects.get(user_ptr=u.pk)
1004
    except AstakosUser.DoesNotExist:
1005
        extended_user = AstakosUser(user_ptr_id=u.pk)
1006
        extended_user.__dict__.update(u.__dict__)
1007
        extended_user.save()
1008
    except BaseException, e:
1009
        logger.exception(e)
1010

    
1011

    
1012
def fix_superusers(sender, **kwargs):
1013
    # Associate superusers with AstakosUser
1014
    admins = User.objects.filter(is_superuser=True)
1015
    for u in admins:
1016
        create_astakos_user(u)
1017

    
1018

    
1019
def user_post_save(sender, instance, created, **kwargs):
1020
    if not created:
1021
        return
1022
    create_astakos_user(instance)
1023

    
1024

    
1025
def set_default_group(user):
1026
    try:
1027
        default = AstakosGroup.objects.get(name='default')
1028
        Membership(
1029
            group=default, person=user, date_joined=datetime.now()).save()
1030
    except AstakosGroup.DoesNotExist, e:
1031
        logger.exception(e)
1032

    
1033

    
1034
def astakosuser_pre_save(sender, instance, **kwargs):
1035
    instance.aquarium_report = False
1036
    instance.new = False
1037
    try:
1038
        db_instance = AstakosUser.objects.get(id=instance.id)
1039
    except AstakosUser.DoesNotExist:
1040
        # create event
1041
        instance.aquarium_report = True
1042
        instance.new = True
1043
    else:
1044
        get = AstakosUser.__getattribute__
1045
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1046
                   BILLING_FIELDS)
1047
        instance.aquarium_report = True if l else False
1048

    
1049

    
1050
def astakosuser_post_save(sender, instance, created, **kwargs):
1051
    if instance.aquarium_report:
1052
        report_user_event(instance, create=instance.new)
1053
    if not created:
1054
        return
1055
    set_default_group(instance)
1056
    # TODO handle socket.error & IOError
1057
    register_users((instance,))
1058
    instance.renew_token()
1059

    
1060

    
1061
def resource_post_save(sender, instance, created, **kwargs):
1062
    if not created:
1063
        return
1064
    register_resources((instance,))
1065

    
1066

    
1067
def send_quota_disturbed(sender, instance, **kwargs):
1068
    users = []
1069
    extend = users.extend
1070
    if sender == Membership:
1071
        if not instance.group.is_enabled:
1072
            return
1073
        extend([instance.person])
1074
    elif sender == AstakosUserQuota:
1075
        extend([instance.user])
1076
    elif sender == AstakosGroupQuota:
1077
        if not instance.group.is_enabled:
1078
            return
1079
        extend(instance.group.astakosuser_set.all())
1080
    elif sender == AstakosGroup:
1081
        if not instance.is_enabled:
1082
            return
1083
    quota_disturbed.send(sender=sender, users=users)
1084

    
1085

    
1086
def on_quota_disturbed(sender, users, **kwargs):
1087
#     print '>>>', locals()
1088
    if not users:
1089
        return
1090
    send_quota(users)
1091

    
1092
def renew_token(sender, instance, **kwargs):
1093
    if not instance.id:
1094
        instance.renew_token()
1095

    
1096
post_syncdb.connect(fix_superusers)
1097
post_save.connect(user_post_save, sender=User)
1098
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1099
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1100
post_save.connect(resource_post_save, sender=Resource)
1101

    
1102
quota_disturbed = Signal(providing_args=["users"])
1103
quota_disturbed.connect(on_quota_disturbed)
1104

    
1105
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1106
post_delete.connect(send_quota_disturbed, sender=Membership)
1107
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1108
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1109
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1110
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1111

    
1112
pre_save.connect(renew_token, sender=AstakosUser)
1113
pre_save.connect(renew_token, sender=Service)