Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (56.9 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

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

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

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

    
65
from astakos.im.settings import (
66
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
67
    AUTH_TOKEN_DURATION, BILLING_FIELDS,
68
    EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
69
    GROUP_CREATION_SUBJECT
70
)
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
from astakos.im.notifications import build_notification
80

    
81
import astakos.im.messages as astakos_messages
82

    
83
logger = logging.getLogger(__name__)
84

    
85
DEFAULT_CONTENT_TYPE = None
86
_content_type = None
87

    
88
def get_content_type():
89
    global _content_type
90
    if _content_type is not None:
91
        return _content_type
92

    
93
    try:
94
        content_type = ContentType.objects.get(app_label='im', model='astakosuser')
95
    except:
96
        content_type = DEFAULT_CONTENT_TYPE
97
    _content_type = content_type
98
    return content_type
99

    
100
RESOURCE_SEPARATOR = '.'
101

    
102
inf = float('inf')
103

    
104
class Service(models.Model):
105
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
106
    url = models.FilePathField()
107
    icon = models.FilePathField(blank=True)
108
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
109
                                  null=True, blank=True)
110
    auth_token_created = models.DateTimeField(_('Token creation date'), null=True)
111
    auth_token_expires = models.DateTimeField(
112
        _('Token expiration date'), null=True)
113

    
114
    def renew_token(self):
115
        md5 = hashlib.md5()
116
        md5.update(self.name.encode('ascii', 'ignore'))
117
        md5.update(self.url.encode('ascii', 'ignore'))
118
        md5.update(asctime())
119

    
120
        self.auth_token = b64encode(md5.digest())
121
        self.auth_token_created = datetime.now()
122
        self.auth_token_expires = self.auth_token_created + \
123
            timedelta(hours=AUTH_TOKEN_DURATION)
124

    
125
    def __str__(self):
126
        return self.name
127

    
128
    @property
129
    def resources(self):
130
        return self.resource_set.all()
131

    
132
    @resources.setter
133
    def resources(self, resources):
134
        for s in resources:
135
            self.resource_set.create(**s)
136

    
137
    def add_resource(self, service, resource, uplimit, update=True):
138
        """Raises ObjectDoesNotExist, IntegrityError"""
139
        resource = Resource.objects.get(service__name=service, name=resource)
140
        if update:
141
            AstakosUserQuota.objects.update_or_create(user=self,
142
                                                      resource=resource,
143
                                                      defaults={'uplimit': uplimit})
144
        else:
145
            q = self.astakosuserquota_set
146
            q.create(resource=resource, uplimit=uplimit)
147

    
148

    
149
class ResourceMetadata(models.Model):
150
    key = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
151
    value = models.CharField(_('Value'), max_length=255)
152

    
153

    
154
class Resource(models.Model):
155
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
156
    meta = models.ManyToManyField(ResourceMetadata)
157
    service = models.ForeignKey(Service)
158
    desc = models.TextField(_('Description'), null=True)
159
    unit = models.CharField(_('Name'), null=True, max_length=255)
160
    group = models.CharField(_('Group'), null=True, max_length=255)
161

    
162
    def __str__(self):
163
        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
164

    
165

    
166
class GroupKind(models.Model):
167
    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
168

    
169
    def __str__(self):
170
        return self.name
171

    
172

    
173
class AstakosGroup(Group):
174
    kind = models.ForeignKey(GroupKind)
175
    homepage = models.URLField(
176
        _('Homepage Url'), max_length=255, null=True, blank=True)
177
    desc = models.TextField(_('Description'), null=True)
178
    policy = models.ManyToManyField(
179
        Resource,
180
        null=True,
181
        blank=True,
182
        through='AstakosGroupQuota'
183
    )
184
    creation_date = models.DateTimeField(
185
        _('Creation date'),
186
        default=datetime.now()
187
    )
188
    issue_date = models.DateTimeField(
189
        _('Start date'),
190
        null=True
191
    )
192
    expiration_date = models.DateTimeField(
193
        _('Expiration date'),
194
        null=True
195
    )
196
    moderation_enabled = models.BooleanField(
197
        _('Moderated membership?'),
198
        default=True
199
    )
200
    approval_date = models.DateTimeField(
201
        _('Activation date'),
202
        null=True,
203
        blank=True
204
    )
205
    estimated_participants = models.PositiveIntegerField(
206
        _('Estimated #members'),
207
        null=True,
208
        blank=True,
209
    )
210
    max_participants = models.PositiveIntegerField(
211
        _('Maximum numder of participants'),
212
        null=True,
213
        blank=True
214
    )
215

    
216
    @property
217
    def is_disabled(self):
218
        if not self.approval_date:
219
            return True
220
        return False
221

    
222
    @property
223
    def is_enabled(self):
224
        if self.is_disabled:
225
            return False
226
        if not self.issue_date:
227
            return False
228
        if not self.expiration_date:
229
            return True
230
        now = datetime.now()
231
        if self.issue_date > now:
232
            return False
233
        if now >= self.expiration_date:
234
            return False
235
        return True
236

    
237
    def enable(self):
238
        if self.is_enabled:
239
            return
240
        self.approval_date = datetime.now()
241
        self.save()
242
        quota_disturbed.send(sender=self, users=self.approved_members)
243
        #propagate_groupmembers_quota.apply_async(
244
        #    args=[self], eta=self.issue_date)
245
        #propagate_groupmembers_quota.apply_async(
246
        #    args=[self], eta=self.expiration_date)
247

    
248
    def disable(self):
249
        if self.is_disabled:
250
            return
251
        self.approval_date = None
252
        self.save()
253
        quota_disturbed.send(sender=self, users=self.approved_members)
254

    
255
    def approve_member(self, person):
256
        m, created = self.membership_set.get_or_create(person=person)
257
        m.approve()
258

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

    
264
    @property
265
    def approved_members(self):
266
        q = self.membership_set.select_related().all()
267
        return [m.person for m in q if m.is_approved]
268

    
269
    @property
270
    def quota(self):
271
        d = defaultdict(int)
272
        for q in self.astakosgroupquota_set.select_related().all():
273
            d[q.resource] += q.uplimit or inf
274
        return d
275

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

    
289
    @property
290
    def policies(self):
291
        return self.astakosgroupquota_set.select_related().all()
292

    
293
    @policies.setter
294
    def policies(self, policies):
295
        for p in policies:
296
            service = p.get('service', None)
297
            resource = p.get('resource', None)
298
            uplimit = p.get('uplimit', 0)
299
            update = p.get('update', True)
300
            self.add_policy(service, resource, uplimit, update)
301

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

    
306
    @property
307
    def owner_details(self):
308
        return self.owner.select_related().all()
309

    
310
    @owners.setter
311
    def owners(self, l):
312
        self.owner = l
313
        map(self.approve_member, l)
314

    
315

    
316

    
317
class AstakosUserManager(UserManager):
318

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

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

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

    
344

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

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

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

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

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

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

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

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

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

    
383
    objects = AstakosUserManager()
384

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

    
388
    class Meta:
389
        unique_together = ("provider", "third_party_identifier")
390

    
391
    def __init__(self, *args, **kwargs):
392
        super(AstakosUser, self).__init__(*args, **kwargs)
393
        self.__has_signed_terms = self.has_signed_terms
394
        if not self.id:
395
            self.is_active = False
396

    
397
    @property
398
    def realname(self):
399
        return '%s %s' % (self.first_name, self.last_name)
400

    
401
    @realname.setter
402
    def realname(self, value):
403
        parts = value.split(' ')
404
        if len(parts) == 2:
405
            self.first_name = parts[0]
406
            self.last_name = parts[1]
407
        else:
408
            self.last_name = parts[0]
409

    
410
    def add_permission(self, pname):
411
        if self.has_perm(pname):
412
            return
413
        p, created = Permission.objects.get_or_create(
414
                                    codename=pname,
415
                                    name=pname.capitalize(),
416
                                    content_type=get_content_type())
417
        self.user_permissions.add(p)
418

    
419
    def remove_permission(self, pname):
420
        if self.has_perm(pname):
421
            return
422
        p = Permission.objects.get(codename=pname,
423
                                   content_type=get_content_type())
424
        self.user_permissions.remove(p)
425

    
426
    @property
427
    def invitation(self):
428
        try:
429
            return Invitation.objects.get(username=self.email)
430
        except Invitation.DoesNotExist:
431
            return None
432

    
433
    def invite(self, email, realname):
434
        inv = Invitation(inviter=self, username=email, realname=realname)
435
        inv.save()
436
        send_invitation(inv)
437
        self.invitations = max(0, self.invitations - 1)
438
        self.save()
439

    
440
    @property
441
    def quota(self):
442
        """Returns a dict with the sum of quota limits per resource"""
443
        d = defaultdict(int)
444
        for q in self.policies:
445
            d[q.resource] += q.uplimit or inf
446
        for m in self.projectmembership_set.select_related().all():
447
            if not m.acceptance_date:
448
                continue
449
            p = m.project
450
            if not p.is_active:
451
                continue
452
            grants = p.application.definition.projectresourcegrant_set.all()
453
            for g in grants:
454
                d[g.resource] += g.member_limit or inf
455
        # TODO set default for remaining
456
        return d
457

    
458
    @property
459
    def policies(self):
460
        return self.astakosuserquota_set.select_related().all()
461

    
462
    @policies.setter
463
    def policies(self, policies):
464
        for p in policies:
465
            service = policies.get('service', None)
466
            resource = policies.get('resource', None)
467
            uplimit = policies.get('uplimit', 0)
468
            update = policies.get('update', True)
469
            self.add_policy(service, resource, uplimit, update)
470

    
471
    def add_policy(self, service, resource, uplimit, update=True):
472
        """Raises ObjectDoesNotExist, IntegrityError"""
473
        resource = Resource.objects.get(service__name=service, name=resource)
474
        if update:
475
            AstakosUserQuota.objects.update_or_create(user=self,
476
                                                      resource=resource,
477
                                                      defaults={'uplimit': uplimit})
478
        else:
479
            q = self.astakosuserquota_set
480
            q.create(resource=resource, uplimit=uplimit)
481

    
482
    def remove_policy(self, service, resource):
483
        """Raises ObjectDoesNotExist, IntegrityError"""
484
        resource = Resource.objects.get(service__name=service, name=resource)
485
        q = self.policies.get(resource=resource).delete()
486

    
487
    @property
488
    def extended_groups(self):
489
        return self.membership_set.select_related().all()
490

    
491
    @extended_groups.setter
492
    def extended_groups(self, groups):
493
        #TODO exceptions
494
        for name in (groups or ()):
495
            group = AstakosGroup.objects.get(name=name)
496
            self.membership_set.create(group=group)
497

    
498
    def save(self, update_timestamps=True, **kwargs):
499
        if update_timestamps:
500
            if not self.id:
501
                self.date_joined = datetime.now()
502
            self.updated = datetime.now()
503

    
504
        # update date_signed_terms if necessary
505
        if self.__has_signed_terms != self.has_signed_terms:
506
            self.date_signed_terms = datetime.now()
507

    
508
        if not self.id:
509
            # set username
510
            self.username = self.email
511

    
512
        self.validate_unique_email_isactive()
513
        if self.is_active and self.activation_sent:
514
            # reset the activation sent
515
            self.activation_sent = None
516

    
517
        super(AstakosUser, self).save(**kwargs)
518

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

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

    
535
    def flush_sessions(self, current_key=None):
536
        q = self.sessions
537
        if current_key:
538
            q = q.exclude(session_key=current_key)
539

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

    
549
    def __unicode__(self):
550
        return '%s (%s)' % (self.realname, self.email)
551

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

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

    
571
    @property
572
    def signed_terms(self):
573
        term = get_latest_terms()
574
        if not term:
575
            return True
576
        if not self.has_signed_terms:
577
            return False
578
        if not self.date_signed_terms:
579
            return False
580
        if self.date_signed_terms < term.date:
581
            self.has_signed_terms = False
582
            self.date_signed_terms = None
583
            self.save()
584
            return False
585
        return True
586

    
587
    def set_invitations_level(self):
588
        """
589
        Update user invitation level
590
        """
591
        level = self.invitation.inviter.level + 1
592
        self.level = level
593
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
594

    
595
    def can_login_with_auth_provider(self, provider):
596
        if not self.has_auth_provider(provider):
597
            return False
598
        else:
599
            return auth_providers.get_provider(provider).is_available_for_login()
600

    
601
    def can_add_auth_provider(self, provider, **kwargs):
602
        provider_settings = auth_providers.get_provider(provider)
603
        if not provider_settings.is_available_for_login():
604
            return False
605

    
606
        if self.has_auth_provider(provider) and \
607
           provider_settings.one_per_user:
608
            return False
609

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

    
620
        return True
621

    
622
    def can_remove_auth_provider(self, provider):
623
        if len(self.get_active_auth_providers()) <= 1:
624
            return False
625
        return True
626

    
627
    def can_change_password(self):
628
        return self.has_auth_provider('local', auth_backend='astakos')
629

    
630
    def has_auth_provider(self, provider, **kwargs):
631
        return bool(self.auth_providers.filter(module=provider,
632
                                               **kwargs).count())
633

    
634
    def add_auth_provider(self, provider, **kwargs):
635
        if self.can_add_auth_provider(provider, **kwargs):
636
            self.auth_providers.create(module=provider, active=True, **kwargs)
637
        else:
638
            raise Exception('Cannot add provider')
639

    
640
    def add_pending_auth_provider(self, pending):
641
        """
642
        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
643
        the current user.
644
        """
645
        if not isinstance(pending, PendingThirdPartyUser):
646
            pending = PendingThirdPartyUser.objects.get(token=pending)
647

    
648
        provider = self.add_auth_provider(pending.provider,
649
                               identifier=pending.third_party_identifier)
650

    
651
        if email_re.match(pending.email or '') and pending.email != self.email:
652
            self.additionalmail_set.get_or_create(email=pending.email)
653

    
654
        pending.delete()
655
        return provider
656

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

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

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

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

    
676
    def get_auth_providers(self):
677
        return self.auth_providers.all()
678

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

    
688
        return providers
689

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

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

    
701

    
702
class AstakosUserAuthProviderManager(models.Manager):
703

    
704
    def active(self):
705
        return self.filter(active=True)
706

    
707

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

    
724
    objects = AstakosUserAuthProviderManager()
725

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

    
729
    @property
730
    def settings(self):
731
        return auth_providers.get_provider(self.module)
732

    
733
    @property
734
    def details_display(self):
735
        return self.settings.details_tpl % self.__dict__
736

    
737
    def can_remove(self):
738
        return self.user.can_remove_auth_provider(self.module)
739

    
740
    def delete(self, *args, **kwargs):
741
        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
742
        if self.module == 'local':
743
            self.user.set_unusable_password()
744
            self.user.save()
745
        return ret
746

    
747
    def __repr__(self):
748
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
749

    
750
    def __unicode__(self):
751
        if self.identifier:
752
            return "%s:%s" % (self.module, self.identifier)
753
        if self.auth_backend:
754
            return "%s:%s" % (self.module, self.auth_backend)
755
        return self.module
756

    
757

    
758

    
759
class Membership(models.Model):
760
    person = models.ForeignKey(AstakosUser)
761
    group = models.ForeignKey(AstakosGroup)
762
    date_requested = models.DateField(default=datetime.now(), blank=True)
763
    date_joined = models.DateField(null=True, db_index=True, blank=True)
764

    
765
    class Meta:
766
        unique_together = ("person", "group")
767

    
768
    def save(self, *args, **kwargs):
769
        if not self.id:
770
            if not self.group.moderation_enabled:
771
                self.date_joined = datetime.now()
772
        super(Membership, self).save(*args, **kwargs)
773

    
774
    @property
775
    def is_approved(self):
776
        if self.date_joined:
777
            return True
778
        return False
779

    
780
    def approve(self):
781
        if self.is_approved:
782
            return
783
        if self.group.max_participants:
784
            assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
785
            'Maximum participant number has been reached.'
786
        self.date_joined = datetime.now()
787
        self.save()
788
        quota_disturbed.send(sender=self, users=(self.person,))
789

    
790
    def disapprove(self):
791
        approved = self.is_approved()
792
        self.delete()
793
        if approved:
794
            quota_disturbed.send(sender=self, users=(self.person,))
795

    
796
class ExtendedManager(models.Manager):
797
    def _update_or_create(self, **kwargs):
798
        assert kwargs, \
799
            'update_or_create() must be passed at least one keyword argument'
800
        obj, created = self.get_or_create(**kwargs)
801
        defaults = kwargs.pop('defaults', {})
802
        if created:
803
            return obj, True, False
804
        else:
805
            try:
806
                params = dict(
807
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
808
                params.update(defaults)
809
                for attr, val in params.items():
810
                    if hasattr(obj, attr):
811
                        setattr(obj, attr, val)
812
                sid = transaction.savepoint()
813
                obj.save(force_update=True)
814
                transaction.savepoint_commit(sid)
815
                return obj, False, True
816
            except IntegrityError, e:
817
                transaction.savepoint_rollback(sid)
818
                try:
819
                    return self.get(**kwargs), False, False
820
                except self.model.DoesNotExist:
821
                    raise e
822

    
823
    update_or_create = _update_or_create
824

    
825
class AstakosGroupQuota(models.Model):
826
    objects = ExtendedManager()
827
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
828
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
829
    resource = models.ForeignKey(Resource)
830
    group = models.ForeignKey(AstakosGroup, blank=True)
831

    
832
    class Meta:
833
        unique_together = ("resource", "group")
834

    
835
class AstakosUserQuota(models.Model):
836
    objects = ExtendedManager()
837
    limit = models.PositiveIntegerField(_('Limit'), null=True)    # obsolete field
838
    uplimit = models.BigIntegerField(_('Up limit'), null=True)
839
    resource = models.ForeignKey(Resource)
840
    user = models.ForeignKey(AstakosUser)
841

    
842
    class Meta:
843
        unique_together = ("resource", "user")
844

    
845

    
846
class ApprovalTerms(models.Model):
847
    """
848
    Model for approval terms
849
    """
850

    
851
    date = models.DateTimeField(
852
        _('Issue date'), db_index=True, default=datetime.now())
853
    location = models.CharField(_('Terms location'), max_length=255)
854

    
855

    
856
class Invitation(models.Model):
857
    """
858
    Model for registring invitations
859
    """
860
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
861
                                null=True)
862
    realname = models.CharField(_('Real name'), max_length=255)
863
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
864
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
865
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
866
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
867
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
868

    
869
    def __init__(self, *args, **kwargs):
870
        super(Invitation, self).__init__(*args, **kwargs)
871
        if not self.id:
872
            self.code = _generate_invitation_code()
873

    
874
    def consume(self):
875
        self.is_consumed = True
876
        self.consumed = datetime.now()
877
        self.save()
878

    
879
    def __unicode__(self):
880
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
881

    
882

    
883
class EmailChangeManager(models.Manager):
884
    @transaction.commit_on_success
885
    def change_email(self, activation_key):
886
        """
887
        Validate an activation key and change the corresponding
888
        ``User`` if valid.
889

890
        If the key is valid and has not expired, return the ``User``
891
        after activating.
892

893
        If the key is not valid or has expired, return ``None``.
894

895
        If the key is valid but the ``User`` is already active,
896
        return ``None``.
897

898
        After successful email change the activation record is deleted.
899

900
        Throws ValueError if there is already
901
        """
902
        try:
903
            email_change = self.model.objects.get(
904
                activation_key=activation_key)
905
            if email_change.activation_key_expired():
906
                email_change.delete()
907
                raise EmailChange.DoesNotExist
908
            # is there an active user with this address?
909
            try:
910
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
911
            except AstakosUser.DoesNotExist:
912
                pass
913
            else:
914
                raise ValueError(_(astakos_messages.NEW_EMAIL_ADDR_RESERVED))
915
            # update user
916
            user = AstakosUser.objects.get(pk=email_change.user_id)
917
            user.email = email_change.new_email_address
918
            user.save()
919
            email_change.delete()
920
            return user
921
        except EmailChange.DoesNotExist:
922
            raise ValueError(_(astakos_messages.INVALID_ACTIVATION_KEY))
923

    
924

    
925
class EmailChange(models.Model):
926
    new_email_address = models.EmailField(_(u'new e-mail address'),
927
                                          help_text=_(astakos_messages.EMAIL_CHANGE_NEW_ADDR_HELP))
928
    user = models.ForeignKey(
929
        AstakosUser, unique=True, related_name='emailchange_user')
930
    requested_at = models.DateTimeField(default=datetime.now())
931
    activation_key = models.CharField(
932
        max_length=40, unique=True, db_index=True)
933

    
934
    objects = EmailChangeManager()
935

    
936
    def activation_key_expired(self):
937
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
938
        return self.requested_at + expiration_date < datetime.now()
939

    
940

    
941
class AdditionalMail(models.Model):
942
    """
943
    Model for registring invitations
944
    """
945
    owner = models.ForeignKey(AstakosUser)
946
    email = models.EmailField()
947

    
948

    
949
def _generate_invitation_code():
950
    while True:
951
        code = randint(1, 2L ** 63 - 1)
952
        try:
953
            Invitation.objects.get(code=code)
954
            # An invitation with this code already exists, try again
955
        except Invitation.DoesNotExist:
956
            return code
957

    
958

    
959
def get_latest_terms():
960
    try:
961
        term = ApprovalTerms.objects.order_by('-id')[0]
962
        return term
963
    except IndexError:
964
        pass
965
    return None
966

    
967
class PendingThirdPartyUser(models.Model):
968
    """
969
    Model for registring successful third party user authentications
970
    """
971
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
972
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
973
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
974
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
975
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
976
    affiliation = models.CharField('Affiliation', max_length=255, blank=True)
977
    username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
978
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
979
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
980

    
981
    class Meta:
982
        unique_together = ("provider", "third_party_identifier")
983

    
984
    @property
985
    def realname(self):
986
        return '%s %s' %(self.first_name, self.last_name)
987

    
988
    @realname.setter
989
    def realname(self, value):
990
        parts = value.split(' ')
991
        if len(parts) == 2:
992
            self.first_name = parts[0]
993
            self.last_name = parts[1]
994
        else:
995
            self.last_name = parts[0]
996

    
997
    def save(self, **kwargs):
998
        if not self.id:
999
            # set username
1000
            while not self.username:
1001
                username =  uuid.uuid4().hex[:30]
1002
                try:
1003
                    AstakosUser.objects.get(username = username)
1004
                except AstakosUser.DoesNotExist, e:
1005
                    self.username = username
1006
        super(PendingThirdPartyUser, self).save(**kwargs)
1007

    
1008
    def generate_token(self):
1009
        self.password = self.third_party_identifier
1010
        self.last_login = datetime.now()
1011
        self.token = default_token_generator.make_token(self)
1012

    
1013
class SessionCatalog(models.Model):
1014
    session_key = models.CharField(_('session key'), max_length=40)
1015
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1016

    
1017
class MemberJoinPolicy(models.Model):
1018
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1019
    description = models.CharField(_('Description'), max_length=80)
1020

    
1021
    def __str__(self):
1022
        return self.policy
1023

    
1024
class MemberLeavePolicy(models.Model):
1025
    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
1026
    description = models.CharField(_('Description'), max_length=80)
1027

    
1028
    def __str__(self):
1029
        return self.policy
1030

    
1031
_auto_accept_join = False
1032
def get_auto_accept_join():
1033
    global _auto_accept
1034
    if _auto_accept is not False:
1035
        return _auto_accept
1036
    try:
1037
        auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
1038
    except:
1039
        auto_accept = None
1040
    _auto_accept = auto_accept
1041
    return auto_accept
1042

    
1043
_auto_accept_leave = False
1044
def get_auto_accept_leave():
1045
    global _auto_accept
1046
    if _auto_accept is not False:
1047
        return _auto_accept
1048
    try:
1049
        auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
1050
    except:
1051
        auto_accept = None
1052
    _auto_accept = auto_accept
1053
    return auto_accept
1054

    
1055
class ProjectDefinition(models.Model):
1056
    name = models.CharField(max_length=80)
1057
    homepage = models.URLField(max_length=255, null=True, blank=True)
1058
    description = models.TextField(null=True)
1059
    start_date = models.DateTimeField()
1060
    end_date = models.DateTimeField()
1061
    member_join_policy = models.ForeignKey(MemberJoinPolicy)
1062
    member_leave_policy = models.ForeignKey(MemberLeavePolicy)
1063
    limit_on_members_number = models.PositiveIntegerField(null=True,blank=True)
1064
    resource_grants = models.ManyToManyField(
1065
        Resource,
1066
        null=True,
1067
        blank=True,
1068
        through='ProjectResourceGrant'
1069
    )
1070
    
1071
    def save(self):
1072
        self.validate_name()
1073
        super(ProjectDefinition, self).save()
1074
        
1075
    @property
1076
    def violated_resource_grants(self):
1077
        return False
1078
    
1079
    def add_resource_policy(self, service, resource, uplimit, update=True):
1080
        """Raises ObjectDoesNotExist, IntegrityError"""
1081
        resource = Resource.objects.get(service__name=service, name=resource)
1082
        if update:
1083
            ProjectResourceGrant.objects.update_or_create(
1084
                project_definition=self,
1085
                resource=resource,
1086
                defaults={'member_limit': uplimit}
1087
            )
1088
        else:
1089
            q = self.projectresourcegrant_set
1090
            q.create(resource=resource, member_limit=uplimit)
1091

    
1092
    @property
1093
    def resource_policies(self):
1094
        return self.projectresourcegrant_set.all()
1095

    
1096
    @resource_policies.setter
1097
    def resource_policies(self, policies):
1098
        for p in policies:
1099
            service = p.get('service', None)
1100
            resource = p.get('resource', None)
1101
            uplimit = p.get('uplimit', 0)
1102
            update = p.get('update', True)
1103
            self.add_resource_policy(service, resource, uplimit, update)
1104
    
1105
    def validate_name(self):
1106
        """
1107
        Validate name uniqueness among all active projects.
1108
        """
1109
        alive_projects = list(get_alive_projects())
1110
        q = filter(
1111
            lambda p: p.definition.name == self.name and \
1112
                p.application.id != self.projectapplication.id,
1113
            alive_projects
1114
        )
1115
        if q:
1116
            raise ValidationError(
1117
                {'name': [_(astakos_messages.UNIQUE_PROJECT_NAME_CONSTRAIN_ERR)]}
1118
            )
1119

    
1120

    
1121
class ProjectResourceGrant(models.Model):
1122
    objects = ExtendedManager()
1123
    member_limit = models.BigIntegerField(null=True)
1124
    project_limit = models.BigIntegerField(null=True)
1125
    resource = models.ForeignKey(Resource)
1126
    project_definition = models.ForeignKey(ProjectDefinition, blank=True)
1127

    
1128
    class Meta:
1129
        unique_together = ("resource", "project_definition")
1130

    
1131
class ProjectApplication(models.Model):
1132
    PENDING, APPROVED, REPLACED = range(3)
1133
    states_list = ['Pending', 'Approved', 'Replaced']
1134
    states = dict((k, v) for k, v in enumerate(states_list))
1135

    
1136
    applicant = models.ForeignKey(
1137
        AstakosUser,
1138
        related_name='my_project_applications',
1139
        db_index=True)
1140
    owner = models.ForeignKey(
1141
        AstakosUser,
1142
        related_name='own_project_applications',
1143
        db_index=True
1144
    )
1145
    comments = models.TextField(null=True, blank=True)
1146
    definition = models.OneToOneField(ProjectDefinition)
1147
    issue_date = models.DateTimeField()
1148
    precursor_application = models.OneToOneField('ProjectApplication',
1149
        null=True,
1150
        blank=True,
1151
        db_index=True
1152
    )
1153
    
1154
    @property
1155
    def follower(self):
1156
        try:
1157
            return ProjectApplication.objects.get(precursor_application=self)
1158
        except ProjectApplication.DoesNotExist:
1159
            return
1160

    
1161
    def save(self):
1162
        self.definition.save()
1163
        self.definition = self.definition
1164
        super(ProjectApplication, self).save()
1165

    
1166
    @property
1167
    def status(self):
1168
        if self.follower:
1169
            try:
1170
                self.follower.project
1171
            except:
1172
                pass
1173
            else:
1174
                if self.follower.project.last_approval_date:
1175
                    return self.states[self.REPLACED]
1176
        try:
1177
            self.project
1178
        except Project.DoesNotExist:
1179
            return self.states[self.PENDING]
1180
        if self.project.is_alive:
1181
            return self.states[self.APPROVED]
1182
        
1183
    @staticmethod
1184
    def submit(definition, resource_policies, applicant, comments, precursor_application=None, commit=True):
1185
        application = None
1186
        if precursor_application:
1187
            try:
1188
                precursor_application.project
1189
            except:
1190
                pass
1191
            else:
1192
                if precursor_application.status != 'Pending':
1193
                    application = precursor_application
1194
                    application.precursor_application = precursor_application
1195
                    application.id = None
1196
                    print '>>>', application.precursor_application.id
1197
        if not application:
1198
            application = ProjectApplication(owner=applicant)
1199
        application.definition = definition
1200
        application.applicant = applicant
1201
        application.comments = comments
1202
        application.issue_date = datetime.now()
1203
        application.definition.id = None
1204
        application.id = None
1205
        if commit:
1206
            application.save()
1207
            application.definition.resource_policies = resource_policies
1208
        if applicant.is_superuser:
1209
            self.approve_application()
1210
#         else:
1211
#             notification = build_notification(
1212
#                 settings.SERVER_EMAIL,
1213
#                 [i[1] for i in settings.ADMINS],
1214
#                 _(GROUP_CREATION_SUBJECT) % {'group':application.definition.name},
1215
#                 _('An new project application identified by %(id)s has been submitted.') % application.__dict__
1216
#             )
1217
#             notification.send()
1218
        return application
1219
        
1220
    def approve(self, approval_user=None):
1221
        """
1222
        If approval_user then during owner membership acceptance
1223
        it is checked whether the request_user is eligible.
1224
        """
1225
        if self.status != self.states[self.PENDING]:
1226
            return
1227
        if not self.precursor_application:
1228
            kwargs = {
1229
                'application':self,
1230
                'creation_date':datetime.now(),
1231
                'last_approval_date':datetime.now(),
1232
            }
1233
            project = _create_object(Project, **kwargs)
1234
            project.accept_member(self.owner, approval_user)
1235
        else:
1236
            project = self.precursor_application.project
1237
            project.application = self
1238
            project.last_approval_date = datetime.now()
1239
            project.save()
1240

    
1241
#         notification = build_notification(
1242
#             settings.SERVER_EMAIL,
1243
#             [project.owner.email],
1244
#             _('Project application has been approved on %s alpha2 testing' % SITENAME),
1245
#             _('Your application request %(id)s has been apporved.')
1246
#         )
1247
#         notification.send()
1248

    
1249
        rejected = self.project.sync()
1250
        if rejected:
1251
            # revert to precursor
1252
            project.appication = app.precursor_application
1253
            if project.application:
1254
                project.last_approval_date = last_approval_date
1255
                project.save()
1256
            rejected = project.sync()
1257
            if rejected:
1258
                raise Exception(_(astakos_messages.QH_SYNC_ERROR))
1259
        else:
1260
            project.last_application_synced = app
1261
            project.save()
1262

    
1263

    
1264
class Project(models.Model):
1265
    application = models.OneToOneField(ProjectApplication, related_name='project')
1266
    creation_date = models.DateTimeField()
1267
    last_approval_date = models.DateTimeField(null=True)
1268
    termination_start_date = models.DateTimeField(null=True)
1269
    termination_date = models.DateTimeField(null=True)
1270
    members = models.ManyToManyField(AstakosUser, through='ProjectMembership')
1271
    membership_dirty = models.BooleanField(default=False)
1272
    last_application_synced = models.OneToOneField(
1273
        ProjectApplication, related_name='last_project', null=True, blank=True
1274
    )
1275
    
1276
    
1277
    @property
1278
    def definition(self):
1279
        return self.application.definition
1280

    
1281
    @property
1282
    def violated_members_number_limit(self):
1283
        return len(self.approved_members) <= self.definition.limit_on_members_number
1284
        
1285
    @property
1286
    def is_active(self):
1287
        if not self.last_approval_date:
1288
            return False
1289
        if self.termination_date:
1290
            return False
1291
        if self.definition.violated_resource_grants:
1292
            return False
1293
#         if self.violated_members_number_limit:
1294
#             return False
1295
        return True
1296
    
1297
    @property
1298
    def is_terminated(self):
1299
        if not self.termination_date:
1300
            return False
1301
        return True
1302
    
1303
    @property
1304
    def is_suspended(self):
1305
        if not self.termination_date:
1306
            return False
1307
        if not self.last_approval_date:
1308
            if not self.definition.violated_resource_grants:
1309
                return False
1310
#             if not self.violated_members_number_limit:
1311
#                 return False
1312
        return True
1313
    
1314
    @property
1315
    def is_alive(self):
1316
        return self.is_active or self.is_suspended
1317
    
1318
    @property
1319
    def is_inconsistent(self):
1320
        now = datetime.now()
1321
        if self.creation_date > now:
1322
            return True
1323
        if self.last_approval_date > now:
1324
            return True
1325
        if self.terminaton_date > now:
1326
            return True
1327
        return False
1328
    
1329
    @property
1330
    def is_synchronized(self):
1331
        return self.last_application_synced == self.application and \
1332
            not self.membership_dirty and \
1333
            (not self.termination_start_date or termination_date)
1334
    
1335
    @property
1336
    def approved_members(self):
1337
        return [m.person for m in self.projectmembership_set.filter(~Q(acceptance_date=None))]
1338
        
1339
    def sync(self, specific_members=()):
1340
        if self.is_synchronized:
1341
            return
1342
        members = specific_members or self.approved_members
1343
        c, rejected = send_quota(self.approved_members)
1344
        return rejected
1345
    
1346
    def accept_member(self, user, request_user=None):
1347
        if isinstance(user, int):
1348
            user = _lookup_object(AstakosUser, id=user)
1349
        if request_user and \
1350
            (not self.owner == request_user and not request_user.is_superuser):
1351
            raise Exception(_(astakos_messages.NOT_ALLOWED))
1352
        if not self.is_alive:
1353
            raise Exception(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1354
        if self.definition.member_join_policy == 'closed':
1355
            raise Exception(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
1356
        if len(self.approved_members) + 1 > self.definition.limit_on_members_number:
1357
            raise Exception(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
1358
        m, created = ProjectMembership.objects.get_or_create(
1359
            person=user, project=self
1360
        )
1361
        m.accept()
1362

    
1363
    def reject_member(self, user, request_user=None):
1364
        if isinstance(user, int):
1365
            user = _lookup_object(AstakosUser, id=user)
1366
        if request_user and \
1367
            (not self.owner == request_user and not request_user.is_superuser):
1368
            raise Exception(_(astakos_messages.NOT_ALLOWED))
1369
        if not self.is_alive:
1370
            raise Exception(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1371
        try:
1372
            m = ProjectMembership.objects.get(person=user, project=self)
1373
        except:
1374
            raise Exception(_(astakos_messages.NOT_MEMBERSHIP_REQUEST) % project.__dict__)
1375
        else:
1376
            m.reject()
1377
        
1378
    def remove_member(self, user, request_user=None):
1379
        if isinstance(user, int):
1380
            user = _lookup_object(AstakosUser, id=user)
1381
        if request_user and \
1382
            (not self.owner == request_user and not request_user.is_superuser):
1383
            raise Exception(_(astakos_messages.NOT_ALLOWED))
1384
        if not self.is_alive:
1385
            raise Exception(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
1386
        if self.definition.member_leave_policy == 'closed':
1387
            raise Exception(_(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED))
1388
        try:
1389
            m = ProjectMembership.objects.get(person=user, project=self)
1390
        except:
1391
            raise Exception(_(astakos_messages.NOT_MEMBERSHIP_REQUEST) % project.__dict__)
1392
        else:
1393
            m.remove()
1394
    
1395
    def terminate(self):
1396
        self.termination_start_date = datetime.now()
1397
        self.terminaton_date = None
1398
        self.save()
1399
        
1400
        rejected = self.sync()
1401
        if not rejected:
1402
            self.termination_start_date = None
1403
            self.terminaton_date = datetime.now()
1404
            self.save()
1405
            
1406
            notification = build_notification(
1407
                settings.SERVER_EMAIL,
1408
                [self.application.owner.email],
1409
                _('Project %(name)s has been terminated.') %  self.definition.__dict__,
1410
                _('Project %(name)s has been terminated.') %  self.definition.__dict__
1411
            )
1412
            notification.send()
1413

    
1414
    def suspend(self):
1415
        self.last_approval_date = None
1416
        self.save()
1417
        notification = build_notification(
1418
            settings.SERVER_EMAIL,
1419
            [self.application.owner.email],
1420
            _('Project %(name)s has been suspended.') %  self.definition.__dict__,
1421
            _('Project %(name)s has been suspended.') %  self.definition.__dict__
1422
        )
1423
        notification.send()
1424

    
1425
class ProjectMembership(models.Model):
1426
    person = models.ForeignKey(AstakosUser)
1427
    project = models.ForeignKey(Project)
1428
    request_date = models.DateField(default=datetime.now())
1429
    acceptance_date = models.DateField(null=True, db_index=True)
1430

    
1431
    class Meta:
1432
        unique_together = ("person", "project")
1433
    
1434
    def accept(self):
1435
        if self.acceptance_date:
1436
            return
1437
        self.acceptance_date = datetime.now()
1438
        self.save()
1439
#         notification = build_notification(
1440
#             settings.SERVER_EMAIL,
1441
#             [self.person.email],
1442
#             _('Your membership on project %(name)s has been accepted.') % self.project.definition.__dict__,
1443
#             _('Your membership on project %(name)s has been accepted.') % self.project.definition.__dict__
1444
#         ).send()
1445
        self.sync()
1446
    
1447
    def reject(self):
1448
        history_item = ProjectMembershipHistory(
1449
            person=self.person,
1450
            project=self.project,
1451
            request_date=self.request_date,
1452
            rejection_date=datetime.now()
1453
        ).save()
1454
        self.delete()
1455
#         notification = build_notification(
1456
#             settings.SERVER_EMAIL,
1457
#             [self.person.email],
1458
#             _('Your membership on project %(name)s has been rejected.') % self.project.definition.__dict__,
1459
#             _('Your membership on project %(name)s has been rejected.') % self.project.definition.__dict__
1460
#         ).send()
1461
    
1462
    def remove(self):
1463
        history_item = ProjectMembershipHistory(
1464
            id=self.id,
1465
            person=self.person,
1466
            project=self.project,
1467
            request_date=self.request_date,
1468
            removal_date=datetime.now()
1469
        ).save()
1470
        self.delete()
1471
#         notification = build_notification(
1472
#             settings.SERVER_EMAIL,
1473
#             [self.person.email],
1474
#             _('Your membership on project %(name)s has been removed.') % self.project.definition.__dict__,
1475
#             _('Your membership on project %(name)s has been removed.') % self.project.definition.__dict__
1476
#         ).send()
1477
        self.sync()
1478
    
1479
    def sync(self):
1480
        # set membership_dirty flag
1481
        self.project.membership_dirty = True
1482
        self.project.save()
1483
        
1484
        rejected = self.project.sync(specific_members=[self])
1485
        if not rejected:
1486
            # if syncing was successful unset membership_dirty flag
1487
            self.membership_dirty = False
1488
            self.save()
1489
        
1490

    
1491
class ProjectMembershipHistory(models.Model):
1492
    person = models.ForeignKey(AstakosUser)
1493
    project = models.ForeignKey(Project)
1494
    request_date = models.DateField(default=datetime.now())
1495
    removal_date = models.DateField(null=True)
1496
    rejection_date = models.DateField(null=True)
1497

    
1498
    class Meta:
1499
        unique_together = ("person", "project")
1500

    
1501

    
1502
def filter_queryset_by_property(q, property):
1503
    """
1504
    Incorporate list comprehension for filtering querysets by property
1505
    since Queryset.filter() operates on the database level.
1506
    """
1507
    return (p for p in q if getattr(p, property, False))
1508

    
1509
def get_alive_projects():
1510
    return filter_queryset_by_property(
1511
        Project.objects.all(),
1512
        'is_alive'
1513
    )
1514

    
1515
def get_active_projects():
1516
    return filter_queryset_by_property(
1517
        Project.objects.all(),
1518
        'is_active'
1519
    )
1520

    
1521
def _lookup_object(model, **kwargs):
1522
    """
1523
    Returns an object of the specific model matching the given lookup
1524
    parameters.
1525
    """
1526
    if not kwargs:
1527
        raise MissingIdentifier
1528
    return model.objects.get(**kwargs)
1529

    
1530
def _create_object(model, **kwargs):
1531
    o = model.objects.create(**kwargs)
1532
    o.save()
1533
    return o
1534

    
1535
def _update_object(model, id, save=True, **kwargs):
1536
    o = self._lookup_object(model, id=id)
1537
    if kwargs:
1538
        o.__dict__.update(kwargs)
1539
    if save:
1540
        o.save()
1541
    return o
1542

    
1543
# def list_applications():
1544
#     return ProjectApplication.objects.all()
1545
# 
1546
# 
1547
# def list_projects(filter_property=None):
1548
#     if filter_property:
1549
#         return filter_queryset_by_property(
1550
#             Project.objects.all(),
1551
#             filter_property
1552
#         )
1553
#     return Project.objects.all()
1554
# 
1555
# 
1556
# def synchonize_project(id):
1557
#     project = _lookup_object(Project, id=id)
1558
#     return project.sync()
1559

    
1560

    
1561
def create_astakos_user(u):
1562
    try:
1563
        AstakosUser.objects.get(user_ptr=u.pk)
1564
    except AstakosUser.DoesNotExist:
1565
        extended_user = AstakosUser(user_ptr_id=u.pk)
1566
        extended_user.__dict__.update(u.__dict__)
1567
        extended_user.save()
1568
        if not extended_user.has_auth_provider('local'):
1569
            extended_user.add_auth_provider('local')
1570
    except BaseException, e:
1571
        logger.exception(e)
1572

    
1573

    
1574
def fix_superusers(sender, **kwargs):
1575
    # Associate superusers with AstakosUser
1576
    admins = User.objects.filter(is_superuser=True)
1577
    for u in admins:
1578
        create_astakos_user(u)
1579

    
1580

    
1581
def user_post_save(sender, instance, created, **kwargs):
1582
    if not created:
1583
        return
1584
    create_astakos_user(instance)
1585

    
1586

    
1587
def set_default_group(user):
1588
    try:
1589
        default = AstakosGroup.objects.get(name='default')
1590
        Membership(
1591
            group=default, person=user, date_joined=datetime.now()).save()
1592
    except AstakosGroup.DoesNotExist, e:
1593
        logger.exception(e)
1594

    
1595

    
1596
def astakosuser_pre_save(sender, instance, **kwargs):
1597
    instance.aquarium_report = False
1598
    instance.new = False
1599
    try:
1600
        db_instance = AstakosUser.objects.get(id=instance.id)
1601
    except AstakosUser.DoesNotExist:
1602
        # create event
1603
        instance.aquarium_report = True
1604
        instance.new = True
1605
    else:
1606
        get = AstakosUser.__getattribute__
1607
        l = filter(lambda f: get(db_instance, f) != get(instance, f),
1608
                   BILLING_FIELDS)
1609
        instance.aquarium_report = True if l else False
1610

    
1611

    
1612
def astakosuser_post_save(sender, instance, created, **kwargs):
1613
    if instance.aquarium_report:
1614
        report_user_event(instance, create=instance.new)
1615
    if not created:
1616
        return
1617
    set_default_group(instance)
1618
    # TODO handle socket.error & IOError
1619
    register_users((instance,))
1620

    
1621

    
1622
def resource_post_save(sender, instance, created, **kwargs):
1623
    if not created:
1624
        return
1625
    register_resources((instance,))
1626

    
1627

    
1628
def send_quota_disturbed(sender, instance, **kwargs):
1629
    users = []
1630
    extend = users.extend
1631
    if sender == Membership:
1632
        if not instance.group.is_enabled:
1633
            return
1634
        extend([instance.person])
1635
    elif sender == AstakosUserQuota:
1636
        extend([instance.user])
1637
    elif sender == AstakosGroupQuota:
1638
        if not instance.group.is_enabled:
1639
            return
1640
        extend(instance.group.astakosuser_set.all())
1641
    elif sender == AstakosGroup:
1642
        if not instance.is_enabled:
1643
            return
1644
    quota_disturbed.send(sender=sender, users=users)
1645

    
1646

    
1647
def on_quota_disturbed(sender, users, **kwargs):
1648
#     print '>>>', locals()
1649
    if not users:
1650
        return
1651
    send_quota(users)
1652

    
1653
def renew_token(sender, instance, **kwargs):
1654
    if not instance.auth_token:
1655
        instance.renew_token()
1656

    
1657
post_syncdb.connect(fix_superusers)
1658
post_save.connect(user_post_save, sender=User)
1659
pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
1660
post_save.connect(astakosuser_post_save, sender=AstakosUser)
1661
post_save.connect(resource_post_save, sender=Resource)
1662

    
1663
quota_disturbed = Signal(providing_args=["users"])
1664
quota_disturbed.connect(on_quota_disturbed)
1665

    
1666
post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
1667
post_delete.connect(send_quota_disturbed, sender=Membership)
1668
post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
1669
post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
1670
post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1671
post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
1672

    
1673
pre_save.connect(renew_token, sender=AstakosUser)
1674
pre_save.connect(renew_token, sender=Service)