Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 63836eda

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
        if not provider_settings.is_available_for_login():
623
            return False
624

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

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

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

    
642
        return True
643

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

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

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

    
655
        return True
656

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

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

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

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

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

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

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

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

    
701
        pending.delete()
702
        return provider
703

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

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

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

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

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

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

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

    
739
        return providers
740

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

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

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

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

    
779

    
780
class AstakosUserAuthProviderManager(models.Manager):
781

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

    
785

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

    
804
    objects = AstakosUserAuthProviderManager()
805

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

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

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

    
822

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

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

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

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

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

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

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

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

    
871

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

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

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

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

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

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

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

    
934
    update_or_create = _update_or_create
935

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

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

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

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

    
956

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

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

    
966

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

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

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

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

    
993

    
994
class EmailChangeManager(models.Manager):
995

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

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

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

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

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

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

    
1040

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

    
1050
    objects = EmailChangeManager()
1051

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

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

    
1060

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

    
1068

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

    
1078

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

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

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

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

    
1114
        return user
1115

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

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

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

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

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

    
1149

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

    
1162

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

    
1169

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

    
1175

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

    
1184

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

    
1200

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

    
1210

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

    
1216

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

    
1235

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

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

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

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

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

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