Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 1b27f8a2

History | View | Annotate | Download (74.2 kB)

1
# Copyright 2011, 2012, 2013 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
import math
39
import copy
40

    
41
from datetime import datetime, timedelta
42
import base64
43
from urllib import quote
44
from random import randint
45
import os
46

    
47
from django.db import models, IntegrityError, transaction
48
from django.contrib.auth.models import User, UserManager, Group, Permission
49
from django.utils.translation import ugettext as _
50
from django.db.models.signals import pre_save, post_save
51
from django.contrib.contenttypes.models import ContentType
52

    
53
from django.db.models import Q, Max
54
from django.core.urlresolvers import reverse
55
from django.utils.http import int_to_base36
56
from django.contrib.auth.tokens import default_token_generator
57
from django.conf import settings
58
from django.utils.importlib import import_module
59
from django.utils.safestring import mark_safe
60

    
61
from synnefo.lib.utils import dict_merge
62

    
63
from astakos.im import settings as astakos_settings
64
from astakos.im import auth_providers as auth
65

    
66
import astakos.im.messages as astakos_messages
67
from snf_django.lib.db.managers import ForUpdateManager
68
from synnefo.lib.ordereddict import OrderedDict
69

    
70
from snf_django.lib.db.fields import intDecimalField
71
from synnefo.util.text import uenc, udec
72
from astakos.im import presentation
73

    
74
logger = logging.getLogger(__name__)
75

    
76
DEFAULT_CONTENT_TYPE = None
77
_content_type = None
78

    
79

    
80
def get_content_type():
81
    global _content_type
82
    if _content_type is not None:
83
        return _content_type
84

    
85
    try:
86
        content_type = ContentType.objects.get(app_label='im',
87
                                               model='astakosuser')
88
    except:
89
        content_type = DEFAULT_CONTENT_TYPE
90
    _content_type = content_type
91
    return content_type
92

    
93
inf = float('inf')
94

    
95

    
96
def generate_token():
97
    s = os.urandom(32)
98
    return base64.urlsafe_b64encode(s).rstrip('=')
99

    
100

    
101
class Component(models.Model):
102
    name = models.CharField(_('Name'), max_length=255, unique=True,
103
                            db_index=True)
104
    url = models.CharField(_('Component url'), max_length=1024, null=True,
105
                           help_text=_("URL the component is accessible from"))
106
    auth_token = models.CharField(_('Authentication Token'), max_length=64,
107
                                  null=True, blank=True, unique=True)
108
    auth_token_created = models.DateTimeField(_('Token creation date'),
109
                                              null=True)
110
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
111
                                              null=True)
112

    
113
    def renew_token(self, expiration_date=None):
114
        for i in range(10):
115
            new_token = generate_token()
116
            count = Component.objects.filter(auth_token=new_token).count()
117
            if count == 0:
118
                break
119
            continue
120
        else:
121
            raise ValueError('Could not generate a token')
122

    
123
        self.auth_token = new_token
124
        self.auth_token_created = datetime.now()
125
        if expiration_date:
126
            self.auth_token_expires = expiration_date
127
        else:
128
            self.auth_token_expires = None
129
        msg = 'Token renewed for component %s' % self.name
130
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
131

    
132
    def __str__(self):
133
        return self.name
134

    
135
    @classmethod
136
    def catalog(cls, orderfor=None):
137
        catalog = {}
138
        components = list(cls.objects.all())
139
        default_metadata = presentation.COMPONENTS
140
        metadata = {}
141

    
142
        for component in components:
143
            d = {'url': component.url,
144
                 'name': component.name}
145
            if component.name in default_metadata:
146
                metadata[component.name] = default_metadata.get(component.name)
147
                metadata[component.name].update(d)
148
            else:
149
                metadata[component.name] = d
150

    
151

    
152
        def component_by_order(s):
153
            return s[1].get('order')
154

    
155
        def component_by_dashboard_order(s):
156
            return s[1].get('dashboard').get('order')
157

    
158
        metadata = dict_merge(metadata,
159
                              astakos_settings.COMPONENTS_META)
160

    
161
        for component, info in metadata.iteritems():
162
            default_meta = presentation.component_defaults(component)
163
            base_meta = metadata.get(component, {})
164
            settings_meta = astakos_settings.COMPONENTS_META.get(component, {})
165
            component_meta = dict_merge(default_meta, base_meta)
166
            meta = dict_merge(component_meta, settings_meta)
167
            catalog[component] = meta
168

    
169
        order_key = component_by_order
170
        if orderfor == 'dashboard':
171
            order_key = component_by_dashboard_order
172

    
173
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
174
                                             key=order_key))
175
        return ordered_catalog
176

    
177

    
178
_presentation_data = {}
179

    
180

    
181
def get_presentation(resource):
182
    global _presentation_data
183
    resource_presentation = _presentation_data.get(resource, {})
184
    if not resource_presentation:
185
        resources_presentation = presentation.RESOURCES.get('resources', {})
186
        resource_presentation = resources_presentation.get(resource, {})
187
        _presentation_data[resource] = resource_presentation
188
    return resource_presentation
189

    
190

    
191
class Service(models.Model):
192
    component = models.ForeignKey(Component)
193
    name = models.CharField(max_length=255, unique=True)
194
    type = models.CharField(max_length=255)
195

    
196

    
197
class Endpoint(models.Model):
198
    service = models.ForeignKey(Service, related_name='endpoints')
199

    
200

    
201
class EndpointData(models.Model):
202
    endpoint = models.ForeignKey(Endpoint, related_name='data')
203
    key = models.CharField(max_length=255)
204
    value = models.CharField(max_length=1024)
205

    
206
    class Meta:
207
        unique_together = (('endpoint', 'key'),)
208

    
209

    
210
class Resource(models.Model):
211
    name = models.CharField(_('Name'), max_length=255, unique=True)
212
    desc = models.TextField(_('Description'), null=True)
213
    service_type = models.CharField(_('Type'), max_length=255)
214
    service_origin = models.CharField(max_length=255, db_index=True)
215
    unit = models.CharField(_('Unit'), null=True, max_length=255)
216
    uplimit = intDecimalField(default=0)
217
    allow_in_projects = models.BooleanField(default=True)
218

    
219
    objects = ForUpdateManager()
220

    
221
    def __str__(self):
222
        return self.name
223

    
224
    def full_name(self):
225
        return str(self)
226

    
227
    def get_info(self):
228
        return {'service': self.service_origin,
229
                'description': self.desc,
230
                'unit': self.unit,
231
                'allow_in_projects': self.allow_in_projects,
232
                }
233

    
234
    @property
235
    def group(self):
236
        default = self.name
237
        return get_presentation(str(self)).get('group', default)
238

    
239
    @property
240
    def help_text(self):
241
        default = "%s resource" % self.name
242
        return get_presentation(str(self)).get('help_text', default)
243

    
244
    @property
245
    def help_text_input_each(self):
246
        default = "%s resource" % self.name
247
        return get_presentation(str(self)).get('help_text_input_each', default)
248

    
249
    @property
250
    def is_abbreviation(self):
251
        return get_presentation(str(self)).get('is_abbreviation', False)
252

    
253
    @property
254
    def report_desc(self):
255
        default = "%s resource" % self.name
256
        return get_presentation(str(self)).get('report_desc', default)
257

    
258
    @property
259
    def placeholder(self):
260
        return get_presentation(str(self)).get('placeholder', self.unit)
261

    
262
    @property
263
    def verbose_name(self):
264
        return get_presentation(str(self)).get('verbose_name', self.name)
265

    
266
    @property
267
    def display_name(self):
268
        name = self.verbose_name
269
        if self.is_abbreviation:
270
            name = name.upper()
271
        return name
272

    
273
    @property
274
    def pluralized_display_name(self):
275
        if not self.unit:
276
            return '%ss' % self.display_name
277
        return self.display_name
278

    
279
def get_resource_names():
280
    _RESOURCE_NAMES = []
281
    resources = Resource.objects.select_related('service').all()
282
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
283
    return _RESOURCE_NAMES
284

    
285

    
286
class AstakosUserManager(UserManager):
287

    
288
    def get_auth_provider_user(self, provider, **kwargs):
289
        """
290
        Retrieve AstakosUser instance associated with the specified third party
291
        id.
292
        """
293
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
294
                          kwargs.iteritems()))
295
        return self.get(auth_providers__module=provider, **kwargs)
296

    
297
    def get_by_email(self, email):
298
        return self.get(email=email)
299

    
300
    def get_by_identifier(self, email_or_username, **kwargs):
301
        try:
302
            return self.get(email__iexact=email_or_username, **kwargs)
303
        except AstakosUser.DoesNotExist:
304
            return self.get(username__iexact=email_or_username, **kwargs)
305

    
306
    def user_exists(self, email_or_username, **kwargs):
307
        qemail = Q(email__iexact=email_or_username)
308
        qusername = Q(username__iexact=email_or_username)
309
        qextra = Q(**kwargs)
310
        return self.filter((qemail | qusername) & qextra).exists()
311

    
312
    def verified_user_exists(self, email_or_username):
313
        return self.user_exists(email_or_username, email_verified=True)
314

    
315
    def verified(self):
316
        return self.filter(email_verified=True)
317

    
318
    def moderated(self):
319
        return self.filter(moderated=True)
320

    
321
    def uuid_catalog(self, l=None):
322
        """
323
        Returns a uuid to username mapping for the uuids appearing in l.
324
        If l is None returns the mapping for all existing users.
325
        """
326
        q = self.filter(uuid__in=l) if l != None else self
327
        return dict(q.values_list('uuid', 'username'))
328

    
329
    def displayname_catalog(self, l=None):
330
        """
331
        Returns a username to uuid mapping for the usernames appearing in l.
332
        If l is None returns the mapping for all existing users.
333
        """
334
        if l is not None:
335
            lmap = dict((x.lower(), x) for x in l)
336
            q = self.filter(username__in=lmap.keys())
337
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
338
        else:
339
            q = self
340
            values = self.values_list('username', 'uuid')
341
        return dict(values)
342

    
343

    
344

    
345
class AstakosUser(User):
346
    """
347
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
348
    """
349
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
350
                                   null=True)
351

    
352
    #for invitations
353
    user_level = astakos_settings.DEFAULT_USER_LEVEL
354
    level = models.IntegerField(_('Inviter level'), default=user_level)
355
    invitations = models.IntegerField(
356
        _('Invitations left'), default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
357

    
358
    auth_token = models.CharField(_('Authentication Token'),
359
                                  max_length=64,
360
                                  unique=True,
361
                                  null=True,
362
                                  blank=True,
363
                                  help_text = _('Renew your authentication '
364
                                                'token. Make sure to set the new '
365
                                                'token in any client you may be '
366
                                                'using, to preserve its '
367
                                                'functionality.'))
368
    auth_token_created = models.DateTimeField(_('Token creation date'),
369
                                              null=True)
370
    auth_token_expires = models.DateTimeField(
371
        _('Token expiration date'), null=True)
372

    
373
    updated = models.DateTimeField(_('Update date'))
374

    
375
    # Arbitrary text to identify the reason user got deactivated.
376
    # To be used as a reference from administrators.
377
    deactivated_reason = models.TextField(
378
        _('Reason the user was disabled for'),
379
        default=None, null=True)
380
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
381
                                          blank=True)
382

    
383
    has_credits = models.BooleanField(_('Has credits?'), default=False)
384

    
385
    # this is set to True when user profile gets updated for the first time
386
    is_verified = models.BooleanField(_('Is verified?'), default=False)
387

    
388
    # user email is verified
389
    email_verified = models.BooleanField(_('Email verified?'), default=False)
390

    
391
    # unique string used in user email verification url
392
    verification_code = models.CharField(max_length=255, null=True,
393
                                         blank=False, unique=True)
394

    
395
    # date user email verified
396
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
397
                                       blank=True)
398

    
399
    # email verification notice was sent to the user at this time
400
    activation_sent = models.DateTimeField(_('Activation sent date'),
401
                                           null=True, blank=True)
402

    
403
    # user got rejected during moderation process
404
    is_rejected = models.BooleanField(_('Account rejected'),
405
                                      default=False)
406
    # reason user got rejected
407
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
408
                                       blank=True)
409
    # moderation status
410
    moderated = models.BooleanField(_('User moderated'), default=False)
411
    # date user moderated (either accepted or rejected)
412
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
413
                                        blank=True, null=True)
414
    # a snapshot of user instance the time got moderated
415
    moderated_data = models.TextField(null=True, default=None, blank=True)
416
    # a string which identifies how the user got moderated
417
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
418
                                       default=None, null=True, blank=True)
419
    # the email used to accept the user
420
    accepted_email = models.EmailField(null=True, default=None, blank=True)
421

    
422
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
423
                                           default=False)
424
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
425
                                             null=True, blank=True)
426
    # permanent unique user identifier
427
    uuid = models.CharField(max_length=255, null=True, blank=False,
428
                            unique=True)
429

    
430
    policy = models.ManyToManyField(
431
        Resource, null=True, through='AstakosUserQuota')
432

    
433
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
434
                                           default=False, db_index=True)
435

    
436
    objects = AstakosUserManager()
437
    forupdate = ForUpdateManager()
438

    
439
    def __init__(self, *args, **kwargs):
440
        super(AstakosUser, self).__init__(*args, **kwargs)
441
        if not self.id:
442
            self.is_active = False
443

    
444
    @property
445
    def realname(self):
446
        return '%s %s' % (self.first_name, self.last_name)
447

    
448
    @property
449
    def log_display(self):
450
        """
451
        Should be used in all logger.* calls that refer to a user so that
452
        user display is consistent across log entries.
453
        """
454
        return '%s::%s' % (self.uuid, self.email)
455

    
456
    @realname.setter
457
    def realname(self, value):
458
        parts = value.split(' ')
459
        if len(parts) == 2:
460
            self.first_name = parts[0]
461
            self.last_name = parts[1]
462
        else:
463
            self.last_name = parts[0]
464

    
465
    def add_permission(self, pname):
466
        if self.has_perm(pname):
467
            return
468
        p, created = Permission.objects.get_or_create(
469
                                    codename=pname,
470
                                    name=pname.capitalize(),
471
                                    content_type=get_content_type())
472
        self.user_permissions.add(p)
473

    
474
    def remove_permission(self, pname):
475
        if self.has_perm(pname):
476
            return
477
        p = Permission.objects.get(codename=pname,
478
                                   content_type=get_content_type())
479
        self.user_permissions.remove(p)
480

    
481
    def add_group(self, gname):
482
        group, _ = Group.objects.get_or_create(name=gname)
483
        self.groups.add(group)
484

    
485
    def is_project_admin(self, application_id=None):
486
        return self.uuid in astakos_settings.PROJECT_ADMINS
487

    
488
    @property
489
    def invitation(self):
490
        try:
491
            return Invitation.objects.get(username=self.email)
492
        except Invitation.DoesNotExist:
493
            return None
494

    
495
    @property
496
    def policies(self):
497
        return self.astakosuserquota_set.select_related().all()
498

    
499
    def get_resource_policy(self, resource):
500
        resource = Resource.objects.get(name=resource)
501
        default_capacity = resource.uplimit
502
        try:
503
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
504
            return policy, default_capacity
505
        except AstakosUserQuota.DoesNotExist:
506
            return None, default_capacity
507

    
508
    def update_uuid(self):
509
        while not self.uuid:
510
            uuid_val = str(uuid.uuid4())
511
            try:
512
                AstakosUser.objects.get(uuid=uuid_val)
513
            except AstakosUser.DoesNotExist, e:
514
                self.uuid = uuid_val
515
        return self.uuid
516

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

    
523
        self.update_uuid()
524

    
525
        if not self.verification_code:
526
            self.renew_verification_code()
527

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

    
532
        super(AstakosUser, self).save(**kwargs)
533

    
534
    def renew_verification_code(self):
535
        self.verification_code = str(uuid.uuid4())
536
        logger.info("Verification code renewed for %s" % self.log_display)
537

    
538
    def renew_token(self, flush_sessions=False, current_key=None):
539
        for i in range(10):
540
            new_token = generate_token()
541
            count = AstakosUser.objects.filter(auth_token=new_token).count()
542
            if count == 0:
543
                break
544
            continue
545
        else:
546
            raise ValueError('Could not generate a token')
547

    
548
        self.auth_token = new_token
549
        self.auth_token_created = datetime.now()
550
        self.auth_token_expires = self.auth_token_created + \
551
                                  timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
552
        if flush_sessions:
553
            self.flush_sessions(current_key)
554
        msg = 'Token renewed for %s' % self.log_display
555
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
556

    
557
    def token_expired(self):
558
        return self.auth_token_expires < datetime.now()
559

    
560
    def flush_sessions(self, current_key=None):
561
        q = self.sessions
562
        if current_key:
563
            q = q.exclude(session_key=current_key)
564

    
565
        keys = q.values_list('session_key', flat=True)
566
        if keys:
567
            msg = 'Flushing sessions: %s' % ','.join(keys)
568
            logger.log(astakos_settings.LOGGING_LEVEL, msg, [])
569
        engine = import_module(settings.SESSION_ENGINE)
570
        for k in keys:
571
            s = engine.SessionStore(k)
572
            s.flush()
573

    
574
    def __unicode__(self):
575
        return '%s (%s)' % (self.realname, self.email)
576

    
577
    def conflicting_email(self):
578
        q = AstakosUser.objects.exclude(username=self.username)
579
        q = q.filter(email__iexact=self.email)
580
        if q.count() != 0:
581
            return True
582
        return False
583

    
584
    def email_change_is_pending(self):
585
        return self.emailchanges.count() > 0
586

    
587
    @property
588
    def status_display(self):
589
        msg = ""
590
        append = None
591
        if self.is_active:
592
            msg = "Accepted/Active"
593
        if self.is_rejected:
594
            msg = "Rejected"
595
            if self.rejected_reason:
596
                msg += " (%s)" % self.rejected_reason
597
        if not self.email_verified:
598
            msg = "Pending email verification"
599
        if not self.moderated:
600
            msg = "Pending moderation"
601
        if not self.is_active and self.email_verified:
602
            msg = "Accepted/Inactive"
603
            if self.deactivated_reason:
604
                msg += " (%s)" % (self.deactivated_reason)
605

    
606
        if self.moderated and not self.is_rejected:
607
            if self.accepted_policy == 'manual':
608
                msg += " (manually accepted)"
609
            else:
610
                msg += " (accepted policy: %s)" % \
611
                        self.accepted_policy
612
        return msg
613

    
614
    @property
615
    def signed_terms(self):
616
        term = get_latest_terms()
617
        if not term:
618
            return True
619
        if not self.has_signed_terms:
620
            return False
621
        if not self.date_signed_terms:
622
            return False
623
        if self.date_signed_terms < term.date:
624
            self.has_signed_terms = False
625
            self.date_signed_terms = None
626
            self.save()
627
            return False
628
        return True
629

    
630
    def set_invitations_level(self):
631
        """
632
        Update user invitation level
633
        """
634
        level = self.invitation.inviter.level + 1
635
        self.level = level
636
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
637

    
638
    def can_change_password(self):
639
        return self.has_auth_provider('local', auth_backend='astakos')
640

    
641
    def can_change_email(self):
642
        if not self.has_auth_provider('local'):
643
            return True
644

    
645
        local = self.get_auth_provider('local')._instance
646
        return local.auth_backend == 'astakos'
647

    
648
    # Auth providers related methods
649
    def get_auth_provider(self, module=None, identifier=None, **filters):
650
        if not module:
651
            return self.auth_providers.active()[0].settings
652

    
653
        params = {'module': module}
654
        if identifier:
655
            params['identifier'] = identifier
656
        params.update(filters)
657
        return self.auth_providers.active().get(**params).settings
658

    
659
    def has_auth_provider(self, provider, **kwargs):
660
        return bool(self.auth_providers.active().filter(module=provider,
661
                                                        **kwargs).count())
662

    
663
    def get_required_providers(self, **kwargs):
664
        return auth.REQUIRED_PROVIDERS.keys()
665

    
666
    def missing_required_providers(self):
667
        required = self.get_required_providers()
668
        missing = []
669
        for provider in required:
670
            if not self.has_auth_provider(provider):
671
                missing.append(auth.get_provider(provider, self))
672
        return missing
673

    
674
    def get_available_auth_providers(self, **filters):
675
        """
676
        Returns a list of providers available for add by the user.
677
        """
678
        modules = astakos_settings.IM_MODULES
679
        providers = []
680
        for p in modules:
681
            providers.append(auth.get_provider(p, self))
682
        available = []
683

    
684
        for p in providers:
685
            if p.get_add_policy:
686
                available.append(p)
687
        return available
688

    
689
    def get_disabled_auth_providers(self, **filters):
690
        providers = self.get_auth_providers(**filters)
691
        disabled = []
692
        for p in providers:
693
            if not p.get_login_policy:
694
                disabled.append(p)
695
        return disabled
696

    
697
    def get_enabled_auth_providers(self, **filters):
698
        providers = self.get_auth_providers(**filters)
699
        enabled = []
700
        for p in providers:
701
            if p.get_login_policy:
702
                enabled.append(p)
703
        return enabled
704

    
705
    def get_auth_providers(self, **filters):
706
        providers = []
707
        for provider in self.auth_providers.active(**filters):
708
            if provider.settings.module_enabled:
709
                providers.append(provider.settings)
710

    
711
        modules = astakos_settings.IM_MODULES
712

    
713
        def key(p):
714
            if not p.module in modules:
715
                return 100
716
            return modules.index(p.module)
717

    
718
        providers = sorted(providers, key=key)
719
        return providers
720

    
721
    # URL methods
722
    @property
723
    def auth_providers_display(self):
724
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
725
                         self.get_enabled_auth_providers()])
726

    
727
    def add_auth_provider(self, module='local', identifier=None, **params):
728
        provider = auth.get_provider(module, self, identifier, **params)
729
        provider.add_to_user()
730

    
731
    def get_resend_activation_url(self):
732
        return reverse('send_activation', kwargs={'user_id': self.pk})
733

    
734
    def get_activation_url(self, nxt=False):
735
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
736
                                 quote(self.verification_code))
737
        if nxt:
738
            url += "&next=%s" % quote(nxt)
739
        return url
740

    
741
    def get_password_reset_url(self, token_generator=default_token_generator):
742
        return reverse('astakos.im.views.target.local.password_reset_confirm',
743
                          kwargs={'uidb36':int_to_base36(self.id),
744
                                  'token':token_generator.make_token(self)})
745

    
746
    def get_inactive_message(self, provider_module, identifier=None):
747
        provider = self.get_auth_provider(provider_module, identifier)
748

    
749
        msg_extra = ''
750
        message = ''
751

    
752
        msg_inactive = provider.get_account_inactive_msg
753
        msg_pending = provider.get_pending_activation_msg
754
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
755
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
756
        msg_pending_mod = provider.get_pending_moderation_msg
757
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
758
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
759

    
760
        if not self.email_verified:
761
            message = msg_pending
762
            url = self.get_resend_activation_url()
763
            msg_extra = msg_pending_help + \
764
                        u' ' + \
765
                        '<a href="%s">%s?</a>' % (url, msg_resend)
766
        else:
767
            if not self.moderated:
768
                message = msg_pending_mod
769
            else:
770
                if self.is_rejected:
771
                    message = msg_rejected
772
                else:
773
                    message = msg_inactive
774

    
775
        return mark_safe(message + u' ' + msg_extra)
776

    
777
    def owns_application(self, application):
778
        return application.owner == self
779

    
780
    def owns_project(self, project):
781
        return project.application.owner == self
782

    
783
    def is_associated(self, project):
784
        try:
785
            m = ProjectMembership.objects.get(person=self, project=project)
786
            return m.state in ProjectMembership.ASSOCIATED_STATES
787
        except ProjectMembership.DoesNotExist:
788
            return False
789

    
790
    def get_membership(self, project):
791
        try:
792
            return ProjectMembership.objects.get(
793
                project=project,
794
                person=self)
795
        except ProjectMembership.DoesNotExist:
796
            return None
797

    
798
    def membership_display(self, project):
799
        m = self.get_membership(project)
800
        if m is None:
801
            return _('Not a member')
802
        else:
803
            return m.user_friendly_state_display()
804

    
805
    def non_owner_can_view(self, maybe_project):
806
        if self.is_project_admin():
807
            return True
808
        if maybe_project is None:
809
            return False
810
        project = maybe_project
811
        if self.is_associated(project):
812
            return True
813
        if project.is_deactivated():
814
            return False
815
        return True
816

    
817

    
818
class AstakosUserAuthProviderManager(models.Manager):
819

    
820
    def active(self, **filters):
821
        return self.filter(active=True, **filters)
822

    
823
    def remove_unverified_providers(self, provider, **filters):
824
        try:
825
            existing = self.filter(module=provider, user__email_verified=False,
826
                                   **filters)
827
            for p in existing:
828
                p.user.delete()
829
        except:
830
            pass
831

    
832
    def unverified(self, provider, **filters):
833
        try:
834
            return self.get(module=provider, user__email_verified=False,
835
                            **filters).settings
836
        except AstakosUserAuthProvider.DoesNotExist:
837
            return None
838

    
839
    def verified(self, provider, **filters):
840
        try:
841
            return self.get(module=provider, user__email_verified=True,
842
                            **filters).settings
843
        except AstakosUserAuthProvider.DoesNotExist:
844
            return None
845

    
846

    
847
class AuthProviderPolicyProfileManager(models.Manager):
848

    
849
    def active(self):
850
        return self.filter(active=True)
851

    
852
    def for_user(self, user, provider):
853
        policies = {}
854
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
855
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
856
        exclusive_q = exclusive_q1 | exclusive_q2
857

    
858
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
859
            policies.update(profile.policies)
860

    
861
        user_groups = user.groups.all().values('pk')
862
        for profile in self.active().filter(groups__in=user_groups).filter(
863
                exclusive_q):
864
            policies.update(profile.policies)
865
        return policies
866

    
867
    def add_policy(self, name, provider, group_or_user, exclusive=False,
868
                   **policies):
869
        is_group = isinstance(group_or_user, Group)
870
        profile, created = self.get_or_create(name=name, provider=provider,
871
                                              is_exclusive=exclusive)
872
        profile.is_exclusive = exclusive
873
        profile.save()
874
        if is_group:
875
            profile.groups.add(group_or_user)
876
        else:
877
            profile.users.add(group_or_user)
878
        profile.set_policies(policies)
879
        profile.save()
880
        return profile
881

    
882

    
883
class AuthProviderPolicyProfile(models.Model):
884
    name = models.CharField(_('Name'), max_length=255, blank=False,
885
                            null=False, db_index=True)
886
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
887
                                null=False)
888

    
889
    # apply policies to all providers excluding the one set in provider field
890
    is_exclusive = models.BooleanField(default=False)
891

    
892
    policy_add = models.NullBooleanField(null=True, default=None)
893
    policy_remove = models.NullBooleanField(null=True, default=None)
894
    policy_create = models.NullBooleanField(null=True, default=None)
895
    policy_login = models.NullBooleanField(null=True, default=None)
896
    policy_limit = models.IntegerField(null=True, default=None)
897
    policy_required = models.NullBooleanField(null=True, default=None)
898
    policy_automoderate = models.NullBooleanField(null=True, default=None)
899
    policy_switch = models.NullBooleanField(null=True, default=None)
900

    
901
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
902
                     'automoderate')
903

    
904
    priority = models.IntegerField(null=False, default=1)
905
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
906
    users = models.ManyToManyField(AstakosUser,
907
                                   related_name='authpolicy_profiles')
908
    active = models.BooleanField(default=True)
909

    
910
    objects = AuthProviderPolicyProfileManager()
911

    
912
    class Meta:
913
        ordering = ['priority']
914

    
915
    @property
916
    def policies(self):
917
        policies = {}
918
        for pkey in self.POLICY_FIELDS:
919
            value = getattr(self, 'policy_%s' % pkey, None)
920
            if value is None:
921
                continue
922
            policies[pkey] = value
923
        return policies
924

    
925
    def set_policies(self, policies_dict):
926
        for key, value in policies_dict.iteritems():
927
            if key in self.POLICY_FIELDS:
928
                setattr(self, 'policy_%s' % key, value)
929
        return self.policies
930

    
931

    
932
class AstakosUserAuthProvider(models.Model):
933
    """
934
    Available user authentication methods.
935
    """
936
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
937
                                   null=True, default=None)
938
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
939
    module = models.CharField(_('Provider'), max_length=255, blank=False,
940
                                default='local')
941
    identifier = models.CharField(_('Third-party identifier'),
942
                                              max_length=255, null=True,
943
                                              blank=True)
944
    active = models.BooleanField(default=True)
945
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
946
                                   default='astakos')
947
    info_data = models.TextField(default="", null=True, blank=True)
948
    created = models.DateTimeField('Creation date', auto_now_add=True)
949

    
950
    objects = AstakosUserAuthProviderManager()
951

    
952
    class Meta:
953
        unique_together = (('identifier', 'module', 'user'), )
954
        ordering = ('module', 'created')
955

    
956
    def __init__(self, *args, **kwargs):
957
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
958
        try:
959
            self.info = json.loads(self.info_data)
960
            if not self.info:
961
                self.info = {}
962
        except Exception, e:
963
            self.info = {}
964

    
965
        for key,value in self.info.iteritems():
966
            setattr(self, 'info_%s' % key, value)
967

    
968
    @property
969
    def settings(self):
970
        extra_data = {}
971

    
972
        info_data = {}
973
        if self.info_data:
974
            info_data = json.loads(self.info_data)
975

    
976
        extra_data['info'] = info_data
977

    
978
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
979
            extra_data[key] = getattr(self, key)
980

    
981
        extra_data['instance'] = self
982
        return auth.get_provider(self.module, self.user,
983
                                           self.identifier, **extra_data)
984

    
985
    def __repr__(self):
986
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
987

    
988
    def __unicode__(self):
989
        if self.identifier:
990
            return "%s:%s" % (self.module, self.identifier)
991
        if self.auth_backend:
992
            return "%s:%s" % (self.module, self.auth_backend)
993
        return self.module
994

    
995
    def save(self, *args, **kwargs):
996
        self.info_data = json.dumps(self.info)
997
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
998

    
999

    
1000
class ExtendedManager(models.Manager):
1001
    def _update_or_create(self, **kwargs):
1002
        assert kwargs, \
1003
            'update_or_create() must be passed at least one keyword argument'
1004
        obj, created = self.get_or_create(**kwargs)
1005
        defaults = kwargs.pop('defaults', {})
1006
        if created:
1007
            return obj, True, False
1008
        else:
1009
            try:
1010
                params = dict(
1011
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
1012
                params.update(defaults)
1013
                for attr, val in params.items():
1014
                    if hasattr(obj, attr):
1015
                        setattr(obj, attr, val)
1016
                sid = transaction.savepoint()
1017
                obj.save(force_update=True)
1018
                transaction.savepoint_commit(sid)
1019
                return obj, False, True
1020
            except IntegrityError, e:
1021
                transaction.savepoint_rollback(sid)
1022
                try:
1023
                    return self.get(**kwargs), False, False
1024
                except self.model.DoesNotExist:
1025
                    raise e
1026

    
1027
    update_or_create = _update_or_create
1028

    
1029

    
1030
class AstakosUserQuota(models.Model):
1031
    objects = ExtendedManager()
1032
    capacity = intDecimalField()
1033
    resource = models.ForeignKey(Resource)
1034
    user = models.ForeignKey(AstakosUser)
1035

    
1036
    class Meta:
1037
        unique_together = ("resource", "user")
1038

    
1039

    
1040
class ApprovalTerms(models.Model):
1041
    """
1042
    Model for approval terms
1043
    """
1044

    
1045
    date = models.DateTimeField(
1046
        _('Issue date'), db_index=True, auto_now_add=True)
1047
    location = models.CharField(_('Terms location'), max_length=255)
1048

    
1049

    
1050
class Invitation(models.Model):
1051
    """
1052
    Model for registring invitations
1053
    """
1054
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1055
                                null=True)
1056
    realname = models.CharField(_('Real name'), max_length=255)
1057
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1058
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1059
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1060
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1061
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1062

    
1063
    def __init__(self, *args, **kwargs):
1064
        super(Invitation, self).__init__(*args, **kwargs)
1065
        if not self.id:
1066
            self.code = _generate_invitation_code()
1067

    
1068
    def consume(self):
1069
        self.is_consumed = True
1070
        self.consumed = datetime.now()
1071
        self.save()
1072

    
1073
    def __unicode__(self):
1074
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1075

    
1076

    
1077
class EmailChangeManager(models.Manager):
1078

    
1079
    @transaction.commit_on_success
1080
    def change_email(self, activation_key):
1081
        """
1082
        Validate an activation key and change the corresponding
1083
        ``User`` if valid.
1084

1085
        If the key is valid and has not expired, return the ``User``
1086
        after activating.
1087

1088
        If the key is not valid or has expired, return ``None``.
1089

1090
        If the key is valid but the ``User`` is already active,
1091
        return ``None``.
1092

1093
        After successful email change the activation record is deleted.
1094

1095
        Throws ValueError if there is already
1096
        """
1097
        try:
1098
            email_change = self.model.objects.get(
1099
                activation_key=activation_key)
1100
            if email_change.activation_key_expired():
1101
                email_change.delete()
1102
                raise EmailChange.DoesNotExist
1103
            # is there an active user with this address?
1104
            try:
1105
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1106
            except AstakosUser.DoesNotExist:
1107
                pass
1108
            else:
1109
                raise ValueError(_('The new email address is reserved.'))
1110
            # update user
1111
            user = AstakosUser.objects.get(pk=email_change.user_id)
1112
            old_email = user.email
1113
            user.email = email_change.new_email_address
1114
            user.save()
1115
            email_change.delete()
1116
            msg = "User %s changed email from %s to %s" % (user.log_display,
1117
                                                           old_email,
1118
                                                           user.email)
1119
            logger.log(astakos_settings.LOGGING_LEVEL, msg)
1120
            return user
1121
        except EmailChange.DoesNotExist:
1122
            raise ValueError(_('Invalid activation key.'))
1123

    
1124

    
1125
class EmailChange(models.Model):
1126
    new_email_address = models.EmailField(
1127
        _(u'new e-mail address'),
1128
        help_text=_('Provide a new email address. Until you verify the new '
1129
                    'address by following the activation link that will be '
1130
                    'sent to it, your old email address will remain active.'))
1131
    user = models.ForeignKey(
1132
        AstakosUser, unique=True, related_name='emailchanges')
1133
    requested_at = models.DateTimeField(auto_now_add=True)
1134
    activation_key = models.CharField(
1135
        max_length=40, unique=True, db_index=True)
1136

    
1137
    objects = EmailChangeManager()
1138

    
1139
    def get_url(self):
1140
        return reverse('email_change_confirm',
1141
                      kwargs={'activation_key': self.activation_key})
1142

    
1143
    def activation_key_expired(self):
1144
        expiration_date = timedelta(days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1145
        return self.requested_at + expiration_date < datetime.now()
1146

    
1147

    
1148
class AdditionalMail(models.Model):
1149
    """
1150
    Model for registring invitations
1151
    """
1152
    owner = models.ForeignKey(AstakosUser)
1153
    email = models.EmailField()
1154

    
1155

    
1156
def _generate_invitation_code():
1157
    while True:
1158
        code = randint(1, 2L ** 63 - 1)
1159
        try:
1160
            Invitation.objects.get(code=code)
1161
            # An invitation with this code already exists, try again
1162
        except Invitation.DoesNotExist:
1163
            return code
1164

    
1165

    
1166
def get_latest_terms():
1167
    try:
1168
        term = ApprovalTerms.objects.order_by('-id')[0]
1169
        return term
1170
    except IndexError:
1171
        pass
1172
    return None
1173

    
1174

    
1175
class PendingThirdPartyUser(models.Model):
1176
    """
1177
    Model for registring successful third party user authentications
1178
    """
1179
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1180
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1181
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1182
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1183
                                  null=True)
1184
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1185
                                 null=True)
1186
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1187
                                   null=True)
1188
    username = models.CharField(_('username'), max_length=30, unique=True,
1189
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1190
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1191
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1192
    info = models.TextField(default="", null=True, blank=True)
1193

    
1194
    class Meta:
1195
        unique_together = ("provider", "third_party_identifier")
1196

    
1197
    def get_user_instance(self):
1198
        """
1199
        Create a new AstakosUser instance based on details provided when user
1200
        initially signed up.
1201
        """
1202
        d = copy.copy(self.__dict__)
1203
        d.pop('_state', None)
1204
        d.pop('id', None)
1205
        d.pop('token', None)
1206
        d.pop('created', None)
1207
        d.pop('info', None)
1208
        d.pop('affiliation', None)
1209
        d.pop('provider', None)
1210
        d.pop('third_party_identifier', None)
1211
        user = AstakosUser(**d)
1212

    
1213
        return user
1214

    
1215
    @property
1216
    def realname(self):
1217
        return '%s %s' %(self.first_name, self.last_name)
1218

    
1219
    @realname.setter
1220
    def realname(self, value):
1221
        parts = value.split(' ')
1222
        if len(parts) == 2:
1223
            self.first_name = parts[0]
1224
            self.last_name = parts[1]
1225
        else:
1226
            self.last_name = parts[0]
1227

    
1228
    def save(self, *args, **kwargs):
1229
        if not self.id:
1230
            # set username
1231
            while not self.username:
1232
                username =  uuid.uuid4().hex[:30]
1233
                try:
1234
                    AstakosUser.objects.get(username = username)
1235
                except AstakosUser.DoesNotExist, e:
1236
                    self.username = username
1237
        super(PendingThirdPartyUser, self).save(*args, **kwargs)
1238

    
1239
    def generate_token(self):
1240
        self.password = self.third_party_identifier
1241
        self.last_login = datetime.now()
1242
        self.token = default_token_generator.make_token(self)
1243

    
1244
    def existing_user(self):
1245
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1246
                                         auth_providers__identifier=self.third_party_identifier)
1247

    
1248
    def get_provider(self, user):
1249
        params = {
1250
            'info_data': self.info,
1251
            'affiliation': self.affiliation
1252
        }
1253
        return auth.get_provider(self.provider, user,
1254
                                 self.third_party_identifier, **params)
1255

    
1256
class SessionCatalog(models.Model):
1257
    session_key = models.CharField(_('session key'), max_length=40)
1258
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1259

    
1260

    
1261
class UserSetting(models.Model):
1262
    user = models.ForeignKey(AstakosUser)
1263
    setting = models.CharField(max_length=255)
1264
    value = models.IntegerField()
1265

    
1266
    objects = ForUpdateManager()
1267

    
1268
    class Meta:
1269
        unique_together = ("user", "setting")
1270

    
1271

    
1272
### PROJECTS ###
1273
################
1274

    
1275
class ChainManager(ForUpdateManager):
1276

    
1277
    def search_by_name(self, *search_strings):
1278
        projects = Project.objects.search_by_name(*search_strings)
1279
        chains = [p.id for p in projects]
1280
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1281
        apps = (app for app in apps if app.is_latest())
1282
        app_chains = [app.chain for app in apps if app.chain not in chains]
1283
        return chains + app_chains
1284

    
1285
    def all_full_state(self):
1286
        chains = self.all()
1287
        cids = [c.chain for c in chains]
1288
        projects = Project.objects.select_related('application').in_bulk(cids)
1289

    
1290
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1291
        chain_latest = dict(objs.values_list('chain', 'latest'))
1292

    
1293
        objs = ProjectApplication.objects.select_related('applicant')
1294
        apps = objs.in_bulk(chain_latest.values())
1295

    
1296
        d = {}
1297
        for chain in chains:
1298
            pk = chain.pk
1299
            project = projects.get(pk, None)
1300
            app = apps[chain_latest[pk]]
1301
            d[chain.pk] = chain.get_state(project, app)
1302

    
1303
        return d
1304

    
1305
    def of_project(self, project):
1306
        if project is None:
1307
            return None
1308
        try:
1309
            return self.get(chain=project.id)
1310
        except Chain.DoesNotExist:
1311
            raise AssertionError('project with no chain')
1312

    
1313

    
1314
class Chain(models.Model):
1315
    chain  =   models.AutoField(primary_key=True)
1316

    
1317
    def __str__(self):
1318
        return "%s" % (self.chain,)
1319

    
1320
    objects = ChainManager()
1321

    
1322
    PENDING            = 0
1323
    DENIED             = 3
1324
    DISMISSED          = 4
1325
    CANCELLED          = 5
1326

    
1327
    APPROVED           = 10
1328
    APPROVED_PENDING   = 11
1329
    SUSPENDED          = 12
1330
    SUSPENDED_PENDING  = 13
1331
    TERMINATED         = 14
1332
    TERMINATED_PENDING = 15
1333

    
1334
    PENDING_STATES = [PENDING,
1335
                      APPROVED_PENDING,
1336
                      SUSPENDED_PENDING,
1337
                      TERMINATED_PENDING,
1338
                      ]
1339

    
1340
    MODIFICATION_STATES = [APPROVED_PENDING,
1341
                           SUSPENDED_PENDING,
1342
                           TERMINATED_PENDING,
1343
                           ]
1344

    
1345
    RELEVANT_STATES = [PENDING,
1346
                       DENIED,
1347
                       APPROVED,
1348
                       APPROVED_PENDING,
1349
                       SUSPENDED,
1350
                       SUSPENDED_PENDING,
1351
                       TERMINATED_PENDING,
1352
                       ]
1353

    
1354
    SKIP_STATES = [DISMISSED,
1355
                   CANCELLED,
1356
                   TERMINATED]
1357

    
1358
    STATE_DISPLAY = {
1359
        PENDING            : _("Pending"),
1360
        DENIED             : _("Denied"),
1361
        DISMISSED          : _("Dismissed"),
1362
        CANCELLED          : _("Cancelled"),
1363
        APPROVED           : _("Active"),
1364
        APPROVED_PENDING   : _("Active - Pending"),
1365
        SUSPENDED          : _("Suspended"),
1366
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1367
        TERMINATED         : _("Terminated"),
1368
        TERMINATED_PENDING : _("Terminated - Pending"),
1369
        }
1370

    
1371

    
1372
    @classmethod
1373
    def _chain_state(cls, project_state, app_state):
1374
        s = CHAIN_STATE.get((project_state, app_state), None)
1375
        if s is None:
1376
            raise AssertionError('inconsistent chain state')
1377
        return s
1378

    
1379
    @classmethod
1380
    def chain_state(cls, project, app):
1381
        p_state = project.state if project else None
1382
        return cls._chain_state(p_state, app.state)
1383

    
1384
    @classmethod
1385
    def state_display(cls, s):
1386
        if s is None:
1387
            return _("Unknown")
1388
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1389

    
1390
    def last_application(self):
1391
        return self.chained_apps.order_by('-id')[0]
1392

    
1393
    def get_project(self):
1394
        try:
1395
            return self.chained_project
1396
        except Project.DoesNotExist:
1397
            return None
1398

    
1399
    def get_elements(self):
1400
        project = self.get_project()
1401
        app = self.last_application()
1402
        return project, app
1403

    
1404
    def get_state(self, project, app):
1405
        s = self.chain_state(project, app)
1406
        return s, project, app
1407

    
1408
    def full_state(self):
1409
        project, app = self.get_elements()
1410
        return self.get_state(project, app)
1411

    
1412

    
1413
def new_chain():
1414
    c = Chain.objects.create()
1415
    return c
1416

    
1417

    
1418
class ProjectApplicationManager(ForUpdateManager):
1419

    
1420
    def user_visible_projects(self, *filters, **kw_filters):
1421
        model = self.model
1422
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1423

    
1424
    def user_visible_by_chain(self, flt):
1425
        model = self.model
1426
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1427
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1428
        by_chain = dict(pending.annotate(models.Max('id')))
1429
        by_chain.update(approved.annotate(models.Max('id')))
1430
        return self.filter(flt, id__in=by_chain.values())
1431

    
1432
    def user_accessible_projects(self, user):
1433
        """
1434
        Return projects accessed by specified user.
1435
        """
1436
        if user.is_project_admin():
1437
            participates_filters = Q()
1438
        else:
1439
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1440
                                   Q(project__projectmembership__person=user)
1441

    
1442
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1443

    
1444
    def search_by_name(self, *search_strings):
1445
        q = Q()
1446
        for s in search_strings:
1447
            q = q | Q(name__icontains=s)
1448
        return self.filter(q)
1449

    
1450
    def latest_of_chain(self, chain_id):
1451
        try:
1452
            return self.filter(chain=chain_id).order_by('-id')[0]
1453
        except IndexError:
1454
            return None
1455

    
1456

    
1457
class ProjectApplication(models.Model):
1458
    applicant               =   models.ForeignKey(
1459
                                    AstakosUser,
1460
                                    related_name='projects_applied',
1461
                                    db_index=True)
1462

    
1463
    PENDING     =    0
1464
    APPROVED    =    1
1465
    REPLACED    =    2
1466
    DENIED      =    3
1467
    DISMISSED   =    4
1468
    CANCELLED   =    5
1469

    
1470
    state                   =   models.IntegerField(default=PENDING,
1471
                                                    db_index=True)
1472

    
1473
    owner                   =   models.ForeignKey(
1474
                                    AstakosUser,
1475
                                    related_name='projects_owned',
1476
                                    db_index=True)
1477

    
1478
    chain                   =   models.ForeignKey(Chain,
1479
                                                  related_name='chained_apps',
1480
                                                  db_column='chain')
1481
    precursor_application   =   models.ForeignKey('ProjectApplication',
1482
                                                  null=True,
1483
                                                  blank=True)
1484

    
1485
    name                    =   models.CharField(max_length=80)
1486
    homepage                =   models.URLField(max_length=255, null=True,
1487
                                                verify_exists=False)
1488
    description             =   models.TextField(null=True, blank=True)
1489
    start_date              =   models.DateTimeField(null=True, blank=True)
1490
    end_date                =   models.DateTimeField()
1491
    member_join_policy      =   models.IntegerField()
1492
    member_leave_policy     =   models.IntegerField()
1493
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1494
    resource_grants         =   models.ManyToManyField(
1495
                                    Resource,
1496
                                    null=True,
1497
                                    blank=True,
1498
                                    through='ProjectResourceGrant')
1499
    comments                =   models.TextField(null=True, blank=True)
1500
    issue_date              =   models.DateTimeField(auto_now_add=True)
1501
    response_date           =   models.DateTimeField(null=True, blank=True)
1502
    response                =   models.TextField(null=True, blank=True)
1503

    
1504
    objects                 =   ProjectApplicationManager()
1505

    
1506
    # Compiled queries
1507
    Q_PENDING  = Q(state=PENDING)
1508
    Q_APPROVED = Q(state=APPROVED)
1509
    Q_DENIED   = Q(state=DENIED)
1510

    
1511
    class Meta:
1512
        unique_together = ("chain", "id")
1513

    
1514
    def __unicode__(self):
1515
        return "%s applied by %s" % (self.name, self.applicant)
1516

    
1517
    # TODO: Move to a more suitable place
1518
    APPLICATION_STATE_DISPLAY = {
1519
        PENDING  : _('Pending review'),
1520
        APPROVED : _('Approved'),
1521
        REPLACED : _('Replaced'),
1522
        DENIED   : _('Denied'),
1523
        DISMISSED: _('Dismissed'),
1524
        CANCELLED: _('Cancelled')
1525
    }
1526

    
1527
    @property
1528
    def log_display(self):
1529
        return "application %s (%s) for project %s" % (
1530
            self.id, self.name, self.chain)
1531

    
1532
    def state_display(self):
1533
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1534

    
1535
    def project_state_display(self):
1536
        try:
1537
            project = self.project
1538
            return project.state_display()
1539
        except Project.DoesNotExist:
1540
            return self.state_display()
1541

    
1542
    def add_resource_policy(self, resource, uplimit):
1543
        """Raises ObjectDoesNotExist, IntegrityError"""
1544
        q = self.projectresourcegrant_set
1545
        resource = Resource.objects.get(name=resource)
1546
        q.create(resource=resource, member_capacity=uplimit)
1547

    
1548
    def members_count(self):
1549
        return self.project.approved_memberships.count()
1550

    
1551
    @property
1552
    def grants(self):
1553
        return self.projectresourcegrant_set.values('member_capacity',
1554
                                                    'resource__name')
1555

    
1556
    @property
1557
    def resource_policies(self):
1558
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1559

    
1560
    def set_resource_policies(self, policies):
1561
        for resource, uplimit in policies:
1562
            self.add_resource_policy(resource, uplimit)
1563

    
1564
    def pending_modifications_incl_me(self):
1565
        q = self.chained_applications()
1566
        q = q.filter(Q(state=self.PENDING))
1567
        return q
1568

    
1569
    def last_pending_incl_me(self):
1570
        try:
1571
            return self.pending_modifications_incl_me().order_by('-id')[0]
1572
        except IndexError:
1573
            return None
1574

    
1575
    def pending_modifications(self):
1576
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1577

    
1578
    def last_pending(self):
1579
        try:
1580
            return self.pending_modifications().order_by('-id')[0]
1581
        except IndexError:
1582
            return None
1583

    
1584
    def is_modification(self):
1585
        # if self.state != self.PENDING:
1586
        #     return False
1587
        parents = self.chained_applications().filter(id__lt=self.id)
1588
        parents = parents.filter(state__in=[self.APPROVED])
1589
        return parents.count() > 0
1590

    
1591
    def chained_applications(self):
1592
        return ProjectApplication.objects.filter(chain=self.chain)
1593

    
1594
    def is_latest(self):
1595
        return self.chained_applications().order_by('-id')[0] == self
1596

    
1597
    def has_pending_modifications(self):
1598
        return bool(self.last_pending())
1599

    
1600
    def denied_modifications(self):
1601
        q = self.chained_applications()
1602
        q = q.filter(Q(state=self.DENIED))
1603
        q = q.filter(~Q(id=self.id))
1604
        return q
1605

    
1606
    def last_denied(self):
1607
        try:
1608
            return self.denied_modifications().order_by('-id')[0]
1609
        except IndexError:
1610
            return None
1611

    
1612
    def has_denied_modifications(self):
1613
        return bool(self.last_denied())
1614

    
1615
    def is_applied(self):
1616
        try:
1617
            self.project
1618
            return True
1619
        except Project.DoesNotExist:
1620
            return False
1621

    
1622
    def get_project(self):
1623
        try:
1624
            return Project.objects.get(id=self.chain)
1625
        except Project.DoesNotExist:
1626
            return None
1627

    
1628
    def project_exists(self):
1629
        return self.get_project() is not None
1630

    
1631
    def can_cancel(self):
1632
        return self.state == self.PENDING
1633

    
1634
    def cancel(self):
1635
        if not self.can_cancel():
1636
            m = _("cannot cancel: application '%s' in state '%s'") % (
1637
                    self.id, self.state)
1638
            raise AssertionError(m)
1639

    
1640
        self.state = self.CANCELLED
1641
        self.save()
1642

    
1643
    def can_dismiss(self):
1644
        return self.state == self.DENIED
1645

    
1646
    def dismiss(self):
1647
        if not self.can_dismiss():
1648
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1649
                    self.id, self.state)
1650
            raise AssertionError(m)
1651

    
1652
        self.state = self.DISMISSED
1653
        self.save()
1654

    
1655
    def can_deny(self):
1656
        return self.state == self.PENDING
1657

    
1658
    def deny(self, reason):
1659
        if not self.can_deny():
1660
            m = _("cannot deny: application '%s' in state '%s'") % (
1661
                    self.id, self.state)
1662
            raise AssertionError(m)
1663

    
1664
        self.state = self.DENIED
1665
        self.response_date = datetime.now()
1666
        self.response = reason
1667
        self.save()
1668

    
1669
    def can_approve(self):
1670
        return self.state == self.PENDING
1671

    
1672
    def approve(self, reason):
1673
        if not self.can_approve():
1674
            m = _("cannot approve: project '%s' in state '%s'") % (
1675
                    self.name, self.state)
1676
            raise AssertionError(m) # invalid argument
1677

    
1678
        now = datetime.now()
1679
        self.state = self.APPROVED
1680
        self.response_date = now
1681
        self.response = reason
1682
        self.save()
1683

    
1684
        project = self.get_project()
1685
        if project is None:
1686
            project = Project(id=self.chain)
1687

    
1688
        project.name = self.name
1689
        project.application = self
1690
        project.last_approval_date = now
1691
        project.save()
1692
        if project.is_deactivated():
1693
            project.resume()
1694
        return project
1695

    
1696
    @property
1697
    def member_join_policy_display(self):
1698
        policy = self.member_join_policy
1699
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1700

    
1701
    @property
1702
    def member_leave_policy_display(self):
1703
        policy = self.member_leave_policy
1704
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1705

    
1706
class ProjectResourceGrant(models.Model):
1707

    
1708
    resource                =   models.ForeignKey(Resource)
1709
    project_application     =   models.ForeignKey(ProjectApplication,
1710
                                                  null=True)
1711
    project_capacity        =   intDecimalField(null=True)
1712
    member_capacity         =   intDecimalField(default=0)
1713

    
1714
    objects = ExtendedManager()
1715

    
1716
    class Meta:
1717
        unique_together = ("resource", "project_application")
1718

    
1719
    def display_member_capacity(self):
1720
        if self.member_capacity:
1721
            if self.resource.unit:
1722
                return ProjectResourceGrant.display_filesize(
1723
                    self.member_capacity)
1724
            else:
1725
                if math.isinf(self.member_capacity):
1726
                    return 'Unlimited'
1727
                else:
1728
                    return self.member_capacity
1729
        else:
1730
            return 'Unlimited'
1731

    
1732
    def __str__(self):
1733
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1734
                                        self.display_member_capacity())
1735

    
1736
    @classmethod
1737
    def display_filesize(cls, value):
1738
        try:
1739
            value = float(value)
1740
        except:
1741
            return
1742
        else:
1743
            if math.isinf(value):
1744
                return 'Unlimited'
1745
            if value > 1:
1746
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1747
                                [0, 0, 0, 0, 0, 0])
1748
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1749
                quotient = float(value) / 1024**exponent
1750
                unit, value_decimals = unit_list[exponent]
1751
                format_string = '{0:.%sf} {1}' % (value_decimals)
1752
                return format_string.format(quotient, unit)
1753
            if value == 0:
1754
                return '0 bytes'
1755
            if value == 1:
1756
                return '1 byte'
1757
            else:
1758
               return '0'
1759

    
1760

    
1761
class ProjectManager(ForUpdateManager):
1762

    
1763
    def terminated_projects(self):
1764
        q = self.model.Q_TERMINATED
1765
        return self.filter(q)
1766

    
1767
    def not_terminated_projects(self):
1768
        q = ~self.model.Q_TERMINATED
1769
        return self.filter(q)
1770

    
1771
    def deactivated_projects(self):
1772
        q = self.model.Q_DEACTIVATED
1773
        return self.filter(q)
1774

    
1775
    def expired_projects(self):
1776
        q = (~Q(state=Project.TERMINATED) &
1777
              Q(application__end_date__lt=datetime.now()))
1778
        return self.filter(q)
1779

    
1780
    def search_by_name(self, *search_strings):
1781
        q = Q()
1782
        for s in search_strings:
1783
            q = q | Q(name__icontains=s)
1784
        return self.filter(q)
1785

    
1786

    
1787
class Project(models.Model):
1788

    
1789
    id                          =   models.OneToOneField(Chain,
1790
                                                      related_name='chained_project',
1791
                                                      db_column='id',
1792
                                                      primary_key=True)
1793

    
1794
    application                 =   models.OneToOneField(
1795
                                            ProjectApplication,
1796
                                            related_name='project')
1797
    last_approval_date          =   models.DateTimeField(null=True)
1798

    
1799
    members                     =   models.ManyToManyField(
1800
                                            AstakosUser,
1801
                                            through='ProjectMembership')
1802

    
1803
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1804
    deactivation_date           =   models.DateTimeField(null=True)
1805

    
1806
    creation_date               =   models.DateTimeField(auto_now_add=True)
1807
    name                        =   models.CharField(
1808
                                            max_length=80,
1809
                                            null=True,
1810
                                            db_index=True,
1811
                                            unique=True)
1812

    
1813
    APPROVED    = 1
1814
    SUSPENDED   = 10
1815
    TERMINATED  = 100
1816

    
1817
    state                       =   models.IntegerField(default=APPROVED,
1818
                                                        db_index=True)
1819

    
1820
    objects     =   ProjectManager()
1821

    
1822
    # Compiled queries
1823
    Q_TERMINATED  = Q(state=TERMINATED)
1824
    Q_SUSPENDED   = Q(state=SUSPENDED)
1825
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1826

    
1827
    def __str__(self):
1828
        return uenc(_("<project %s '%s'>") %
1829
                    (self.id, udec(self.application.name)))
1830

    
1831
    __repr__ = __str__
1832

    
1833
    def __unicode__(self):
1834
        return _("<project %s '%s'>") % (self.id, self.application.name)
1835

    
1836
    STATE_DISPLAY = {
1837
        APPROVED   : 'Active',
1838
        SUSPENDED  : 'Suspended',
1839
        TERMINATED : 'Terminated'
1840
        }
1841

    
1842
    def state_display(self):
1843
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1844

    
1845
    def expiration_info(self):
1846
        return (str(self.id), self.name, self.state_display(),
1847
                str(self.application.end_date))
1848

    
1849
    def is_deactivated(self, reason=None):
1850
        if reason is not None:
1851
            return self.state == reason
1852

    
1853
        return self.state != self.APPROVED
1854

    
1855
    ### Deactivation calls
1856

    
1857
    def terminate(self):
1858
        self.deactivation_reason = 'TERMINATED'
1859
        self.deactivation_date = datetime.now()
1860
        self.state = self.TERMINATED
1861
        self.name = None
1862
        self.save()
1863

    
1864
    def suspend(self):
1865
        self.deactivation_reason = 'SUSPENDED'
1866
        self.deactivation_date = datetime.now()
1867
        self.state = self.SUSPENDED
1868
        self.save()
1869

    
1870
    def resume(self):
1871
        self.deactivation_reason = None
1872
        self.deactivation_date = None
1873
        self.state = self.APPROVED
1874
        self.save()
1875

    
1876
    ### Logical checks
1877

    
1878
    def is_inconsistent(self):
1879
        now = datetime.now()
1880
        dates = [self.creation_date,
1881
                 self.last_approval_date,
1882
                 self.deactivation_date]
1883
        return any([date > now for date in dates])
1884

    
1885
    def is_approved(self):
1886
        return self.state == self.APPROVED
1887

    
1888
    @property
1889
    def is_alive(self):
1890
        return not self.is_terminated
1891

    
1892
    @property
1893
    def is_terminated(self):
1894
        return self.is_deactivated(self.TERMINATED)
1895

    
1896
    @property
1897
    def is_suspended(self):
1898
        return self.is_deactivated(self.SUSPENDED)
1899

    
1900
    def violates_resource_grants(self):
1901
        return False
1902

    
1903
    def violates_members_limit(self, adding=0):
1904
        application = self.application
1905
        limit = application.limit_on_members_number
1906
        if limit is None:
1907
            return False
1908
        return (len(self.approved_members) + adding > limit)
1909

    
1910

    
1911
    ### Other
1912

    
1913
    def count_pending_memberships(self):
1914
        return self.projectmembership_set.requested().count()
1915

    
1916
    def members_count(self):
1917
        return self.approved_memberships.count()
1918

    
1919
    @property
1920
    def approved_memberships(self):
1921
        query = ProjectMembership.Q_ACCEPTED_STATES
1922
        return self.projectmembership_set.filter(query)
1923

    
1924
    @property
1925
    def approved_members(self):
1926
        return [m.person for m in self.approved_memberships]
1927

    
1928

    
1929
CHAIN_STATE = {
1930
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1931
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1932
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1933
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1934
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1935

    
1936
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1937
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1938
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1939
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1940
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1941

    
1942
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1943
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1944
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1945
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1946
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1947

    
1948
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1949
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1950
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1951
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1952
    }
1953

    
1954

    
1955
class ProjectMembershipManager(ForUpdateManager):
1956

    
1957
    def any_accepted(self):
1958
        q = self.model.Q_ACCEPTED_STATES
1959
        return self.filter(q)
1960

    
1961
    def actually_accepted(self):
1962
        q = self.model.Q_ACTUALLY_ACCEPTED
1963
        return self.filter(q)
1964

    
1965
    def requested(self):
1966
        return self.filter(state=ProjectMembership.REQUESTED)
1967

    
1968
    def suspended(self):
1969
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1970

    
1971
class ProjectMembership(models.Model):
1972

    
1973
    person              =   models.ForeignKey(AstakosUser)
1974
    request_date        =   models.DateTimeField(auto_now_add=True)
1975
    project             =   models.ForeignKey(Project)
1976

    
1977
    REQUESTED           =   0
1978
    ACCEPTED            =   1
1979
    LEAVE_REQUESTED     =   5
1980
    # User deactivation
1981
    USER_SUSPENDED      =   10
1982

    
1983
    REMOVED             =   200
1984

    
1985
    ASSOCIATED_STATES   =   set([REQUESTED,
1986
                                 ACCEPTED,
1987
                                 LEAVE_REQUESTED,
1988
                                 USER_SUSPENDED,
1989
                                 ])
1990

    
1991
    ACCEPTED_STATES     =   set([ACCEPTED,
1992
                                 LEAVE_REQUESTED,
1993
                                 USER_SUSPENDED,
1994
                                 ])
1995

    
1996
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1997

    
1998
    state               =   models.IntegerField(default=REQUESTED,
1999
                                                db_index=True)
2000
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
2001
    leave_request_date  =   models.DateTimeField(null=True)
2002

    
2003
    objects     =   ProjectMembershipManager()
2004

    
2005
    # Compiled queries
2006
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
2007
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2008

    
2009
    MEMBERSHIP_STATE_DISPLAY = {
2010
        REQUESTED           : _('Requested'),
2011
        ACCEPTED            : _('Accepted'),
2012
        LEAVE_REQUESTED     : _('Leave Requested'),
2013
        USER_SUSPENDED      : _('Suspended'),
2014
        REMOVED             : _('Pending removal'),
2015
        }
2016

    
2017
    USER_FRIENDLY_STATE_DISPLAY = {
2018
        REQUESTED           : _('Join requested'),
2019
        ACCEPTED            : _('Accepted member'),
2020
        LEAVE_REQUESTED     : _('Requested to leave'),
2021
        USER_SUSPENDED      : _('Suspended member'),
2022
        REMOVED             : _('Pending removal'),
2023
        }
2024

    
2025
    def state_display(self):
2026
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2027

    
2028
    def user_friendly_state_display(self):
2029
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2030

    
2031
    class Meta:
2032
        unique_together = ("person", "project")
2033
        #index_together = [["project", "state"]]
2034

    
2035
    def __str__(self):
2036
        return uenc(_("<'%s' membership in '%s'>") % (
2037
                self.person.username, self.project))
2038

    
2039
    __repr__ = __str__
2040

    
2041
    def __init__(self, *args, **kwargs):
2042
        self.state = self.REQUESTED
2043
        super(ProjectMembership, self).__init__(*args, **kwargs)
2044

    
2045
    def _set_history_item(self, reason, date=None):
2046
        if isinstance(reason, basestring):
2047
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2048

    
2049
        history_item = ProjectMembershipHistory(
2050
                            serial=self.id,
2051
                            person=self.person_id,
2052
                            project=self.project_id,
2053
                            date=date or datetime.now(),
2054
                            reason=reason)
2055
        history_item.save()
2056
        serial = history_item.id
2057

    
2058
    def can_accept(self):
2059
        return self.state == self.REQUESTED
2060

    
2061
    def accept(self):
2062
        if not self.can_accept():
2063
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2064
            raise AssertionError(m)
2065

    
2066
        now = datetime.now()
2067
        self.acceptance_date = now
2068
        self._set_history_item(reason='ACCEPT', date=now)
2069
        self.state = self.ACCEPTED
2070
        self.save()
2071

    
2072
    def can_leave(self):
2073
        return self.state in self.ACCEPTED_STATES
2074

    
2075
    def leave_request(self):
2076
        if not self.can_leave():
2077
            m = _("%s: attempt to request to leave in state '%s'") % (
2078
                self, self.state)
2079
            raise AssertionError(m)
2080

    
2081
        self.leave_request_date = datetime.now()
2082
        self.state = self.LEAVE_REQUESTED
2083
        self.save()
2084

    
2085
    def can_deny_leave(self):
2086
        return self.state == self.LEAVE_REQUESTED
2087

    
2088
    def leave_request_deny(self):
2089
        if not self.can_deny_leave():
2090
            m = _("%s: attempt to deny leave request in state '%s'") % (
2091
                self, self.state)
2092
            raise AssertionError(m)
2093

    
2094
        self.leave_request_date = None
2095
        self.state = self.ACCEPTED
2096
        self.save()
2097

    
2098
    def can_cancel_leave(self):
2099
        return self.state == self.LEAVE_REQUESTED
2100

    
2101
    def leave_request_cancel(self):
2102
        if not self.can_cancel_leave():
2103
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2104
                self, self.state)
2105
            raise AssertionError(m)
2106

    
2107
        self.leave_request_date = None
2108
        self.state = self.ACCEPTED
2109
        self.save()
2110

    
2111
    def can_remove(self):
2112
        return self.state in self.ACCEPTED_STATES
2113

    
2114
    def remove(self):
2115
        if not self.can_remove():
2116
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2117
            raise AssertionError(m)
2118

    
2119
        self._set_history_item(reason='REMOVE')
2120
        self.delete()
2121

    
2122
    def can_reject(self):
2123
        return self.state == self.REQUESTED
2124

    
2125
    def reject(self):
2126
        if not self.can_reject():
2127
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2128
            raise AssertionError(m)
2129

    
2130
        # rejected requests don't need sync,
2131
        # because they were never effected
2132
        self._set_history_item(reason='REJECT')
2133
        self.delete()
2134

    
2135
    def can_cancel(self):
2136
        return self.state == self.REQUESTED
2137

    
2138
    def cancel(self):
2139
        if not self.can_cancel():
2140
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2141
            raise AssertionError(m)
2142

    
2143
        # rejected requests don't need sync,
2144
        # because they were never effected
2145
        self._set_history_item(reason='CANCEL')
2146
        self.delete()
2147

    
2148

    
2149
class Serial(models.Model):
2150
    serial  =   models.AutoField(primary_key=True)
2151

    
2152

    
2153
class ProjectMembershipHistory(models.Model):
2154
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2155
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2156

    
2157
    person  =   models.BigIntegerField()
2158
    project =   models.BigIntegerField()
2159
    date    =   models.DateTimeField(auto_now_add=True)
2160
    reason  =   models.IntegerField()
2161
    serial  =   models.BigIntegerField()
2162

    
2163
### SIGNALS ###
2164
################
2165

    
2166
def create_astakos_user(u):
2167
    try:
2168
        AstakosUser.objects.get(user_ptr=u.pk)
2169
    except AstakosUser.DoesNotExist:
2170
        extended_user = AstakosUser(user_ptr_id=u.pk)
2171
        extended_user.__dict__.update(u.__dict__)
2172
        extended_user.save()
2173
        if not extended_user.has_auth_provider('local'):
2174
            extended_user.add_auth_provider('local')
2175
    except BaseException, e:
2176
        logger.exception(e)
2177

    
2178
def fix_superusers():
2179
    # Associate superusers with AstakosUser
2180
    admins = User.objects.filter(is_superuser=True)
2181
    for u in admins:
2182
        create_astakos_user(u)
2183

    
2184
def user_post_save(sender, instance, created, **kwargs):
2185
    if not created:
2186
        return
2187
    create_astakos_user(instance)
2188
post_save.connect(user_post_save, sender=User)
2189

    
2190
def astakosuser_post_save(sender, instance, created, **kwargs):
2191
    pass
2192

    
2193
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2194

    
2195
def resource_post_save(sender, instance, created, **kwargs):
2196
    pass
2197

    
2198
post_save.connect(resource_post_save, sender=Resource)
2199

    
2200
def renew_token(sender, instance, **kwargs):
2201
    if not instance.auth_token:
2202
        instance.renew_token()
2203
pre_save.connect(renew_token, sender=AstakosUser)
2204
pre_save.connect(renew_token, sender=Component)