Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 3a72a5d4

History | View | Annotate | Download (39.1 kB)

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

    
34
import hashlib
35
import uuid
36
import logging
37
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.core.validators import email_re
65

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

    
77
import astakos.im.messages as astakos_messages
78

    
79
logger = logging.getLogger(__name__)
80

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

    
87
RESOURCE_SEPARATOR = '.'
88

    
89
inf = float('inf')
90

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

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

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

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

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

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

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

    
135

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

    
140

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

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

    
152

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

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

    
159

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
309

    
310

    
311
class AstakosUserManager(UserManager):
312

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

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

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

    
338

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

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

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

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

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

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

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

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

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

    
377
    objects = AstakosUserManager()
378

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
500
        if not self.id:
501
            # set username
502
            self.username = self.email
503

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

    
509
        super(AstakosUser, self).save(**kwargs)
510

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

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

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

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

    
541
    def __unicode__(self):
542
        return '%s (%s)' % (self.realname, self.email)
543

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

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

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

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

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

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

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

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

    
612
        return True
613

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

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

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

    
626
    def add_auth_provider(self, provider, **kwargs):
627
        info_data = ''
628
        if 'provider_info' in kwargs:
629
            info_data = json.dumps(kwargs.pop('provider_info'))
630

    
631
        if self.can_add_auth_provider(provider, **kwargs):
632
            self.auth_providers.create(module=provider, active=True,
633
                                       info_data=info_data,
634
                                       **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 or '') and pending.email != self.email:
650
            self.additionalmail_set.get_or_create(email=pending.email)
651

    
652
        pending.delete()
653
        return provider
654

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

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

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

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

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

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

    
686
        return providers
687

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

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

    
699

    
700
class AstakosUserAuthProviderManager(models.Manager):
701

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

    
705

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

    
723
    objects = AstakosUserAuthProviderManager()
724

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

    
728
    def __init__(self, *args, **kwargs):
729
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
730
        try:
731
            self.info = json.loads(self.info_data)
732
        except:
733
            self.info = {}
734
        for key,value in self.info.iteritems():
735
            setattr(self, 'info_%s' % key, value)
736

    
737

    
738
    @property
739
    def settings(self):
740
        return auth_providers.get_provider(self.module)
741

    
742
    @property
743
    def details_display(self):
744
        return self.settings.get_details_tpl_display % self.__dict__
745

    
746
    @property
747
    def title_display(self):
748
        title_tpl = self.settings.get_title_display
749
        try:
750
            if self.settings.get_user_title_display:
751
                title_tpl = self.settings.get_user_title_display
752
        except Exception, e:
753
            pass
754
        return title_tpl % self.__dict__
755

    
756
    def can_remove(self):
757
        return self.user.can_remove_auth_provider(self.module)
758

    
759
    def delete(self, *args, **kwargs):
760
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
761
        if self.module == 'local':
762
            self.user.set_unusable_password()
763
            self.user.save()
764
        return ret
765

    
766
    def __repr__(self):
767
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
768

    
769
    def __unicode__(self):
770
        if self.identifier:
771
            return "%s:%s" % (self.module, self.identifier)
772
        if self.auth_backend:
773
            return "%s:%s" % (self.module, self.auth_backend)
774
        return self.module
775

    
776
    def save(self, *args, **kwargs):
777
        self.info_data = json.dumps(self.info)
778
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
779

    
780

    
781
class Membership(models.Model):
782
    person = models.ForeignKey(AstakosUser)
783
    group = models.ForeignKey(AstakosGroup)
784
    date_requested = models.DateField(default=datetime.now(), blank=True)
785
    date_joined = models.DateField(null=True, db_index=True, blank=True)
786

    
787
    class Meta:
788
        unique_together = ("person", "group")
789

    
790
    def save(self, *args, **kwargs):
791
        if not self.id:
792
            if not self.group.moderation_enabled:
793
                self.date_joined = datetime.now()
794
        super(Membership, self).save(*args, **kwargs)
795

    
796
    @property
797
    def is_approved(self):
798
        if self.date_joined:
799
            return True
800
        return False
801

    
802
    def approve(self):
803
        if self.is_approved:
804
            return
805
        if self.group.max_participants:
806
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
807
            'Maximum participant number has been reached.'
808
        self.date_joined = datetime.now()
809
        self.save()
810
        quota_disturbed.send(sender=self, users=(self.person,))
811

    
812
    def disapprove(self):
813
        self.delete()
814
        quota_disturbed.send(sender=self, users=(self.person,))
815

    
816
class AstakosQuotaManager(models.Manager):
817
    def _update_or_create(self, **kwargs):
818
        assert kwargs, \
819
            'update_or_create() must be passed at least one keyword argument'
820
        obj, created = self.get_or_create(**kwargs)
821
        defaults = kwargs.pop('defaults', {})
822
        if created:
823
            return obj, True, False
824
        else:
825
            try:
826
                params = dict(
827
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
828
                params.update(defaults)
829
                for attr, val in params.items():
830
                    if hasattr(obj, attr):
831
                        setattr(obj, attr, val)
832
                sid = transaction.savepoint()
833
                obj.save(force_update=True)
834
                transaction.savepoint_commit(sid)
835
                return obj, False, True
836
            except IntegrityError, e:
837
                transaction.savepoint_rollback(sid)
838
                try:
839
                    return self.get(**kwargs), False, False
840
                except self.model.DoesNotExist:
841
                    raise e
842

    
843
    update_or_create = _update_or_create
844

    
845
class AstakosGroupQuota(models.Model):
846
    objects = AstakosQuotaManager()
847
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
848
    uplimit = models.BigIntegerField('Up limit', null=True)
849
    resource = models.ForeignKey(Resource)
850
    group = models.ForeignKey(AstakosGroup, blank=True)
851

    
852
    class Meta:
853
        unique_together = ("resource", "group")
854

    
855
class AstakosUserQuota(models.Model):
856
    objects = AstakosQuotaManager()
857
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
858
    uplimit = models.BigIntegerField('Up limit', null=True)
859
    resource = models.ForeignKey(Resource)
860
    user = models.ForeignKey(AstakosUser)
861

    
862
    class Meta:
863
        unique_together = ("resource", "user")
864

    
865

    
866
class ApprovalTerms(models.Model):
867
    """
868
    Model for approval terms
869
    """
870

    
871
    date = models.DateTimeField(
872
        'Issue date', db_index=True, default=datetime.now())
873
    location = models.CharField('Terms location', max_length=255)
874

    
875

    
876
class Invitation(models.Model):
877
    """
878
    Model for registring invitations
879
    """
880
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
881
                                null=True)
882
    realname = models.CharField('Real name', max_length=255)
883
    username = models.CharField('Unique ID', max_length=255, unique=True)
884
    code = models.BigIntegerField('Invitation code', db_index=True)
885
    is_consumed = models.BooleanField('Consumed?', default=False)
886
    created = models.DateTimeField('Creation date', auto_now_add=True)
887
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
888

    
889
    def __init__(self, *args, **kwargs):
890
        super(Invitation, self).__init__(*args, **kwargs)
891
        if not self.id:
892
            self.code = _generate_invitation_code()
893

    
894
    def consume(self):
895
        self.is_consumed = True
896
        self.consumed = datetime.now()
897
        self.save()
898

    
899
    def __unicode__(self):
900
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
901

    
902

    
903
class EmailChangeManager(models.Manager):
904
    @transaction.commit_on_success
905
    def change_email(self, activation_key):
906
        """
907
        Validate an activation key and change the corresponding
908
        ``User`` if valid.
909

910
        If the key is valid and has not expired, return the ``User``
911
        after activating.
912

913
        If the key is not valid or has expired, return ``None``.
914

915
        If the key is valid but the ``User`` is already active,
916
        return ``None``.
917

918
        After successful email change the activation record is deleted.
919

920
        Throws ValueError if there is already
921
        """
922
        try:
923
            email_change = self.model.objects.get(
924
                activation_key=activation_key)
925
            if email_change.activation_key_expired():
926
                email_change.delete()
927
                raise EmailChange.DoesNotExist
928
            # is there an active user with this address?
929
            try:
930
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
931
            except AstakosUser.DoesNotExist:
932
                pass
933
            else:
934
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
935
            # update user
936
            user = AstakosUser.objects.get(pk=email_change.user_id)
937
            user.email = email_change.new_email_address
938
            user.save()
939
            email_change.delete()
940
            return user
941
        except EmailChange.DoesNotExist:
942
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
943

    
944

    
945
class EmailChange(models.Model):
946
    new_email_address = models.EmailField(_(u'new e-mail address'),
947
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
948
    user = models.ForeignKey(
949
        AstakosUser, unique=True, related_name='emailchange_user')
950
    requested_at = models.DateTimeField(default=datetime.now())
951
    activation_key = models.CharField(
952
        max_length=40, unique=True, db_index=True)
953

    
954
    objects = EmailChangeManager()
955

    
956
    def activation_key_expired(self):
957
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
958
        return self.requested_at + expiration_date < datetime.now()
959

    
960

    
961
class AdditionalMail(models.Model):
962
    """
963
    Model for registring invitations
964
    """
965
    owner = models.ForeignKey(AstakosUser)
966
    email = models.EmailField()
967

    
968

    
969
def _generate_invitation_code():
970
    while True:
971
        code = randint(1, 2L ** 63 - 1)
972
        try:
973
            Invitation.objects.get(code=code)
974
            # An invitation with this code already exists, try again
975
        except Invitation.DoesNotExist:
976
            return code
977

    
978

    
979
def get_latest_terms():
980
    try:
981
        term = ApprovalTerms.objects.order_by('-id')[0]
982
        return term
983
    except IndexError:
984
        pass
985
    return None
986

    
987
class PendingThirdPartyUser(models.Model):
988
    """
989
    Model for registring successful third party user authentications
990
    """
991
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
992
    provider = models.CharField('Provider', max_length=255, blank=True)
993
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
994
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
995
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
996
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
997
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
998
    token = models.CharField('Token', max_length=255, null=True, blank=True)
999
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1000

    
1001
    class Meta:
1002
        unique_together = ("provider", "third_party_identifier")
1003

    
1004
    @property
1005
    def realname(self):
1006
        return '%s %s' %(self.first_name, self.last_name)
1007

    
1008
    @realname.setter
1009
    def realname(self, value):
1010
        parts = value.split(' ')
1011
        if len(parts) == 2:
1012
            self.first_name = parts[0]
1013
            self.last_name = parts[1]
1014
        else:
1015
            self.last_name = parts[0]
1016

    
1017
    def save(self, **kwargs):
1018
        if not self.id:
1019
            # set username
1020
            while not self.username:
1021
                username =  uuid.uuid4().hex[:30]
1022
                try:
1023
                    AstakosUser.objects.get(username = username)
1024
                except AstakosUser.DoesNotExist, e:
1025
                    self.username = username
1026
        super(PendingThirdPartyUser, self).save(**kwargs)
1027

    
1028
    def generate_token(self):
1029
        self.password = self.third_party_identifier
1030
        self.last_login = datetime.now()
1031
        self.token = default_token_generator.make_token(self)
1032

    
1033
class SessionCatalog(models.Model):
1034
    session_key = models.CharField(_('session key'), max_length=40)
1035
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1036

    
1037

    
1038
def create_astakos_user(u):
1039
    try:
1040
        AstakosUser.objects.get(user_ptr=u.pk)
1041
    except AstakosUser.DoesNotExist:
1042
        extended_user = AstakosUser(user_ptr_id=u.pk)
1043
        extended_user.__dict__.update(u.__dict__)
1044
        extended_user.save()
1045
        if not extended_user.has_auth_provider('local'):
1046
            extended_user.add_auth_provider('local')
1047
    except BaseException, e:
1048
        logger.exception(e)
1049

    
1050

    
1051
def fix_superusers(sender, **kwargs):
1052
    # Associate superusers with AstakosUser
1053
    admins = User.objects.filter(is_superuser=True)
1054
    for u in admins:
1055
        create_astakos_user(u)
1056

    
1057

    
1058
def user_post_save(sender, instance, created, **kwargs):
1059
    if not created:
1060
        return
1061
    create_astakos_user(instance)
1062

    
1063

    
1064
def set_default_group(user):
1065
    try:
1066
        default = AstakosGroup.objects.get(name='default')
1067
        Membership(
1068
            group=default, person=user, date_joined=datetime.now()).save()
1069
    except AstakosGroup.DoesNotExist, e:
1070
        logger.exception(e)
1071

    
1072

    
1073
def astakosuser_pre_save(sender, instance, **kwargs):
1074
    instance.aquarium_report = False
1075
    instance.new = False
1076
    try:
1077
        db_instance = AstakosUser.objects.get(id=instance.id)
1078
    except AstakosUser.DoesNotExist:
1079
        # create event
1080
        instance.aquarium_report = True
1081
        instance.new = True
1082
    else:
1083
        get = AstakosUser.__getattribute__
1084
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1085
                   BILLING_FIELDS)
1086
        instance.aquarium_report = True if l else False
1087

    
1088

    
1089
def astakosuser_post_save(sender, instance, created, **kwargs):
1090
    if instance.aquarium_report:
1091
        report_user_event(instance, create=instance.new)
1092
    if not created:
1093
        return
1094
    set_default_group(instance)
1095
    # TODO handle socket.error & IOError
1096
    register_users((instance,))
1097

    
1098

    
1099
def resource_post_save(sender, instance, created, **kwargs):
1100
    if not created:
1101
        return
1102
    register_resources((instance,))
1103

    
1104

    
1105
def send_quota_disturbed(sender, instance, **kwargs):
1106
    users = []
1107
    extend = users.extend
1108
    if sender == Membership:
1109
        if not instance.group.is_enabled:
1110
            return
1111
        extend([instance.person])
1112
    elif sender == AstakosUserQuota:
1113
        extend([instance.user])
1114
    elif sender == AstakosGroupQuota:
1115
        if not instance.group.is_enabled:
1116
            return
1117
        extend(instance.group.astakosuser_set.all())
1118
    elif sender == AstakosGroup:
1119
        if not instance.is_enabled:
1120
            return
1121
    quota_disturbed.send(sender=sender, users=users)
1122

    
1123

    
1124
def on_quota_disturbed(sender, users, **kwargs):
1125
#     print '>>>', locals()
1126
    if not users:
1127
        return
1128
    send_quota(users)
1129

    
1130
def renew_token(sender, instance, **kwargs):
1131
    if not instance.auth_token:
1132
        instance.renew_token()
1133

    
1134
post_syncdb.connect(fix_superusers)
1135
post_save.connect(user_post_save, sender=User)
1136
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1137
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1138
post_save.connect(resource_post_save, sender=Resource)
1139

    
1140
quota_disturbed = Signal(providing_args=["users"])
1141
quota_disturbed.connect(on_quota_disturbed)
1142

    
1143
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1144
post_delete.connect(send_quota_disturbed, sender=Membership)
1145
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1146
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1147
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1148
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1149

    
1150
pre_save.connect(renew_token, sender=AstakosUser)
1151
pre_save.connect(renew_token, sender=Service)