Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (41.5 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import hashlib
35
import uuid
36
import logging
37
import json
38

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

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

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

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

    
79
import astakos.im.messages as astakos_messages
80

    
81
logger = logging.getLogger(__name__)
82

    
83
DEFAULT_CONTENT_TYPE = None
84
try:
85
    content_type = ContentType.objects.get(app_label='im', model='astakosuser')
86
except:
87
    content_type = DEFAULT_CONTENT_TYPE
88

    
89
RESOURCE_SEPARATOR = '.'
90

    
91
inf = float('inf')
92

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

    
103
    def renew_token(self):
104
        md5 = hashlib.md5()
105
        md5.update(self.name.encode('ascii', 'ignore'))
106
        md5.update(self.url.encode('ascii', 'ignore'))
107
        md5.update(asctime())
108

    
109
        self.auth_token = b64encode(md5.digest())
110
        self.auth_token_created = datetime.now()
111
        self.auth_token_expires = self.auth_token_created + \
112
            timedelta(hours=AUTH_TOKEN_DURATION)
113

    
114
    def __str__(self):
115
        return self.name
116

    
117
    @property
118
    def resources(self):
119
        return self.resource_set.all()
120

    
121
    @resources.setter
122
    def resources(self, resources):
123
        for s in resources:
124
            self.resource_set.create(**s)
125

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

    
137

    
138
class ResourceMetadata(models.Model):
139
    key = models.CharField('Name', max_length=255, unique=True, db_index=True)
140
    value = models.CharField('Value', max_length=255)
141

    
142

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

    
151
    def __str__(self):
152
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
153

    
154

    
155
class GroupKind(models.Model):
156
    name = models.CharField('Name', max_length=255, unique=True, db_index=True)
157

    
158
    def __str__(self):
159
        return self.name
160

    
161

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

    
202
    @property
203
    def is_disabled(self):
204
        if not self.approval_date:
205
            return True
206
        return False
207

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

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

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

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

    
252
#     def disapprove_member(self, person):
253
#         self.membership_set.remove(person=person)
254

    
255
    @property
256
    def members(self):
257
        q = self.membership_set.select_related().all()
258
        return [m.person for m in q]
259

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

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

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

    
285
    @property
286
    def policies(self):
287
        return self.astakosgroupquota_set.select_related().all()
288

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

    
298
    @property
299
    def owners(self):
300
        return self.owner.all()
301

    
302
    @property
303
    def owner_details(self):
304
        return self.owner.select_related().all()
305

    
306
    @owners.setter
307
    def owners(self, l):
308
        self.owner = l
309
        map(self.approve_member, l)
310

    
311

    
312

    
313
class AstakosUserManager(UserManager):
314

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

    
324
    def get_by_email(self, email):
325
        return self.get(email=email)
326

    
327
class AstakosUser(User):
328
    """
329
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
330
    """
331
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
332
                                   null=True)
333

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

    
343

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

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

    
356
    updated = models.DateTimeField('Update date')
357
    is_verified = models.BooleanField('Is verified?', default=False)
358

    
359
    email_verified = models.BooleanField('Email verified?', default=False)
360

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

    
367
    activation_sent = models.DateTimeField(
368
        'Activation sent data', null=True, blank=True)
369

    
370
    policy = models.ManyToManyField(
371
        Resource, null=True, through='AstakosUserQuota')
372

    
373
    astakos_groups = models.ManyToManyField(
374
        AstakosGroup, verbose_name=_('agroups'), blank=True,
375
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
376
        through='Membership')
377

    
378
    __has_signed_terms = False
379
    disturbed_quota = models.BooleanField('Needs quotaholder syncing',
380
                                           default=False, db_index=True)
381

    
382
    objects = AstakosUserManager()
383

    
384
    owner = models.ManyToManyField(
385
        AstakosGroup, related_name='owner', null=True)
386

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
502
        if not self.id:
503
            # set username
504
            self.username = self.email
505

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

    
511
        super(AstakosUser, self).save(**kwargs)
512

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

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

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

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

    
543
    def __unicode__(self):
544
        return '%s (%s)' % (self.realname, self.email)
545

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

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

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

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

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

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

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

    
604
        if 'provider_info' in kwargs:
605
            kwargs.pop('provider_info')
606

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

    
617
        return True
618

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

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

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

    
631
    def add_auth_provider(self, provider, **kwargs):
632
        info_data = ''
633
        if 'provider_info' in kwargs:
634
            info_data = kwargs.pop('provider_info')
635
            if isinstance(info_data, dict):
636
                info_data = json.dumps(info_data)
637

    
638
        if self.can_add_auth_provider(provider, **kwargs):
639
            self.auth_providers.create(module=provider, active=True,
640
                                       info_data=info_data,
641
                                       **kwargs)
642
        else:
643
            raise Exception('Cannot add provider')
644

    
645
    def add_pending_auth_provider(self, pending):
646
        """
647
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
648
        the current user.
649
        """
650
        if not isinstance(pending, PendingThirdPartyUser):
651
            pending = PendingThirdPartyUser.objects.get(token=pending)
652

    
653
        provider = self.add_auth_provider(pending.provider,
654
                               identifier=pending.third_party_identifier,
655
                                affiliation=pending.affiliation,
656
                                          provider_info=pending.info)
657

    
658
        if email_re.match(pending.email or '') and pending.email != self.email:
659
            self.additionalmail_set.get_or_create(email=pending.email)
660

    
661
        pending.delete()
662
        return provider
663

    
664
    def remove_auth_provider(self, provider, **kwargs):
665
        self.auth_providers.get(module=provider, **kwargs).delete()
666

    
667
    # user urls
668
    def get_resend_activation_url(self):
669
        return reverse('send_activation', kwargs={'user_id': self.pk})
670

    
671
    def get_provider_remove_url(self, module, **kwargs):
672
        return reverse('remove_auth_provider', kwargs={
673
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
674

    
675
    def get_activation_url(self, nxt=False):
676
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
677
                                 quote(self.auth_token))
678
        if nxt:
679
            url += "&next=%s" % quote(nxt)
680
        return url
681

    
682
    def get_password_reset_url(self, token_generator=default_token_generator):
683
        return reverse('django.contrib.auth.views.password_reset_confirm',
684
                          kwargs={'uidb36':int_to_base36(self.id),
685
                                  'token':token_generator.make_token(self)})
686

    
687
    def get_auth_providers(self):
688
        return self.auth_providers.all()
689

    
690
    def get_available_auth_providers(self):
691
        """
692
        Returns a list of providers available for user to connect to.
693
        """
694
        providers = []
695
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
696
            if self.can_add_auth_provider(module):
697
                providers.append(provider_settings(self))
698

    
699
        return providers
700

    
701
    def get_active_auth_providers(self):
702
        providers = []
703
        for provider in self.auth_providers.active():
704
            if auth_providers.get_provider(provider.module).is_available_for_login():
705
                providers.append(provider)
706
        return providers
707

    
708
    @property
709
    def auth_providers_display(self):
710
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
711

    
712
    def get_inactive_message(self):
713
        msg_extra = ''
714
        message = ''
715
        if self.activation_sent:
716
            if self.email_verified:
717
                message = _(astakos_messages.ACCOUNT_INACTIVE)
718
            else:
719
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
720
                if astakos_settings.MODERATION_ENABLED:
721
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
722
                else:
723
                    url = self.get_resend_activation_url()
724
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
725
                                _('<a href="%s">%s?</a>') % (url,
726
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
727
        else:
728
            if astakos_settings.MODERATION_ENABLED:
729
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
730
            else:
731
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
732
                url = self.get_resend_activation_url()
733
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
734
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
735

    
736
        return mark_safe(message + msg_extra)
737

    
738

    
739
class AstakosUserAuthProviderManager(models.Manager):
740

    
741
    def active(self):
742
        return self.filter(active=True)
743

    
744

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

    
763
    objects = AstakosUserAuthProviderManager()
764

    
765
    class Meta:
766
        unique_together = (('identifier', 'module', 'user'), )
767
        ordering = ('module', 'created')
768

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

    
778
        for key,value in self.info.iteritems():
779
            setattr(self, 'info_%s' % key, value)
780

    
781

    
782
    @property
783
    def settings(self):
784
        return auth_providers.get_provider(self.module)
785

    
786
    @property
787
    def details_display(self):
788
        try:
789
          return self.settings.get_details_tpl_display % self.__dict__
790
        except:
791
          return ''
792

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

    
806
    def can_remove(self):
807
        return self.user.can_remove_auth_provider(self.module)
808

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

    
816
    def __repr__(self):
817
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
818

    
819
    def __unicode__(self):
820
        if self.identifier:
821
            return "%s:%s" % (self.module, self.identifier)
822
        if self.auth_backend:
823
            return "%s:%s" % (self.module, self.auth_backend)
824
        return self.module
825

    
826
    def save(self, *args, **kwargs):
827
        self.info_data = json.dumps(self.info)
828
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
829

    
830

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

    
837
    class Meta:
838
        unique_together = ("person", "group")
839

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

    
846
    @property
847
    def is_approved(self):
848
        if self.date_joined:
849
            return True
850
        return False
851

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

    
862
    def disapprove(self):
863
        self.delete()
864
        quota_disturbed.send(sender=self, users=(self.person,))
865

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

    
893
    update_or_create = _update_or_create
894

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

    
902
    class Meta:
903
        unique_together = ("resource", "group")
904

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

    
912
    class Meta:
913
        unique_together = ("resource", "user")
914

    
915

    
916
class ApprovalTerms(models.Model):
917
    """
918
    Model for approval terms
919
    """
920

    
921
    date = models.DateTimeField(
922
        'Issue date', db_index=True, default=datetime.now())
923
    location = models.CharField('Terms location', max_length=255)
924

    
925

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

    
939
    def __init__(self, *args, **kwargs):
940
        super(Invitation, self).__init__(*args, **kwargs)
941
        if not self.id:
942
            self.code = _generate_invitation_code()
943

    
944
    def consume(self):
945
        self.is_consumed = True
946
        self.consumed = datetime.now()
947
        self.save()
948

    
949
    def __unicode__(self):
950
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
951

    
952

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

960
        If the key is valid and has not expired, return the ``User``
961
        after activating.
962

963
        If the key is not valid or has expired, return ``None``.
964

965
        If the key is valid but the ``User`` is already active,
966
        return ``None``.
967

968
        After successful email change the activation record is deleted.
969

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

    
994

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

    
1004
    objects = EmailChangeManager()
1005

    
1006
    def activation_key_expired(self):
1007
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1008
        return self.requested_at + expiration_date < datetime.now()
1009

    
1010

    
1011
class AdditionalMail(models.Model):
1012
    """
1013
    Model for registring invitations
1014
    """
1015
    owner = models.ForeignKey(AstakosUser)
1016
    email = models.EmailField()
1017

    
1018

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

    
1028

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

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

    
1052
    class Meta:
1053
        unique_together = ("provider", "third_party_identifier")
1054

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

    
1064
        return user
1065

    
1066
    @property
1067
    def realname(self):
1068
        return '%s %s' %(self.first_name, self.last_name)
1069

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

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

    
1090
    def generate_token(self):
1091
        self.password = self.third_party_identifier
1092
        self.last_login = datetime.now()
1093
        self.token = default_token_generator.make_token(self)
1094

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

    
1099

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

    
1112

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

    
1119

    
1120
def user_post_save(sender, instance, created, **kwargs):
1121
    if not created:
1122
        return
1123
    create_astakos_user(instance)
1124

    
1125

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

    
1134

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

    
1150

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

    
1160

    
1161
def resource_post_save(sender, instance, created, **kwargs):
1162
    if not created:
1163
        return
1164
    register_resources((instance,))
1165

    
1166

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

    
1185

    
1186
def on_quota_disturbed(sender, users, **kwargs):
1187
#     print '>>>', locals()
1188
    if not users:
1189
        return
1190
    send_quota(users)
1191

    
1192
def renew_token(sender, instance, **kwargs):
1193
    if not instance.auth_token:
1194
        instance.renew_token()
1195

    
1196
post_syncdb.connect(fix_superusers)
1197
post_save.connect(user_post_save, sender=User)
1198
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1199
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1200
post_save.connect(resource_post_save, sender=Resource)
1201

    
1202
quota_disturbed = Signal(providing_args=["users"])
1203
quota_disturbed.connect(on_quota_disturbed)
1204

    
1205
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1206
post_delete.connect(send_quota_disturbed, sender=Membership)
1207
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1208
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1209
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1210
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1211

    
1212
pre_save.connect(renew_token, sender=AstakosUser)
1213
pre_save.connect(renew_token, sender=Service)