Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 34a76cdb

History | View | Annotate | Download (42.3 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
    def get_by_identifier(self, email_or_username, **kwargs):
328
        try:
329
            return self.get(email__iexact=email_or_username, **kwargs)
330
        except AstakosUser.DoesNotExist:
331
            return self.get(username__iexact=email_or_username, **kwargs)
332

    
333
    def user_exists(self, email_or_username, **kwargs):
334
        qemail = Q(email__iexact=email_or_username)
335
        qusername = Q(username__iexact=email_or_username)
336
        return self.filter(qemail | qusername).exists()
337

    
338

    
339
class AstakosUser(User):
340
    """
341
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
342
    """
343
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
344
                                   null=True)
345

    
346
    # DEPRECATED FIELDS: provider, third_party_identifier moved in
347
    #                    AstakosUserProvider model.
348
    provider = models.CharField('Provider', max_length=255, blank=True,
349
                                null=True)
350
    # ex. screen_name for twitter, eppn for shibboleth
351
    third_party_identifier = models.CharField('Third-party identifier',
352
                                              max_length=255, null=True,
353
                                              blank=True)
354

    
355

    
356
    #for invitations
357
    user_level = DEFAULT_USER_LEVEL
358
    level = models.IntegerField('Inviter level', default=user_level)
359
    invitations = models.IntegerField(
360
        'Invitations left', default=INVITATIONS_PER_LEVEL.get(user_level, 0))
361

    
362
    auth_token = models.CharField('Authentication Token', max_length=32,
363
                                  null=True, blank=True)
364
    auth_token_created = models.DateTimeField('Token creation date', null=True)
365
    auth_token_expires = models.DateTimeField(
366
        'Token expiration date', null=True)
367

    
368
    updated = models.DateTimeField('Update date')
369
    is_verified = models.BooleanField('Is verified?', default=False)
370

    
371
    email_verified = models.BooleanField('Email verified?', default=False)
372

    
373
    has_credits = models.BooleanField('Has credits?', default=False)
374
    has_signed_terms = models.BooleanField(
375
        'I agree with the terms', default=False)
376
    date_signed_terms = models.DateTimeField(
377
        'Signed terms date', null=True, blank=True)
378

    
379
    activation_sent = models.DateTimeField(
380
        'Activation sent data', null=True, blank=True)
381

    
382
    policy = models.ManyToManyField(
383
        Resource, null=True, through='AstakosUserQuota')
384

    
385
    astakos_groups = models.ManyToManyField(
386
        AstakosGroup, verbose_name=_('agroups'), blank=True,
387
        help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
388
        through='Membership')
389

    
390
    __has_signed_terms = False
391
    disturbed_quota = models.BooleanField('Needs quotaholder syncing',
392
                                           default=False, db_index=True)
393

    
394
    objects = AstakosUserManager()
395

    
396
    owner = models.ManyToManyField(
397
        AstakosGroup, related_name='owner', null=True)
398

    
399
    def __init__(self, *args, **kwargs):
400
        super(AstakosUser, self).__init__(*args, **kwargs)
401
        self.__has_signed_terms = self.has_signed_terms
402
        if not self.id:
403
            self.is_active = False
404

    
405
    @property
406
    def realname(self):
407
        return '%s %s' % (self.first_name, self.last_name)
408

    
409
    @realname.setter
410
    def realname(self, value):
411
        parts = value.split(' ')
412
        if len(parts) == 2:
413
            self.first_name = parts[0]
414
            self.last_name = parts[1]
415
        else:
416
            self.last_name = parts[0]
417

    
418
    def add_permission(self, pname):
419
        if self.has_perm(pname):
420
            return
421
        p, created = Permission.objects.get_or_create(codename=pname,
422
                                                      name=pname.capitalize(),
423
                                                      content_type=content_type)
424
        self.user_permissions.add(p)
425

    
426
    def remove_permission(self, pname):
427
        if self.has_perm(pname):
428
            return
429
        p = Permission.objects.get(codename=pname,
430
                                   content_type=content_type)
431
        self.user_permissions.remove(p)
432

    
433
    @property
434
    def invitation(self):
435
        try:
436
            return Invitation.objects.get(username=self.email)
437
        except Invitation.DoesNotExist:
438
            return None
439

    
440
    def invite(self, email, realname):
441
        inv = Invitation(inviter=self, username=email, realname=realname)
442
        inv.save()
443
        send_invitation(inv)
444
        self.invitations = max(0, self.invitations - 1)
445
        self.save()
446

    
447
    @property
448
    def quota(self):
449
        """Returns a dict with the sum of quota limits per resource"""
450
        d = defaultdict(int)
451
        for q in self.policies:
452
            d[q.resource] += q.uplimit or inf
453
        for m in self.extended_groups:
454
            if not m.is_approved:
455
                continue
456
            g = m.group
457
            if not g.is_enabled:
458
                continue
459
            for r, uplimit in g.quota.iteritems():
460
                d[r] += uplimit or inf
461
        # TODO set default for remaining
462
        return d
463

    
464
    @property
465
    def policies(self):
466
        return self.astakosuserquota_set.select_related().all()
467

    
468
    @policies.setter
469
    def policies(self, policies):
470
        for p in policies:
471
            service = policies.get('service', None)
472
            resource = policies.get('resource', None)
473
            uplimit = policies.get('uplimit', 0)
474
            update = policies.get('update', True)
475
            self.add_policy(service, resource, uplimit, update)
476

    
477
    def add_policy(self, service, resource, uplimit, update=True):
478
        """Raises ObjectDoesNotExist, IntegrityError"""
479
        resource = Resource.objects.get(service__name=service, name=resource)
480
        if update:
481
            AstakosUserQuota.objects.update_or_create(user=self,
482
                                                      resource=resource,
483
                                                      defaults={'uplimit': uplimit})
484
        else:
485
            q = self.astakosuserquota_set
486
            q.create(resource=resource, uplimit=uplimit)
487

    
488
    def remove_policy(self, service, resource):
489
        """Raises ObjectDoesNotExist, IntegrityError"""
490
        resource = Resource.objects.get(service__name=service, name=resource)
491
        q = self.policies.get(resource=resource).delete()
492

    
493
    @property
494
    def extended_groups(self):
495
        return self.membership_set.select_related().all()
496

    
497
    @extended_groups.setter
498
    def extended_groups(self, groups):
499
        #TODO exceptions
500
        for name in (groups or ()):
501
            group = AstakosGroup.objects.get(name=name)
502
            self.membership_set.create(group=group)
503

    
504
    def save(self, update_timestamps=True, **kwargs):
505
        if update_timestamps:
506
            if not self.id:
507
                self.date_joined = datetime.now()
508
            self.updated = datetime.now()
509

    
510
        # update date_signed_terms if necessary
511
        if self.__has_signed_terms != self.has_signed_terms:
512
            self.date_signed_terms = datetime.now()
513

    
514
        if not self.id:
515
            # set username
516
            self.username = self.email.lower()
517

    
518
        self.validate_unique_email_isactive()
519

    
520
        super(AstakosUser, self).save(**kwargs)
521

    
522
    def renew_token(self, flush_sessions=False, current_key=None):
523
        md5 = hashlib.md5()
524
        md5.update(settings.SECRET_KEY)
525
        md5.update(self.username)
526
        md5.update(self.realname.encode('ascii', 'ignore'))
527
        md5.update(asctime())
528

    
529
        self.auth_token = b64encode(md5.digest())
530
        self.auth_token_created = datetime.now()
531
        self.auth_token_expires = self.auth_token_created + \
532
                                  timedelta(hours=AUTH_TOKEN_DURATION)
533
        if flush_sessions:
534
            self.flush_sessions(current_key)
535
        msg = 'Token renewed for %s' % self.email
536
        logger.log(LOGGING_LEVEL, msg)
537

    
538
    def flush_sessions(self, current_key=None):
539
        q = self.sessions
540
        if current_key:
541
            q = q.exclude(session_key=current_key)
542

    
543
        keys = q.values_list('session_key', flat=True)
544
        if keys:
545
            msg = 'Flushing sessions: %s' % ','.join(keys)
546
            logger.log(LOGGING_LEVEL, msg, [])
547
        engine = import_module(settings.SESSION_ENGINE)
548
        for k in keys:
549
            s = engine.SessionStore(k)
550
            s.flush()
551

    
552
    def __unicode__(self):
553
        return '%s (%s)' % (self.realname, self.email)
554

    
555
    def conflicting_email(self):
556
        q = AstakosUser.objects.exclude(username=self.username)
557
        q = q.filter(email__iexact=self.email)
558
        if q.count() != 0:
559
            return True
560
        return False
561

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

    
573
    def email_change_is_pending(self):
574
        return self.emailchanges.count() > 0
575

    
576
    @property
577
    def signed_terms(self):
578
        term = get_latest_terms()
579
        if not term:
580
            return True
581
        if not self.has_signed_terms:
582
            return False
583
        if not self.date_signed_terms:
584
            return False
585
        if self.date_signed_terms < term.date:
586
            self.has_signed_terms = False
587
            self.date_signed_terms = None
588
            self.save()
589
            return False
590
        return True
591

    
592
    def set_invitations_level(self):
593
        """
594
        Update user invitation level
595
        """
596
        level = self.invitation.inviter.level + 1
597
        self.level = level
598
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
599

    
600
    def can_login_with_auth_provider(self, provider):
601
        if not self.has_auth_provider(provider):
602
            return False
603
        else:
604
            return auth_providers.get_provider(provider).is_available_for_login()
605

    
606
    def can_add_auth_provider(self, provider, **kwargs):
607
        provider_settings = auth_providers.get_provider(provider)
608
        if not provider_settings.is_available_for_login():
609
            return False
610

    
611
        if self.has_auth_provider(provider) and \
612
           provider_settings.one_per_user:
613
            return False
614

    
615
        if 'provider_info' in kwargs:
616
            kwargs.pop('provider_info')
617

    
618
        if 'identifier' in kwargs:
619
            try:
620
                # provider with specified params already exist
621
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
622
                                                                   **kwargs)
623
            except AstakosUser.DoesNotExist:
624
                return True
625
            else:
626
                return False
627

    
628
        return True
629

    
630
    def can_remove_auth_provider(self, provider):
631
        if len(self.get_active_auth_providers()) <= 1:
632
            return False
633
        return True
634

    
635
    def can_change_password(self):
636
        return self.has_auth_provider('local', auth_backend='astakos')
637

    
638
    def has_auth_provider(self, provider, **kwargs):
639
        return bool(self.auth_providers.filter(module=provider,
640
                                               **kwargs).count())
641

    
642
    def add_auth_provider(self, provider, **kwargs):
643
        info_data = ''
644
        if 'provider_info' in kwargs:
645
            info_data = kwargs.pop('provider_info')
646
            if isinstance(info_data, dict):
647
                info_data = json.dumps(info_data)
648

    
649
        if self.can_add_auth_provider(provider, **kwargs):
650
            self.auth_providers.create(module=provider, active=True,
651
                                       info_data=info_data,
652
                                       **kwargs)
653
        else:
654
            raise Exception('Cannot add provider')
655

    
656
    def add_pending_auth_provider(self, pending):
657
        """
658
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
659
        the current user.
660
        """
661
        if not isinstance(pending, PendingThirdPartyUser):
662
            pending = PendingThirdPartyUser.objects.get(token=pending)
663

    
664
        provider = self.add_auth_provider(pending.provider,
665
                               identifier=pending.third_party_identifier,
666
                                affiliation=pending.affiliation,
667
                                          provider_info=pending.info)
668

    
669
        if email_re.match(pending.email or '') and pending.email != self.email:
670
            self.additionalmail_set.get_or_create(email=pending.email)
671

    
672
        pending.delete()
673
        return provider
674

    
675
    def remove_auth_provider(self, provider, **kwargs):
676
        self.auth_providers.get(module=provider, **kwargs).delete()
677

    
678
    # user urls
679
    def get_resend_activation_url(self):
680
        return reverse('send_activation', kwargs={'user_id': self.pk})
681

    
682
    def get_provider_remove_url(self, module, **kwargs):
683
        return reverse('remove_auth_provider', kwargs={
684
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
685

    
686
    def get_activation_url(self, nxt=False):
687
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
688
                                 quote(self.auth_token))
689
        if nxt:
690
            url += "&next=%s" % quote(nxt)
691
        return url
692

    
693
    def get_password_reset_url(self, token_generator=default_token_generator):
694
        return reverse('django.contrib.auth.views.password_reset_confirm',
695
                          kwargs={'uidb36':int_to_base36(self.id),
696
                                  'token':token_generator.make_token(self)})
697

    
698
    def get_auth_providers(self):
699
        return self.auth_providers.all()
700

    
701
    def get_available_auth_providers(self):
702
        """
703
        Returns a list of providers available for user to connect to.
704
        """
705
        providers = []
706
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
707
            if self.can_add_auth_provider(module):
708
                providers.append(provider_settings(self))
709

    
710
        return providers
711

    
712
    def get_active_auth_providers(self):
713
        providers = []
714
        for provider in self.auth_providers.active():
715
            if auth_providers.get_provider(provider.module).is_available_for_login():
716
                providers.append(provider)
717
        return providers
718

    
719
    @property
720
    def auth_providers_display(self):
721
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
722

    
723
    def get_inactive_message(self):
724
        msg_extra = ''
725
        message = ''
726
        if self.activation_sent:
727
            if self.email_verified:
728
                message = _(astakos_messages.ACCOUNT_INACTIVE)
729
            else:
730
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
731
                if astakos_settings.MODERATION_ENABLED:
732
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
733
                else:
734
                    url = self.get_resend_activation_url()
735
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
736
                                u' ' + \
737
                                _('<a href="%s">%s?</a>') % (url,
738
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
739
        else:
740
            if astakos_settings.MODERATION_ENABLED:
741
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
742
            else:
743
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
744
                url = self.get_resend_activation_url()
745
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
746
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
747

    
748
        return mark_safe(message + u' '+ msg_extra)
749

    
750

    
751
class AstakosUserAuthProviderManager(models.Manager):
752

    
753
    def active(self):
754
        return self.filter(active=True)
755

    
756

    
757
class AstakosUserAuthProvider(models.Model):
758
    """
759
    Available user authentication methods.
760
    """
761
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
762
                                   null=True, default=None)
763
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
764
    module = models.CharField('Provider', max_length=255, blank=False,
765
                                default='local')
766
    identifier = models.CharField('Third-party identifier',
767
                                              max_length=255, null=True,
768
                                              blank=True)
769
    active = models.BooleanField(default=True)
770
    auth_backend = models.CharField('Backend', max_length=255, blank=False,
771
                                   default='astakos')
772
    info_data = models.TextField(default="", null=True, blank=True)
773
    created = models.DateTimeField('Creation date', auto_now_add=True)
774

    
775
    objects = AstakosUserAuthProviderManager()
776

    
777
    class Meta:
778
        unique_together = (('identifier', 'module', 'user'), )
779
        ordering = ('module', 'created')
780

    
781
    def __init__(self, *args, **kwargs):
782
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
783
        try:
784
            self.info = json.loads(self.info_data)
785
            if not self.info:
786
                self.info = {}
787
        except Exception, e:
788
            self.info = {}
789

    
790
        for key,value in self.info.iteritems():
791
            setattr(self, 'info_%s' % key, value)
792

    
793

    
794
    @property
795
    def settings(self):
796
        return auth_providers.get_provider(self.module)
797

    
798
    @property
799
    def details_display(self):
800
        try:
801
          return self.settings.get_details_tpl_display % self.__dict__
802
        except:
803
          return ''
804

    
805
    @property
806
    def title_display(self):
807
        title_tpl = self.settings.get_title_display
808
        try:
809
            if self.settings.get_user_title_display:
810
                title_tpl = self.settings.get_user_title_display
811
        except Exception, e:
812
            pass
813
        try:
814
          return title_tpl % self.__dict__
815
        except:
816
          return self.settings.get_title_display % self.__dict__
817

    
818
    def can_remove(self):
819
        return self.user.can_remove_auth_provider(self.module)
820

    
821
    def delete(self, *args, **kwargs):
822
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
823
        if self.module == 'local':
824
            self.user.set_unusable_password()
825
            self.user.save()
826
        return ret
827

    
828
    def __repr__(self):
829
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
830

    
831
    def __unicode__(self):
832
        if self.identifier:
833
            return "%s:%s" % (self.module, self.identifier)
834
        if self.auth_backend:
835
            return "%s:%s" % (self.module, self.auth_backend)
836
        return self.module
837

    
838
    def save(self, *args, **kwargs):
839
        self.info_data = json.dumps(self.info)
840
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
841

    
842

    
843
class Membership(models.Model):
844
    person = models.ForeignKey(AstakosUser)
845
    group = models.ForeignKey(AstakosGroup)
846
    date_requested = models.DateField(default=datetime.now(), blank=True)
847
    date_joined = models.DateField(null=True, db_index=True, blank=True)
848

    
849
    class Meta:
850
        unique_together = ("person", "group")
851

    
852
    def save(self, *args, **kwargs):
853
        if not self.id:
854
            if not self.group.moderation_enabled:
855
                self.date_joined = datetime.now()
856
        super(Membership, self).save(*args, **kwargs)
857

    
858
    @property
859
    def is_approved(self):
860
        if self.date_joined:
861
            return True
862
        return False
863

    
864
    def approve(self):
865
        if self.is_approved:
866
            return
867
        if self.group.max_participants:
868
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
869
            'Maximum participant number has been reached.'
870
        self.date_joined = datetime.now()
871
        self.save()
872
        quota_disturbed.send(sender=self, users=(self.person,))
873

    
874
    def disapprove(self):
875
        self.delete()
876
        quota_disturbed.send(sender=self, users=(self.person,))
877

    
878
class AstakosQuotaManager(models.Manager):
879
    def _update_or_create(self, **kwargs):
880
        assert kwargs, \
881
            'update_or_create() must be passed at least one keyword argument'
882
        obj, created = self.get_or_create(**kwargs)
883
        defaults = kwargs.pop('defaults', {})
884
        if created:
885
            return obj, True, False
886
        else:
887
            try:
888
                params = dict(
889
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
890
                params.update(defaults)
891
                for attr, val in params.items():
892
                    if hasattr(obj, attr):
893
                        setattr(obj, attr, val)
894
                sid = transaction.savepoint()
895
                obj.save(force_update=True)
896
                transaction.savepoint_commit(sid)
897
                return obj, False, True
898
            except IntegrityError, e:
899
                transaction.savepoint_rollback(sid)
900
                try:
901
                    return self.get(**kwargs), False, False
902
                except self.model.DoesNotExist:
903
                    raise e
904

    
905
    update_or_create = _update_or_create
906

    
907
class AstakosGroupQuota(models.Model):
908
    objects = AstakosQuotaManager()
909
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
910
    uplimit = models.BigIntegerField('Up limit', null=True)
911
    resource = models.ForeignKey(Resource)
912
    group = models.ForeignKey(AstakosGroup, blank=True)
913

    
914
    class Meta:
915
        unique_together = ("resource", "group")
916

    
917
class AstakosUserQuota(models.Model):
918
    objects = AstakosQuotaManager()
919
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
920
    uplimit = models.BigIntegerField('Up limit', null=True)
921
    resource = models.ForeignKey(Resource)
922
    user = models.ForeignKey(AstakosUser)
923

    
924
    class Meta:
925
        unique_together = ("resource", "user")
926

    
927

    
928
class ApprovalTerms(models.Model):
929
    """
930
    Model for approval terms
931
    """
932

    
933
    date = models.DateTimeField(
934
        'Issue date', db_index=True, default=datetime.now())
935
    location = models.CharField('Terms location', max_length=255)
936

    
937

    
938
class Invitation(models.Model):
939
    """
940
    Model for registring invitations
941
    """
942
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
943
                                null=True)
944
    realname = models.CharField('Real name', max_length=255)
945
    username = models.CharField('Unique ID', max_length=255, unique=True)
946
    code = models.BigIntegerField('Invitation code', db_index=True)
947
    is_consumed = models.BooleanField('Consumed?', default=False)
948
    created = models.DateTimeField('Creation date', auto_now_add=True)
949
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
950

    
951
    def __init__(self, *args, **kwargs):
952
        super(Invitation, self).__init__(*args, **kwargs)
953
        if not self.id:
954
            self.code = _generate_invitation_code()
955

    
956
    def consume(self):
957
        self.is_consumed = True
958
        self.consumed = datetime.now()
959
        self.save()
960

    
961
    def __unicode__(self):
962
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
963

    
964

    
965
class EmailChangeManager(models.Manager):
966

    
967
    @transaction.commit_on_success
968
    def change_email(self, activation_key):
969
        """
970
        Validate an activation key and change the corresponding
971
        ``User`` if valid.
972

973
        If the key is valid and has not expired, return the ``User``
974
        after activating.
975

976
        If the key is not valid or has expired, return ``None``.
977

978
        If the key is valid but the ``User`` is already active,
979
        return ``None``.
980

981
        After successful email change the activation record is deleted.
982

983
        Throws ValueError if there is already
984
        """
985
        try:
986
            email_change = self.model.objects.get(
987
                activation_key=activation_key)
988
            if email_change.activation_key_expired():
989
                email_change.delete()
990
                raise EmailChange.DoesNotExist
991
            # is there an active user with this address?
992
            try:
993
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
994
            except AstakosUser.DoesNotExist:
995
                pass
996
            else:
997
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
998
            # update user
999
            user = AstakosUser.objects.get(pk=email_change.user_id)
1000
            old_email = user.email
1001
            user.email = email_change.new_email_address
1002
            user.save()
1003
            email_change.delete()
1004
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1005
                                                          user.email)
1006
            logger.log(LOGGING_LEVEL, msg)
1007
            return user
1008
        except EmailChange.DoesNotExist:
1009
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
1010

    
1011

    
1012
class EmailChange(models.Model):
1013
    new_email_address = models.EmailField(_(u'new e-mail address'),
1014
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
1015
    user = models.ForeignKey(
1016
        AstakosUser, unique=True, related_name='emailchanges')
1017
    requested_at = models.DateTimeField(default=datetime.now())
1018
    activation_key = models.CharField(
1019
        max_length=40, unique=True, db_index=True)
1020

    
1021
    objects = EmailChangeManager()
1022

    
1023
    def get_url(self):
1024
        return reverse('email_change_confirm',
1025
                      kwargs={'activation_key': self.activation_key})
1026

    
1027
    def activation_key_expired(self):
1028
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1029
        return self.requested_at + expiration_date < datetime.now()
1030

    
1031

    
1032
class AdditionalMail(models.Model):
1033
    """
1034
    Model for registring invitations
1035
    """
1036
    owner = models.ForeignKey(AstakosUser)
1037
    email = models.EmailField()
1038

    
1039

    
1040
def _generate_invitation_code():
1041
    while True:
1042
        code = randint(1, 2L ** 63 - 1)
1043
        try:
1044
            Invitation.objects.get(code=code)
1045
            # An invitation with this code already exists, try again
1046
        except Invitation.DoesNotExist:
1047
            return code
1048

    
1049

    
1050
def get_latest_terms():
1051
    try:
1052
        term = ApprovalTerms.objects.order_by('-id')[0]
1053
        return term
1054
    except IndexError:
1055
        pass
1056
    return None
1057

    
1058
class PendingThirdPartyUser(models.Model):
1059
    """
1060
    Model for registring successful third party user authentications
1061
    """
1062
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
1063
    provider = models.CharField('Provider', max_length=255, blank=True)
1064
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1065
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
1066
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
1067
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
1068
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1069
    token = models.CharField('Token', max_length=255, null=True, blank=True)
1070
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1071
    info = models.TextField(default="", null=True, blank=True)
1072

    
1073
    class Meta:
1074
        unique_together = ("provider", "third_party_identifier")
1075

    
1076
    def get_user_instance(self):
1077
        d = self.__dict__
1078
        d.pop('_state', None)
1079
        d.pop('id', None)
1080
        d.pop('token', None)
1081
        d.pop('created', None)
1082
        d.pop('info', None)
1083
        user = AstakosUser(**d)
1084

    
1085
        return user
1086

    
1087
    @property
1088
    def realname(self):
1089
        return '%s %s' %(self.first_name, self.last_name)
1090

    
1091
    @realname.setter
1092
    def realname(self, value):
1093
        parts = value.split(' ')
1094
        if len(parts) == 2:
1095
            self.first_name = parts[0]
1096
            self.last_name = parts[1]
1097
        else:
1098
            self.last_name = parts[0]
1099

    
1100
    def save(self, **kwargs):
1101
        if not self.id:
1102
            # set username
1103
            while not self.username:
1104
                username =  uuid.uuid4().hex[:30]
1105
                try:
1106
                    AstakosUser.objects.get(username = username)
1107
                except AstakosUser.DoesNotExist, e:
1108
                    self.username = username
1109
        super(PendingThirdPartyUser, self).save(**kwargs)
1110

    
1111
    def generate_token(self):
1112
        self.password = self.third_party_identifier
1113
        self.last_login = datetime.now()
1114
        self.token = default_token_generator.make_token(self)
1115

    
1116
class SessionCatalog(models.Model):
1117
    session_key = models.CharField(_('session key'), max_length=40)
1118
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1119

    
1120

    
1121
def create_astakos_user(u):
1122
    try:
1123
        AstakosUser.objects.get(user_ptr=u.pk)
1124
    except AstakosUser.DoesNotExist:
1125
        extended_user = AstakosUser(user_ptr_id=u.pk)
1126
        extended_user.__dict__.update(u.__dict__)
1127
        extended_user.save()
1128
        if not extended_user.has_auth_provider('local'):
1129
            extended_user.add_auth_provider('local')
1130
    except BaseException, e:
1131
        logger.exception(e)
1132

    
1133

    
1134
def fix_superusers(sender, **kwargs):
1135
    # Associate superusers with AstakosUser
1136
    admins = User.objects.filter(is_superuser=True)
1137
    for u in admins:
1138
        create_astakos_user(u)
1139

    
1140

    
1141
def user_post_save(sender, instance, created, **kwargs):
1142
    if not created:
1143
        return
1144
    create_astakos_user(instance)
1145

    
1146

    
1147
def set_default_group(user):
1148
    try:
1149
        default = AstakosGroup.objects.get(name='default')
1150
        Membership(
1151
            group=default, person=user, date_joined=datetime.now()).save()
1152
    except AstakosGroup.DoesNotExist, e:
1153
        logger.exception(e)
1154

    
1155

    
1156
def astakosuser_pre_save(sender, instance, **kwargs):
1157
    instance.aquarium_report = False
1158
    instance.new = False
1159
    try:
1160
        db_instance = AstakosUser.objects.get(id=instance.id)
1161
    except AstakosUser.DoesNotExist:
1162
        # create event
1163
        instance.aquarium_report = True
1164
        instance.new = True
1165
    else:
1166
        get = AstakosUser.__getattribute__
1167
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1168
                   BILLING_FIELDS)
1169
        instance.aquarium_report = True if l else False
1170

    
1171

    
1172
def astakosuser_post_save(sender, instance, created, **kwargs):
1173
    if instance.aquarium_report:
1174
        report_user_event(instance, create=instance.new)
1175
    if not created:
1176
        return
1177
    set_default_group(instance)
1178
    # TODO handle socket.error & IOError
1179
    register_users((instance,))
1180

    
1181

    
1182
def resource_post_save(sender, instance, created, **kwargs):
1183
    if not created:
1184
        return
1185
    register_resources((instance,))
1186

    
1187

    
1188
def send_quota_disturbed(sender, instance, **kwargs):
1189
    users = []
1190
    extend = users.extend
1191
    if sender == Membership:
1192
        if not instance.group.is_enabled:
1193
            return
1194
        extend([instance.person])
1195
    elif sender == AstakosUserQuota:
1196
        extend([instance.user])
1197
    elif sender == AstakosGroupQuota:
1198
        if not instance.group.is_enabled:
1199
            return
1200
        extend(instance.group.astakosuser_set.all())
1201
    elif sender == AstakosGroup:
1202
        if not instance.is_enabled:
1203
            return
1204
    quota_disturbed.send(sender=sender, users=users)
1205

    
1206

    
1207
def on_quota_disturbed(sender, users, **kwargs):
1208
#     print '>>>', locals()
1209
    if not users:
1210
        return
1211
    send_quota(users)
1212

    
1213
def renew_token(sender, instance, **kwargs):
1214
    if not instance.auth_token:
1215
        instance.renew_token()
1216

    
1217
post_syncdb.connect(fix_superusers)
1218
post_save.connect(user_post_save, sender=User)
1219
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1220
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1221
post_save.connect(resource_post_save, sender=Resource)
1222

    
1223
quota_disturbed = Signal(providing_args=["users"])
1224
quota_disturbed.connect(on_quota_disturbed)
1225

    
1226
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1227
post_delete.connect(send_quota_disturbed, sender=Membership)
1228
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1229
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1230
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1231
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1232

    
1233
pre_save.connect(renew_token, sender=AstakosUser)
1234
pre_save.connect(renew_token, sender=Service)