Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 222d8e52

History | View | Annotate | Download (43.2 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
    uuid = models.CharField(max_length=255, null=True, blank=False, unique=True)
386

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

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

    
396
    objects = AstakosUserManager()
397

    
398

    
399
    owner = models.ManyToManyField(
400
        AstakosGroup, related_name='owner', null=True)
401

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

    
408
    @property
409
    def realname(self):
410
        return '%s %s' % (self.first_name, self.last_name)
411

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

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

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

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

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

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

    
467
    @property
468
    def policies(self):
469
        return self.astakosuserquota_set.select_related().all()
470

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

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

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

    
496
    def update_uuid(self):
497
        while not self.uuid:
498
            uuid_val =  str(uuid.uuid4())
499
            try:
500
                AstakosUser.objects.get(uuid=uuid_val)
501
            except AstakosUser.DoesNotExist, e:
502
                self.uuid = uuid_val
503
        return self.uuid
504

    
505
    @property
506
    def extended_groups(self):
507
        return self.membership_set.select_related().all()
508

    
509
    @extended_groups.setter
510
    def extended_groups(self, groups):
511
        #TODO exceptions
512
        for name in (groups or ()):
513
            group = AstakosGroup.objects.get(name=name)
514
            self.membership_set.create(group=group)
515

    
516
    def save(self, update_timestamps=True, **kwargs):
517
        if update_timestamps:
518
            if not self.id:
519
                self.date_joined = datetime.now()
520
            self.updated = datetime.now()
521

    
522
        # update date_signed_terms if necessary
523
        if self.__has_signed_terms != self.has_signed_terms:
524
            self.date_signed_terms = datetime.now()
525

    
526
        self.update_uuid()
527

    
528
        if self.username != self.email.lower():
529
            # set username
530
            self.username = self.email.lower()
531

    
532
        self.validate_unique_email_isactive()
533

    
534
        super(AstakosUser, self).save(**kwargs)
535

    
536
    def renew_token(self, flush_sessions=False, current_key=None):
537
        md5 = hashlib.md5()
538
        md5.update(settings.SECRET_KEY)
539
        md5.update(self.username)
540
        md5.update(self.realname.encode('ascii', 'ignore'))
541
        md5.update(asctime())
542

    
543
        self.auth_token = b64encode(md5.digest())
544
        self.auth_token_created = datetime.now()
545
        self.auth_token_expires = self.auth_token_created + \
546
                                  timedelta(hours=AUTH_TOKEN_DURATION)
547
        if flush_sessions:
548
            self.flush_sessions(current_key)
549
        msg = 'Token renewed for %s' % self.email
550
        logger.log(LOGGING_LEVEL, msg)
551

    
552
    def flush_sessions(self, current_key=None):
553
        q = self.sessions
554
        if current_key:
555
            q = q.exclude(session_key=current_key)
556

    
557
        keys = q.values_list('session_key', flat=True)
558
        if keys:
559
            msg = 'Flushing sessions: %s' % ','.join(keys)
560
            logger.log(LOGGING_LEVEL, msg, [])
561
        engine = import_module(settings.SESSION_ENGINE)
562
        for k in keys:
563
            s = engine.SessionStore(k)
564
            s.flush()
565

    
566
    def __unicode__(self):
567
        return '%s (%s)' % (self.realname, self.email)
568

    
569
    def conflicting_email(self):
570
        q = AstakosUser.objects.exclude(username=self.username)
571
        q = q.filter(email__iexact=self.email)
572
        if q.count() != 0:
573
            return True
574
        return False
575

    
576
    def validate_unique_email_isactive(self):
577
        """
578
        Implements a unique_together constraint for email and is_active fields.
579
        """
580
        q = AstakosUser.objects.all()
581
        q = q.filter(email = self.email)
582
        if self.id:
583
            q = q.filter(~Q(id = self.id))
584
        if q.count() != 0:
585
            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
586

    
587
    def email_change_is_pending(self):
588
        return self.emailchanges.count() > 0
589

    
590
    @property
591
    def signed_terms(self):
592
        term = get_latest_terms()
593
        if not term:
594
            return True
595
        if not self.has_signed_terms:
596
            return False
597
        if not self.date_signed_terms:
598
            return False
599
        if self.date_signed_terms < term.date:
600
            self.has_signed_terms = False
601
            self.date_signed_terms = None
602
            self.save()
603
            return False
604
        return True
605

    
606
    def set_invitations_level(self):
607
        """
608
        Update user invitation level
609
        """
610
        level = self.invitation.inviter.level + 1
611
        self.level = level
612
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
613

    
614
    def can_login_with_auth_provider(self, provider):
615
        if not self.has_auth_provider(provider):
616
            return False
617
        else:
618
            return auth_providers.get_provider(provider).is_available_for_login()
619

    
620
    def can_add_auth_provider(self, provider, **kwargs):
621
        provider_settings = auth_providers.get_provider(provider)
622

    
623
        if not provider_settings.is_available_for_add():
624
            return False
625

    
626
        if self.has_auth_provider(provider) and \
627
           provider_settings.one_per_user:
628
            return False
629

    
630
        if 'provider_info' in kwargs:
631
            kwargs.pop('provider_info')
632

    
633
        if 'identifier' in kwargs:
634
            try:
635
                # provider with specified params already exist
636
                existing_user = AstakosUser.objects.get_auth_provider_user(provider,
637
                                                                   **kwargs)
638
            except AstakosUser.DoesNotExist:
639
                return True
640
            else:
641
                return False
642

    
643
        return True
644

    
645
    def can_remove_auth_provider(self, module):
646
        provider = auth_providers.get_provider(module)
647
        existing = self.get_active_auth_providers()
648
        existing_for_provider = self.get_active_auth_providers(module=module)
649

    
650
        if len(existing) <= 1:
651
            return False
652

    
653
        if len(existing_for_provider) == 1 and provider.is_required():
654
            return False
655

    
656
        return True
657

    
658
    def can_change_password(self):
659
        return self.has_auth_provider('local', auth_backend='astakos')
660

    
661
    def has_required_auth_providers(self):
662
        required = auth_providers.REQUIRED_PROVIDERS
663
        for provider in required:
664
            if not self.has_auth_provider(provider):
665
                return False
666
        return True
667

    
668
    def has_auth_provider(self, provider, **kwargs):
669
        return bool(self.auth_providers.filter(module=provider,
670
                                               **kwargs).count())
671

    
672
    def add_auth_provider(self, provider, **kwargs):
673
        info_data = ''
674
        if 'provider_info' in kwargs:
675
            info_data = kwargs.pop('provider_info')
676
            if isinstance(info_data, dict):
677
                info_data = json.dumps(info_data)
678

    
679
        if self.can_add_auth_provider(provider, **kwargs):
680
            self.auth_providers.create(module=provider, active=True,
681
                                       info_data=info_data,
682
                                       **kwargs)
683
        else:
684
            raise Exception('Cannot add provider')
685

    
686
    def add_pending_auth_provider(self, pending):
687
        """
688
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
689
        the current user.
690
        """
691
        if not isinstance(pending, PendingThirdPartyUser):
692
            pending = PendingThirdPartyUser.objects.get(token=pending)
693

    
694
        provider = self.add_auth_provider(pending.provider,
695
                               identifier=pending.third_party_identifier,
696
                                affiliation=pending.affiliation,
697
                                          provider_info=pending.info)
698

    
699
        if email_re.match(pending.email or '') and pending.email != self.email:
700
            self.additionalmail_set.get_or_create(email=pending.email)
701

    
702
        pending.delete()
703
        return provider
704

    
705
    def remove_auth_provider(self, provider, **kwargs):
706
        self.auth_providers.get(module=provider, **kwargs).delete()
707

    
708
    # user urls
709
    def get_resend_activation_url(self):
710
        return reverse('send_activation', kwargs={'user_id': self.pk})
711

    
712
    def get_provider_remove_url(self, module, **kwargs):
713
        return reverse('remove_auth_provider', kwargs={
714
            'pk': self.auth_providers.get(module=module, **kwargs).pk})
715

    
716
    def get_activation_url(self, nxt=False):
717
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
718
                                 quote(self.auth_token))
719
        if nxt:
720
            url += "&next=%s" % quote(nxt)
721
        return url
722

    
723
    def get_password_reset_url(self, token_generator=default_token_generator):
724
        return reverse('django.contrib.auth.views.password_reset_confirm',
725
                          kwargs={'uidb36':int_to_base36(self.id),
726
                                  'token':token_generator.make_token(self)})
727

    
728
    def get_auth_providers(self):
729
        return self.auth_providers.all()
730

    
731
    def get_available_auth_providers(self):
732
        """
733
        Returns a list of providers available for user to connect to.
734
        """
735
        providers = []
736
        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
737
            if self.can_add_auth_provider(module):
738
                providers.append(provider_settings(self))
739

    
740
        return providers
741

    
742
    def get_active_auth_providers(self, **filters):
743
        providers = []
744
        for provider in self.auth_providers.active(**filters):
745
            if auth_providers.get_provider(provider.module).is_available_for_login():
746
                providers.append(provider)
747
        return providers
748

    
749
    @property
750
    def auth_providers_display(self):
751
        return ",".join(map(lambda x:unicode(x), self.auth_providers.active()))
752

    
753
    def get_inactive_message(self):
754
        msg_extra = ''
755
        message = ''
756
        if self.activation_sent:
757
            if self.email_verified:
758
                message = _(astakos_messages.ACCOUNT_INACTIVE)
759
            else:
760
                message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
761
                if astakos_settings.MODERATION_ENABLED:
762
                    msg_extra = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
763
                else:
764
                    url = self.get_resend_activation_url()
765
                    msg_extra = mark_safe(_(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP) + \
766
                                u' ' + \
767
                                _('<a href="%s">%s?</a>') % (url,
768
                                _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
769
        else:
770
            if astakos_settings.MODERATION_ENABLED:
771
                message = _(astakos_messages.ACCOUNT_PENDING_MODERATION)
772
            else:
773
                message = astakos_messages.ACCOUNT_PENDING_ACTIVATION
774
                url = self.get_resend_activation_url()
775
                msg_extra = mark_safe(_('<a href="%s">%s?</a>') % (url,
776
                            _(astakos_messages.ACCOUNT_RESEND_ACTIVATION_PROMPT)))
777

    
778
        return mark_safe(message + u' '+ msg_extra)
779

    
780

    
781
class AstakosUserAuthProviderManager(models.Manager):
782

    
783
    def active(self, **filters):
784
        return self.filter(active=True, **filters)
785

    
786

    
787
class AstakosUserAuthProvider(models.Model):
788
    """
789
    Available user authentication methods.
790
    """
791
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
792
                                   null=True, default=None)
793
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
794
    module = models.CharField('Provider', max_length=255, blank=False,
795
                                default='local')
796
    identifier = models.CharField('Third-party identifier',
797
                                              max_length=255, null=True,
798
                                              blank=True)
799
    active = models.BooleanField(default=True)
800
    auth_backend = models.CharField('Backend', max_length=255, blank=False,
801
                                   default='astakos')
802
    info_data = models.TextField(default="", null=True, blank=True)
803
    created = models.DateTimeField('Creation date', auto_now_add=True)
804

    
805
    objects = AstakosUserAuthProviderManager()
806

    
807
    class Meta:
808
        unique_together = (('identifier', 'module', 'user'), )
809
        ordering = ('module', 'created')
810

    
811
    def __init__(self, *args, **kwargs):
812
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
813
        try:
814
            self.info = json.loads(self.info_data)
815
            if not self.info:
816
                self.info = {}
817
        except Exception, e:
818
            self.info = {}
819

    
820
        for key,value in self.info.iteritems():
821
            setattr(self, 'info_%s' % key, value)
822

    
823

    
824
    @property
825
    def settings(self):
826
        return auth_providers.get_provider(self.module)
827

    
828
    @property
829
    def details_display(self):
830
        try:
831
          return self.settings.get_details_tpl_display % self.__dict__
832
        except:
833
          return ''
834

    
835
    @property
836
    def title_display(self):
837
        title_tpl = self.settings.get_title_display
838
        try:
839
            if self.settings.get_user_title_display:
840
                title_tpl = self.settings.get_user_title_display
841
        except Exception, e:
842
            pass
843
        try:
844
          return title_tpl % self.__dict__
845
        except:
846
          return self.settings.get_title_display % self.__dict__
847

    
848
    def can_remove(self):
849
        return self.user.can_remove_auth_provider(self.module)
850

    
851
    def delete(self, *args, **kwargs):
852
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
853
        if self.module == 'local':
854
            self.user.set_unusable_password()
855
            self.user.save()
856
        return ret
857

    
858
    def __repr__(self):
859
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
860

    
861
    def __unicode__(self):
862
        if self.identifier:
863
            return "%s:%s" % (self.module, self.identifier)
864
        if self.auth_backend:
865
            return "%s:%s" % (self.module, self.auth_backend)
866
        return self.module
867

    
868
    def save(self, *args, **kwargs):
869
        self.info_data = json.dumps(self.info)
870
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
871

    
872

    
873
class Membership(models.Model):
874
    person = models.ForeignKey(AstakosUser)
875
    group = models.ForeignKey(AstakosGroup)
876
    date_requested = models.DateField(default=datetime.now(), blank=True)
877
    date_joined = models.DateField(null=True, db_index=True, blank=True)
878

    
879
    class Meta:
880
        unique_together = ("person", "group")
881

    
882
    def save(self, *args, **kwargs):
883
        if not self.id:
884
            if not self.group.moderation_enabled:
885
                self.date_joined = datetime.now()
886
        super(Membership, self).save(*args, **kwargs)
887

    
888
    @property
889
    def is_approved(self):
890
        if self.date_joined:
891
            return True
892
        return False
893

    
894
    def approve(self):
895
        if self.is_approved:
896
            return
897
        if self.group.max_participants:
898
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
899
            'Maximum participant number has been reached.'
900
        self.date_joined = datetime.now()
901
        self.save()
902
        quota_disturbed.send(sender=self, users=(self.person,))
903

    
904
    def disapprove(self):
905
        self.delete()
906
        quota_disturbed.send(sender=self, users=(self.person,))
907

    
908
class AstakosQuotaManager(models.Manager):
909
    def _update_or_create(self, **kwargs):
910
        assert kwargs, \
911
            'update_or_create() must be passed at least one keyword argument'
912
        obj, created = self.get_or_create(**kwargs)
913
        defaults = kwargs.pop('defaults', {})
914
        if created:
915
            return obj, True, False
916
        else:
917
            try:
918
                params = dict(
919
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
920
                params.update(defaults)
921
                for attr, val in params.items():
922
                    if hasattr(obj, attr):
923
                        setattr(obj, attr, val)
924
                sid = transaction.savepoint()
925
                obj.save(force_update=True)
926
                transaction.savepoint_commit(sid)
927
                return obj, False, True
928
            except IntegrityError, e:
929
                transaction.savepoint_rollback(sid)
930
                try:
931
                    return self.get(**kwargs), False, False
932
                except self.model.DoesNotExist:
933
                    raise e
934

    
935
    update_or_create = _update_or_create
936

    
937
class AstakosGroupQuota(models.Model):
938
    objects = AstakosQuotaManager()
939
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
940
    uplimit = models.BigIntegerField('Up limit', null=True)
941
    resource = models.ForeignKey(Resource)
942
    group = models.ForeignKey(AstakosGroup, blank=True)
943

    
944
    class Meta:
945
        unique_together = ("resource", "group")
946

    
947
class AstakosUserQuota(models.Model):
948
    objects = AstakosQuotaManager()
949
    limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
950
    uplimit = models.BigIntegerField('Up limit', null=True)
951
    resource = models.ForeignKey(Resource)
952
    user = models.ForeignKey(AstakosUser)
953

    
954
    class Meta:
955
        unique_together = ("resource", "user")
956

    
957

    
958
class ApprovalTerms(models.Model):
959
    """
960
    Model for approval terms
961
    """
962

    
963
    date = models.DateTimeField(
964
        'Issue date', db_index=True, default=datetime.now())
965
    location = models.CharField('Terms location', max_length=255)
966

    
967

    
968
class Invitation(models.Model):
969
    """
970
    Model for registring invitations
971
    """
972
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
973
                                null=True)
974
    realname = models.CharField('Real name', max_length=255)
975
    username = models.CharField('Unique ID', max_length=255, unique=True)
976
    code = models.BigIntegerField('Invitation code', db_index=True)
977
    is_consumed = models.BooleanField('Consumed?', default=False)
978
    created = models.DateTimeField('Creation date', auto_now_add=True)
979
    consumed = models.DateTimeField('Consumption date', null=True, blank=True)
980

    
981
    def __init__(self, *args, **kwargs):
982
        super(Invitation, self).__init__(*args, **kwargs)
983
        if not self.id:
984
            self.code = _generate_invitation_code()
985

    
986
    def consume(self):
987
        self.is_consumed = True
988
        self.consumed = datetime.now()
989
        self.save()
990

    
991
    def __unicode__(self):
992
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
993

    
994

    
995
class EmailChangeManager(models.Manager):
996

    
997
    @transaction.commit_on_success
998
    def change_email(self, activation_key):
999
        """
1000
        Validate an activation key and change the corresponding
1001
        ``User`` if valid.
1002

1003
        If the key is valid and has not expired, return the ``User``
1004
        after activating.
1005

1006
        If the key is not valid or has expired, return ``None``.
1007

1008
        If the key is valid but the ``User`` is already active,
1009
        return ``None``.
1010

1011
        After successful email change the activation record is deleted.
1012

1013
        Throws ValueError if there is already
1014
        """
1015
        try:
1016
            email_change = self.model.objects.get(
1017
                activation_key=activation_key)
1018
            if email_change.activation_key_expired():
1019
                email_change.delete()
1020
                raise EmailChange.DoesNotExist
1021
            # is there an active user with this address?
1022
            try:
1023
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1024
            except AstakosUser.DoesNotExist:
1025
                pass
1026
            else:
1027
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
1028
            # update user
1029
            user = AstakosUser.objects.get(pk=email_change.user_id)
1030
            old_email = user.email
1031
            user.email = email_change.new_email_address
1032
            user.save()
1033
            email_change.delete()
1034
            msg = "User %d changed email from %s to %s" % (user.pk, old_email,
1035
                                                          user.email)
1036
            logger.log(LOGGING_LEVEL, msg)
1037
            return user
1038
        except EmailChange.DoesNotExist:
1039
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
1040

    
1041

    
1042
class EmailChange(models.Model):
1043
    new_email_address = models.EmailField(_(u'new e-mail address'),
1044
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
1045
    user = models.ForeignKey(
1046
        AstakosUser, unique=True, related_name='emailchanges')
1047
    requested_at = models.DateTimeField(default=datetime.now())
1048
    activation_key = models.CharField(
1049
        max_length=40, unique=True, db_index=True)
1050

    
1051
    objects = EmailChangeManager()
1052

    
1053
    def get_url(self):
1054
        return reverse('email_change_confirm',
1055
                      kwargs={'activation_key': self.activation_key})
1056

    
1057
    def activation_key_expired(self):
1058
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1059
        return self.requested_at + expiration_date < datetime.now()
1060

    
1061

    
1062
class AdditionalMail(models.Model):
1063
    """
1064
    Model for registring invitations
1065
    """
1066
    owner = models.ForeignKey(AstakosUser)
1067
    email = models.EmailField()
1068

    
1069

    
1070
def _generate_invitation_code():
1071
    while True:
1072
        code = randint(1, 2L ** 63 - 1)
1073
        try:
1074
            Invitation.objects.get(code=code)
1075
            # An invitation with this code already exists, try again
1076
        except Invitation.DoesNotExist:
1077
            return code
1078

    
1079

    
1080
def get_latest_terms():
1081
    try:
1082
        term = ApprovalTerms.objects.order_by('-id')[0]
1083
        return term
1084
    except IndexError:
1085
        pass
1086
    return None
1087

    
1088
class PendingThirdPartyUser(models.Model):
1089
    """
1090
    Model for registring successful third party user authentications
1091
    """
1092
    third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
1093
    provider = models.CharField('Provider', max_length=255, blank=True)
1094
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1095
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
1096
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
1097
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
1098
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1099
    token = models.CharField('Token', max_length=255, null=True, blank=True)
1100
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1101
    info = models.TextField(default="", null=True, blank=True)
1102

    
1103
    class Meta:
1104
        unique_together = ("provider", "third_party_identifier")
1105

    
1106
    def get_user_instance(self):
1107
        d = self.__dict__
1108
        d.pop('_state', None)
1109
        d.pop('id', None)
1110
        d.pop('token', None)
1111
        d.pop('created', None)
1112
        d.pop('info', None)
1113
        user = AstakosUser(**d)
1114

    
1115
        return user
1116

    
1117
    @property
1118
    def realname(self):
1119
        return '%s %s' %(self.first_name, self.last_name)
1120

    
1121
    @realname.setter
1122
    def realname(self, value):
1123
        parts = value.split(' ')
1124
        if len(parts) == 2:
1125
            self.first_name = parts[0]
1126
            self.last_name = parts[1]
1127
        else:
1128
            self.last_name = parts[0]
1129

    
1130
    def save(self, **kwargs):
1131
        if not self.id:
1132
            # set username
1133
            while not self.username:
1134
                username =  uuid.uuid4().hex[:30]
1135
                try:
1136
                    AstakosUser.objects.get(username = username)
1137
                except AstakosUser.DoesNotExist, e:
1138
                    self.username = username
1139
        super(PendingThirdPartyUser, self).save(**kwargs)
1140

    
1141
    def generate_token(self):
1142
        self.password = self.third_party_identifier
1143
        self.last_login = datetime.now()
1144
        self.token = default_token_generator.make_token(self)
1145

    
1146
class SessionCatalog(models.Model):
1147
    session_key = models.CharField(_('session key'), max_length=40)
1148
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1149

    
1150

    
1151
def create_astakos_user(u):
1152
    try:
1153
        AstakosUser.objects.get(user_ptr=u.pk)
1154
    except AstakosUser.DoesNotExist:
1155
        extended_user = AstakosUser(user_ptr_id=u.pk)
1156
        extended_user.__dict__.update(u.__dict__)
1157
        extended_user.save()
1158
        if not extended_user.has_auth_provider('local'):
1159
            extended_user.add_auth_provider('local')
1160
    except BaseException, e:
1161
        logger.exception(e)
1162

    
1163

    
1164
def fix_superusers(sender, **kwargs):
1165
    # Associate superusers with AstakosUser
1166
    admins = User.objects.filter(is_superuser=True)
1167
    for u in admins:
1168
        create_astakos_user(u)
1169

    
1170

    
1171
def user_post_save(sender, instance, created, **kwargs):
1172
    if not created:
1173
        return
1174
    create_astakos_user(instance)
1175

    
1176

    
1177
def set_default_group(user):
1178
    try:
1179
        default = AstakosGroup.objects.get(name='default')
1180
        Membership(
1181
            group=default, person=user, date_joined=datetime.now()).save()
1182
    except AstakosGroup.DoesNotExist, e:
1183
        logger.exception(e)
1184

    
1185

    
1186
def astakosuser_pre_save(sender, instance, **kwargs):
1187
    instance.aquarium_report = False
1188
    instance.new = False
1189
    try:
1190
        db_instance = AstakosUser.objects.get(id=instance.id)
1191
    except AstakosUser.DoesNotExist:
1192
        # create event
1193
        instance.aquarium_report = True
1194
        instance.new = True
1195
    else:
1196
        get = AstakosUser.__getattribute__
1197
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1198
                   BILLING_FIELDS)
1199
        instance.aquarium_report = True if l else False
1200

    
1201

    
1202
def astakosuser_post_save(sender, instance, created, **kwargs):
1203
    if instance.aquarium_report:
1204
        report_user_event(instance, create=instance.new)
1205
    if not created:
1206
        return
1207
    set_default_group(instance)
1208
    # TODO handle socket.error & IOError
1209
    register_users((instance,))
1210

    
1211

    
1212
def resource_post_save(sender, instance, created, **kwargs):
1213
    if not created:
1214
        return
1215
    register_resources((instance,))
1216

    
1217

    
1218
def send_quota_disturbed(sender, instance, **kwargs):
1219
    users = []
1220
    extend = users.extend
1221
    if sender == Membership:
1222
        if not instance.group.is_enabled:
1223
            return
1224
        extend([instance.person])
1225
    elif sender == AstakosUserQuota:
1226
        extend([instance.user])
1227
    elif sender == AstakosGroupQuota:
1228
        if not instance.group.is_enabled:
1229
            return
1230
        extend(instance.group.astakosuser_set.all())
1231
    elif sender == AstakosGroup:
1232
        if not instance.is_enabled:
1233
            return
1234
    quota_disturbed.send(sender=sender, users=users)
1235

    
1236

    
1237
def on_quota_disturbed(sender, users, **kwargs):
1238
#     print '>>>', locals()
1239
    if not users:
1240
        return
1241
    send_quota(users)
1242

    
1243
def renew_token(sender, instance, **kwargs):
1244
    if not instance.auth_token:
1245
        instance.renew_token()
1246

    
1247
post_syncdb.connect(fix_superusers)
1248
post_save.connect(user_post_save, sender=User)
1249
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1250
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1251
post_save.connect(resource_post_save, sender=Resource)
1252

    
1253
quota_disturbed = Signal(providing_args=["users"])
1254
quota_disturbed.connect(on_quota_disturbed)
1255

    
1256
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1257
post_delete.connect(send_quota_disturbed, sender=Membership)
1258
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1259
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1260
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1261
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1262

    
1263
pre_save.connect(renew_token, sender=AstakosUser)
1264
pre_save.connect(renew_token, sender=Service)