Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 8fb8d0cf

History | View | Annotate | Download (72.4 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
        def component_by_order(s):
152
            return s[1].get('order')
153

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

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

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

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

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

    
176

    
177
_presentation_data = {}
178

    
179

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

    
189

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

    
195

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

    
199

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

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

    
208

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

    
218
    objects = ForUpdateManager()
219

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

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

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

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

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

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

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

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

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

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

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

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

    
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 uuid_catalog(self, l=None):
319
        """
320
        Returns a uuid to username mapping for the uuids appearing in l.
321
        If l is None returns the mapping for all existing users.
322
        """
323
        q = self.filter(uuid__in=l) if l is not None else self
324
        return dict(q.values_list('uuid', 'username'))
325

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

    
341

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
522
        self.update_uuid()
523

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
710
        modules = astakos_settings.IM_MODULES
711

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

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

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

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

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

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

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

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

    
748
        msg_extra = ''
749
        message = ''
750

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

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

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

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

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

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

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

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

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

    
816

    
817
class AstakosUserAuthProviderManager(models.Manager):
818

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

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

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

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

    
845

    
846
class AuthProviderPolicyProfileManager(models.Manager):
847

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

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

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

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

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

    
881

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

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

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

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

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

    
909
    objects = AuthProviderPolicyProfileManager()
910

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

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

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

    
930

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

    
949
    objects = AstakosUserAuthProviderManager()
950

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

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

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

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

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

    
975
        extra_data['info'] = info_data
976

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

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

    
984
    def __repr__(self):
985
        return '<AstakosUserAuthProvider %s:%s>' % (
986
            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'),
1062
                                    null=True, blank=True)
1063

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

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

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

    
1077

    
1078
class EmailChangeManager(models.Manager):
1079

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

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

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

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

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

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

    
1126

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

    
1139
    objects = EmailChangeManager()
1140

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

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

    
1150

    
1151
class AdditionalMail(models.Model):
1152
    """
1153
    Model for registring invitations
1154
    """
1155
    owner = models.ForeignKey(AstakosUser)
1156
    email = models.EmailField()
1157

    
1158

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

    
1168

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

    
1177

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

    
1200
    class Meta:
1201
        unique_together = ("provider", "third_party_identifier")
1202

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

    
1219
        return user
1220

    
1221
    @property
1222
    def realname(self):
1223
        return '%s %s' % (self.first_name, self.last_name)
1224

    
1225
    @realname.setter
1226
    def realname(self, value):
1227
        parts = value.split(' ')
1228
        if len(parts) == 2:
1229
            self.first_name = parts[0]
1230
            self.last_name = parts[1]
1231
        else:
1232
            self.last_name = parts[0]
1233

    
1234
    def save(self, **kwargs):
1235
        if not self.id:
1236
            # set username
1237
            while not self.username:
1238
                username = uuid.uuid4().hex[:30]
1239
                try:
1240
                    AstakosUser.objects.get(username=username)
1241
                except AstakosUser.DoesNotExist, e:
1242
                    self.username = username
1243
        super(PendingThirdPartyUser, self).save(**kwargs)
1244

    
1245
    def generate_token(self):
1246
        self.password = self.third_party_identifier
1247
        self.last_login = datetime.now()
1248
        self.token = default_token_generator.make_token(self)
1249

    
1250
    def existing_user(self):
1251
        return AstakosUser.objects.filter(
1252
            auth_providers__module=self.provider,
1253
            auth_providers__identifier=self.third_party_identifier)
1254

    
1255
    def get_provider(self, user):
1256
        params = {
1257
            'info_data': self.info,
1258
            'affiliation': self.affiliation
1259
        }
1260
        return auth.get_provider(self.provider, user,
1261
                                 self.third_party_identifier, **params)
1262

    
1263

    
1264
class SessionCatalog(models.Model):
1265
    session_key = models.CharField(_('session key'), max_length=40)
1266
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1267

    
1268

    
1269
class UserSetting(models.Model):
1270
    user = models.ForeignKey(AstakosUser)
1271
    setting = models.CharField(max_length=255)
1272
    value = models.IntegerField()
1273

    
1274
    objects = ForUpdateManager()
1275

    
1276
    class Meta:
1277
        unique_together = ("user", "setting")
1278

    
1279

    
1280
### PROJECTS ###
1281
################
1282

    
1283
class ChainManager(ForUpdateManager):
1284

    
1285
    def search_by_name(self, *search_strings):
1286
        projects = Project.objects.search_by_name(*search_strings)
1287
        chains = [p.id for p in projects]
1288
        apps = ProjectApplication.objects.search_by_name(*search_strings)
1289
        apps = (app for app in apps if app.is_latest())
1290
        app_chains = [app.chain for app in apps if app.chain not in chains]
1291
        return chains + app_chains
1292

    
1293
    def all_full_state(self):
1294
        chains = self.all()
1295
        cids = [c.chain for c in chains]
1296
        projects = Project.objects.select_related('application').in_bulk(cids)
1297

    
1298
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1299
        chain_latest = dict(objs.values_list('chain', 'latest'))
1300

    
1301
        objs = ProjectApplication.objects.select_related('applicant')
1302
        apps = objs.in_bulk(chain_latest.values())
1303

    
1304
        d = {}
1305
        for chain in chains:
1306
            pk = chain.pk
1307
            project = projects.get(pk, None)
1308
            app = apps[chain_latest[pk]]
1309
            d[chain.pk] = chain.get_state(project, app)
1310

    
1311
        return d
1312

    
1313
    def of_project(self, project):
1314
        if project is None:
1315
            return None
1316
        try:
1317
            return self.get(chain=project.id)
1318
        except Chain.DoesNotExist:
1319
            raise AssertionError('project with no chain')
1320

    
1321

    
1322
class Chain(models.Model):
1323
    chain = models.AutoField(primary_key=True)
1324

    
1325
    def __str__(self):
1326
        return "%s" % (self.chain,)
1327

    
1328
    objects = ChainManager()
1329

    
1330
    PENDING = 0
1331
    DENIED = 3
1332
    DISMISSED = 4
1333
    CANCELLED = 5
1334

    
1335
    APPROVED = 10
1336
    APPROVED_PENDING = 11
1337
    SUSPENDED = 12
1338
    SUSPENDED_PENDING = 13
1339
    TERMINATED = 14
1340
    TERMINATED_PENDING = 15
1341

    
1342
    PENDING_STATES = [PENDING,
1343
                      APPROVED_PENDING,
1344
                      SUSPENDED_PENDING,
1345
                      TERMINATED_PENDING,
1346
                      ]
1347

    
1348
    MODIFICATION_STATES = [APPROVED_PENDING,
1349
                           SUSPENDED_PENDING,
1350
                           TERMINATED_PENDING,
1351
                           ]
1352

    
1353
    RELEVANT_STATES = [PENDING,
1354
                       DENIED,
1355
                       APPROVED,
1356
                       APPROVED_PENDING,
1357
                       SUSPENDED,
1358
                       SUSPENDED_PENDING,
1359
                       TERMINATED_PENDING,
1360
                       ]
1361

    
1362
    SKIP_STATES = [DISMISSED,
1363
                   CANCELLED,
1364
                   TERMINATED]
1365

    
1366
    STATE_DISPLAY = {
1367
        PENDING:             _("Pending"),
1368
        DENIED:              _("Denied"),
1369
        DISMISSED:           _("Dismissed"),
1370
        CANCELLED:           _("Cancelled"),
1371
        APPROVED:            _("Active"),
1372
        APPROVED_PENDING:    _("Active - Pending"),
1373
        SUSPENDED:           _("Suspended"),
1374
        SUSPENDED_PENDING:   _("Suspended - Pending"),
1375
        TERMINATED:          _("Terminated"),
1376
        TERMINATED_PENDING:  _("Terminated - Pending"),
1377
    }
1378

    
1379
    @classmethod
1380
    def _chain_state(cls, project_state, app_state):
1381
        s = CHAIN_STATE.get((project_state, app_state), None)
1382
        if s is None:
1383
            raise AssertionError('inconsistent chain state')
1384
        return s
1385

    
1386
    @classmethod
1387
    def chain_state(cls, project, app):
1388
        p_state = project.state if project else None
1389
        return cls._chain_state(p_state, app.state)
1390

    
1391
    @classmethod
1392
    def state_display(cls, s):
1393
        if s is None:
1394
            return _("Unknown")
1395
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1396

    
1397
    def last_application(self):
1398
        return self.chained_apps.order_by('-id')[0]
1399

    
1400
    def get_project(self):
1401
        try:
1402
            return self.chained_project
1403
        except Project.DoesNotExist:
1404
            return None
1405

    
1406
    def get_elements(self):
1407
        project = self.get_project()
1408
        app = self.last_application()
1409
        return project, app
1410

    
1411
    def get_state(self, project, app):
1412
        s = self.chain_state(project, app)
1413
        return s, project, app
1414

    
1415
    def full_state(self):
1416
        project, app = self.get_elements()
1417
        return self.get_state(project, app)
1418

    
1419

    
1420
def new_chain():
1421
    c = Chain.objects.create()
1422
    return c
1423

    
1424

    
1425
class ProjectApplicationManager(ForUpdateManager):
1426

    
1427
    def user_visible_projects(self, *filters, **kw_filters):
1428
        model = self.model
1429
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1430

    
1431
    def user_visible_by_chain(self, flt):
1432
        model = self.model
1433
        pending = self.filter(
1434
            model.Q_PENDING | model.Q_DENIED).values_list('chain')
1435
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1436
        by_chain = dict(pending.annotate(models.Max('id')))
1437
        by_chain.update(approved.annotate(models.Max('id')))
1438
        return self.filter(flt, id__in=by_chain.values())
1439

    
1440
    def user_accessible_projects(self, user):
1441
        """
1442
        Return projects accessed by specified user.
1443
        """
1444
        if user.is_project_admin():
1445
            participates_filters = Q()
1446
        else:
1447
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1448
                Q(project__projectmembership__person=user)
1449

    
1450
        return self.user_visible_by_chain(
1451
            participates_filters).order_by('issue_date').distinct()
1452

    
1453
    def search_by_name(self, *search_strings):
1454
        q = Q()
1455
        for s in search_strings:
1456
            q = q | Q(name__icontains=s)
1457
        return self.filter(q)
1458

    
1459
    def latest_of_chain(self, chain_id):
1460
        try:
1461
            return self.filter(chain=chain_id).order_by('-id')[0]
1462
        except IndexError:
1463
            return None
1464

    
1465

    
1466
class ProjectApplication(models.Model):
1467
    applicant = models.ForeignKey(
1468
        AstakosUser,
1469
        related_name='projects_applied',
1470
        db_index=True)
1471

    
1472
    PENDING = 0
1473
    APPROVED = 1
1474
    REPLACED = 2
1475
    DENIED = 3
1476
    DISMISSED = 4
1477
    CANCELLED = 5
1478

    
1479
    state = models.IntegerField(default=PENDING,
1480
                                db_index=True)
1481
    owner = models.ForeignKey(
1482
        AstakosUser,
1483
        related_name='projects_owned',
1484
        db_index=True)
1485
    chain = models.ForeignKey(Chain,
1486
                              related_name='chained_apps',
1487
                              db_column='chain')
1488
    precursor_application = models.ForeignKey('ProjectApplication',
1489
                                              null=True,
1490
                                              blank=True)
1491
    name = models.CharField(max_length=80)
1492
    homepage = models.URLField(max_length=255, null=True,
1493
                               verify_exists=False)
1494
    description = models.TextField(null=True, blank=True)
1495
    start_date = models.DateTimeField(null=True, blank=True)
1496
    end_date = models.DateTimeField()
1497
    member_join_policy = models.IntegerField()
1498
    member_leave_policy = models.IntegerField()
1499
    limit_on_members_number = models.PositiveIntegerField(null=True)
1500
    resource_grants = models.ManyToManyField(
1501
        Resource,
1502
        null=True,
1503
        blank=True,
1504
        through='ProjectResourceGrant')
1505
    comments = models.TextField(null=True, blank=True)
1506
    issue_date = models.DateTimeField(auto_now_add=True)
1507
    response_date = models.DateTimeField(null=True, blank=True)
1508
    response = models.TextField(null=True, blank=True)
1509

    
1510
    objects = ProjectApplicationManager()
1511

    
1512
    # Compiled queries
1513
    Q_PENDING = Q(state=PENDING)
1514
    Q_APPROVED = Q(state=APPROVED)
1515
    Q_DENIED = Q(state=DENIED)
1516

    
1517
    class Meta:
1518
        unique_together = ("chain", "id")
1519

    
1520
    def __unicode__(self):
1521
        return "%s applied by %s" % (self.name, self.applicant)
1522

    
1523
    # TODO: Move to a more suitable place
1524
    APPLICATION_STATE_DISPLAY = {
1525
        PENDING:   _('Pending review'),
1526
        APPROVED:  _('Approved'),
1527
        REPLACED:  _('Replaced'),
1528
        DENIED:    _('Denied'),
1529
        DISMISSED: _('Dismissed'),
1530
        CANCELLED: _('Cancelled')
1531
    }
1532

    
1533
    @property
1534
    def log_display(self):
1535
        return "application %s (%s) for project %s" % (
1536
            self.id, self.name, self.chain)
1537

    
1538
    def state_display(self):
1539
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1540

    
1541
    def project_state_display(self):
1542
        try:
1543
            project = self.project
1544
            return project.state_display()
1545
        except Project.DoesNotExist:
1546
            return self.state_display()
1547

    
1548
    def add_resource_policy(self, resource, uplimit):
1549
        """Raises ObjectDoesNotExist, IntegrityError"""
1550
        q = self.projectresourcegrant_set
1551
        resource = Resource.objects.get(name=resource)
1552
        q.create(resource=resource, member_capacity=uplimit)
1553

    
1554
    def members_count(self):
1555
        return self.project.approved_memberships.count()
1556

    
1557
    @property
1558
    def grants(self):
1559
        return self.projectresourcegrant_set.values('member_capacity',
1560
                                                    'resource__name')
1561

    
1562
    @property
1563
    def resource_policies(self):
1564
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1565

    
1566
    def set_resource_policies(self, policies):
1567
        for resource, uplimit in policies:
1568
            self.add_resource_policy(resource, uplimit)
1569

    
1570
    def pending_modifications_incl_me(self):
1571
        q = self.chained_applications()
1572
        q = q.filter(Q(state=self.PENDING))
1573
        return q
1574

    
1575
    def last_pending_incl_me(self):
1576
        try:
1577
            return self.pending_modifications_incl_me().order_by('-id')[0]
1578
        except IndexError:
1579
            return None
1580

    
1581
    def pending_modifications(self):
1582
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1583

    
1584
    def last_pending(self):
1585
        try:
1586
            return self.pending_modifications().order_by('-id')[0]
1587
        except IndexError:
1588
            return None
1589

    
1590
    def is_modification(self):
1591
        # if self.state != self.PENDING:
1592
        #     return False
1593
        parents = self.chained_applications().filter(id__lt=self.id)
1594
        parents = parents.filter(state__in=[self.APPROVED])
1595
        return parents.count() > 0
1596

    
1597
    def chained_applications(self):
1598
        return ProjectApplication.objects.filter(chain=self.chain)
1599

    
1600
    def is_latest(self):
1601
        return self.chained_applications().order_by('-id')[0] == self
1602

    
1603
    def has_pending_modifications(self):
1604
        return bool(self.last_pending())
1605

    
1606
    def denied_modifications(self):
1607
        q = self.chained_applications()
1608
        q = q.filter(Q(state=self.DENIED))
1609
        q = q.filter(~Q(id=self.id))
1610
        return q
1611

    
1612
    def last_denied(self):
1613
        try:
1614
            return self.denied_modifications().order_by('-id')[0]
1615
        except IndexError:
1616
            return None
1617

    
1618
    def has_denied_modifications(self):
1619
        return bool(self.last_denied())
1620

    
1621
    def is_applied(self):
1622
        try:
1623
            self.project
1624
            return True
1625
        except Project.DoesNotExist:
1626
            return False
1627

    
1628
    def get_project(self):
1629
        try:
1630
            return Project.objects.get(id=self.chain)
1631
        except Project.DoesNotExist:
1632
            return None
1633

    
1634
    def project_exists(self):
1635
        return self.get_project() is not None
1636

    
1637
    def can_cancel(self):
1638
        return self.state == self.PENDING
1639

    
1640
    def cancel(self):
1641
        if not self.can_cancel():
1642
            m = _("cannot cancel: application '%s' in state '%s'") % (
1643
                self.id, self.state)
1644
            raise AssertionError(m)
1645

    
1646
        self.state = self.CANCELLED
1647
        self.save()
1648

    
1649
    def can_dismiss(self):
1650
        return self.state == self.DENIED
1651

    
1652
    def dismiss(self):
1653
        if not self.can_dismiss():
1654
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1655
                self.id, self.state)
1656
            raise AssertionError(m)
1657

    
1658
        self.state = self.DISMISSED
1659
        self.save()
1660

    
1661
    def can_deny(self):
1662
        return self.state == self.PENDING
1663

    
1664
    def deny(self, reason):
1665
        if not self.can_deny():
1666
            m = _("cannot deny: application '%s' in state '%s'") % (
1667
                self.id, self.state)
1668
            raise AssertionError(m)
1669

    
1670
        self.state = self.DENIED
1671
        self.response_date = datetime.now()
1672
        self.response = reason
1673
        self.save()
1674

    
1675
    def can_approve(self):
1676
        return self.state == self.PENDING
1677

    
1678
    def approve(self, reason):
1679
        if not self.can_approve():
1680
            m = _("cannot approve: project '%s' in state '%s'") % (
1681
                self.name, self.state)
1682
            raise AssertionError(m)  # invalid argument
1683

    
1684
        now = datetime.now()
1685
        self.state = self.APPROVED
1686
        self.response_date = now
1687
        self.response = reason
1688
        self.save()
1689

    
1690
        project = self.get_project()
1691
        if project is None:
1692
            project = Project(id=self.chain)
1693

    
1694
        project.name = self.name
1695
        project.application = self
1696
        project.last_approval_date = now
1697
        project.save()
1698
        return project
1699

    
1700
    @property
1701
    def member_join_policy_display(self):
1702
        policy = self.member_join_policy
1703
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1704

    
1705
    @property
1706
    def member_leave_policy_display(self):
1707
        policy = self.member_leave_policy
1708
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1709

    
1710

    
1711
class ProjectResourceGrant(models.Model):
1712

    
1713
    resource = models.ForeignKey(Resource)
1714
    project_application = models.ForeignKey(ProjectApplication,
1715
                                            null=True)
1716
    project_capacity = intDecimalField(null=True)
1717
    member_capacity = intDecimalField(default=0)
1718

    
1719
    objects = ExtendedManager()
1720

    
1721
    class Meta:
1722
        unique_together = ("resource", "project_application")
1723

    
1724
    def display_member_capacity(self):
1725
        if self.member_capacity:
1726
            if self.resource.unit:
1727
                return ProjectResourceGrant.display_filesize(
1728
                    self.member_capacity)
1729
            else:
1730
                if math.isinf(self.member_capacity):
1731
                    return 'Unlimited'
1732
                else:
1733
                    return self.member_capacity
1734
        else:
1735
            return 'Unlimited'
1736

    
1737
    def __str__(self):
1738
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1739
                                        self.display_member_capacity())
1740

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

    
1765

    
1766
class ProjectManager(ForUpdateManager):
1767

    
1768
    def terminated_projects(self):
1769
        q = self.model.Q_TERMINATED
1770
        return self.filter(q)
1771

    
1772
    def not_terminated_projects(self):
1773
        q = ~self.model.Q_TERMINATED
1774
        return self.filter(q)
1775

    
1776
    def deactivated_projects(self):
1777
        q = self.model.Q_DEACTIVATED
1778
        return self.filter(q)
1779

    
1780
    def expired_projects(self):
1781
        q = (~Q(state=Project.TERMINATED) &
1782
             Q(application__end_date__lt=datetime.now()))
1783
        return self.filter(q)
1784

    
1785
    def search_by_name(self, *search_strings):
1786
        q = Q()
1787
        for s in search_strings:
1788
            q = q | Q(name__icontains=s)
1789
        return self.filter(q)
1790

    
1791

    
1792
class Project(models.Model):
1793

    
1794
    id = models.OneToOneField(Chain,
1795
                              related_name='chained_project',
1796
                              db_column='id',
1797
                              primary_key=True)
1798

    
1799
    application = models.OneToOneField(
1800
        ProjectApplication,
1801
        related_name='project')
1802
    last_approval_date = models.DateTimeField(null=True)
1803

    
1804
    members = models.ManyToManyField(
1805
        AstakosUser,
1806
        through='ProjectMembership')
1807

    
1808
    deactivation_reason = models.CharField(max_length=255, null=True)
1809
    deactivation_date = models.DateTimeField(null=True)
1810

    
1811
    creation_date = models.DateTimeField(auto_now_add=True)
1812
    name = models.CharField(
1813
        max_length=80,
1814
        null=True,
1815
        db_index=True,
1816
        unique=True)
1817

    
1818
    APPROVED = 1
1819
    SUSPENDED = 10
1820
    TERMINATED = 100
1821

    
1822
    state = models.IntegerField(default=APPROVED,
1823
                                db_index=True)
1824

    
1825
    objects = ProjectManager()
1826

    
1827
    # Compiled queries
1828
    Q_TERMINATED = Q(state=TERMINATED)
1829
    Q_SUSPENDED = Q(state=SUSPENDED)
1830
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1831

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

    
1836
    __repr__ = __str__
1837

    
1838
    def __unicode__(self):
1839
        return _("<project %s '%s'>") % (self.id, self.application.name)
1840

    
1841
    STATE_DISPLAY = {
1842
        APPROVED:   'Active',
1843
        SUSPENDED:  'Suspended',
1844
        TERMINATED: 'Terminated'
1845
    }
1846

    
1847
    def state_display(self):
1848
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1849

    
1850
    def expiration_info(self):
1851
        return (str(self.id), self.name, self.state_display(),
1852
                str(self.application.end_date))
1853

    
1854
    def is_deactivated(self, reason=None):
1855
        if reason is not None:
1856
            return self.state == reason
1857

    
1858
        return self.state != self.APPROVED
1859

    
1860
    ### Deactivation calls
1861

    
1862
    def terminate(self):
1863
        self.deactivation_reason = 'TERMINATED'
1864
        self.deactivation_date = datetime.now()
1865
        self.state = self.TERMINATED
1866
        self.name = None
1867
        self.save()
1868

    
1869
    def suspend(self):
1870
        self.deactivation_reason = 'SUSPENDED'
1871
        self.deactivation_date = datetime.now()
1872
        self.state = self.SUSPENDED
1873
        self.save()
1874

    
1875
    def resume(self):
1876
        self.deactivation_reason = None
1877
        self.deactivation_date = None
1878
        self.state = self.APPROVED
1879
        self.save()
1880

    
1881
    ### Logical checks
1882

    
1883
    def is_inconsistent(self):
1884
        now = datetime.now()
1885
        dates = [self.creation_date,
1886
                 self.last_approval_date,
1887
                 self.deactivation_date]
1888
        return any([date > now for date in dates])
1889

    
1890
    def is_approved(self):
1891
        return self.state == self.APPROVED
1892

    
1893
    @property
1894
    def is_alive(self):
1895
        return not self.is_terminated
1896

    
1897
    @property
1898
    def is_terminated(self):
1899
        return self.is_deactivated(self.TERMINATED)
1900

    
1901
    @property
1902
    def is_suspended(self):
1903
        return self.is_deactivated(self.SUSPENDED)
1904

    
1905
    def violates_resource_grants(self):
1906
        return False
1907

    
1908
    def violates_members_limit(self, adding=0):
1909
        application = self.application
1910
        limit = application.limit_on_members_number
1911
        if limit is None:
1912
            return False
1913
        return (len(self.approved_members) + adding > limit)
1914

    
1915
    ### Other
1916

    
1917
    def count_pending_memberships(self):
1918
        memb_set = self.projectmembership_set
1919
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1920
        return memb_count
1921

    
1922
    def count_actually_accepted_memberships(self):
1923
        memb_set = self.projectmembership_set
1924
        memb_count = memb_set.filter(state=ProjectMembership.LEAVE_REQUESTED)
1925
        memb_count = memb_set.filter(state=ProjectMembership.ACCEPTED).count()
1926
        return memb_count
1927

    
1928
    def members_count(self):
1929
        return self.approved_memberships.count()
1930

    
1931
    @property
1932
    def approved_memberships(self):
1933
        query = ProjectMembership.Q_ACCEPTED_STATES
1934
        return self.projectmembership_set.filter(query)
1935

    
1936
    @property
1937
    def approved_members(self):
1938
        return [m.person for m in self.approved_memberships]
1939

    
1940

    
1941
CHAIN_STATE = {
1942
    (Project.APPROVED, ProjectApplication.PENDING):   Chain.APPROVED_PENDING,
1943
    (Project.APPROVED, ProjectApplication.APPROVED):  Chain.APPROVED,
1944
    (Project.APPROVED, ProjectApplication.DENIED):    Chain.APPROVED,
1945
    (Project.APPROVED, ProjectApplication.DISMISSED): Chain.APPROVED,
1946
    (Project.APPROVED, ProjectApplication.CANCELLED): Chain.APPROVED,
1947

    
1948
    (Project.SUSPENDED, ProjectApplication.PENDING):   Chain.SUSPENDED_PENDING,
1949
    (Project.SUSPENDED, ProjectApplication.APPROVED):  Chain.SUSPENDED,
1950
    (Project.SUSPENDED, ProjectApplication.DENIED):    Chain.SUSPENDED,
1951
    (Project.SUSPENDED, ProjectApplication.DISMISSED): Chain.SUSPENDED,
1952
    (Project.SUSPENDED, ProjectApplication.CANCELLED): Chain.SUSPENDED,
1953

    
1954
    (Project.TERMINATED, ProjectApplication.PENDING): Chain.TERMINATED_PENDING,
1955
    (Project.TERMINATED, ProjectApplication.APPROVED):  Chain.TERMINATED,
1956
    (Project.TERMINATED, ProjectApplication.DENIED):    Chain.TERMINATED,
1957
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1958
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1959

    
1960
    (None, ProjectApplication.PENDING):   Chain.PENDING,
1961
    (None, ProjectApplication.DENIED):    Chain.DENIED,
1962
    (None, ProjectApplication.DISMISSED): Chain.DISMISSED,
1963
    (None, ProjectApplication.CANCELLED): Chain.CANCELLED,
1964
}
1965

    
1966

    
1967
class ProjectMembershipManager(ForUpdateManager):
1968

    
1969
    def any_accepted(self):
1970
        q = self.model.Q_ACTUALLY_ACCEPTED
1971
        return self.filter(q)
1972

    
1973
    def actually_accepted(self):
1974
        q = self.model.Q_ACTUALLY_ACCEPTED
1975
        return self.filter(q)
1976

    
1977
    def requested(self):
1978
        return self.filter(state=ProjectMembership.REQUESTED)
1979

    
1980
    def suspended(self):
1981
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1982

    
1983

    
1984
class ProjectMembership(models.Model):
1985

    
1986
    person = models.ForeignKey(AstakosUser)
1987
    request_date = models.DateTimeField(auto_now_add=True)
1988
    project = models.ForeignKey(Project)
1989

    
1990
    REQUESTED = 0
1991
    ACCEPTED = 1
1992
    LEAVE_REQUESTED = 5
1993
    # User deactivation
1994
    USER_SUSPENDED = 10
1995

    
1996
    REMOVED = 200
1997

    
1998
    ASSOCIATED_STATES = set([REQUESTED,
1999
                             ACCEPTED,
2000
                             LEAVE_REQUESTED,
2001
                             USER_SUSPENDED,
2002
                             ])
2003

    
2004
    ACCEPTED_STATES = set([ACCEPTED,
2005
                           LEAVE_REQUESTED,
2006
                           USER_SUSPENDED,
2007
                           ])
2008

    
2009
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
2010

    
2011
    state = models.IntegerField(default=REQUESTED,
2012
                                db_index=True)
2013
    acceptance_date = models.DateTimeField(null=True, db_index=True)
2014
    leave_request_date = models.DateTimeField(null=True)
2015

    
2016
    objects = ProjectMembershipManager()
2017

    
2018
    # Compiled queries
2019
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2020
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2021

    
2022
    MEMBERSHIP_STATE_DISPLAY = {
2023
        REQUESTED:       _('Requested'),
2024
        ACCEPTED:        _('Accepted'),
2025
        LEAVE_REQUESTED: _('Leave Requested'),
2026
        USER_SUSPENDED:  _('Suspended'),
2027
        REMOVED:         _('Pending removal'),
2028
    }
2029

    
2030
    USER_FRIENDLY_STATE_DISPLAY = {
2031
        REQUESTED:       _('Join requested'),
2032
        ACCEPTED:        _('Accepted member'),
2033
        LEAVE_REQUESTED: _('Requested to leave'),
2034
        USER_SUSPENDED:  _('Suspended member'),
2035
        REMOVED:         _('Pending removal'),
2036
    }
2037

    
2038
    def state_display(self):
2039
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2040

    
2041
    def user_friendly_state_display(self):
2042
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2043

    
2044
    class Meta:
2045
        unique_together = ("person", "project")
2046
        #index_together = [["project", "state"]]
2047

    
2048
    def __str__(self):
2049
        return uenc(_("<'%s' membership in '%s'>") %
2050
                    (self.person.username, self.project))
2051

    
2052
    __repr__ = __str__
2053

    
2054
    def __init__(self, *args, **kwargs):
2055
        self.state = self.REQUESTED
2056
        super(ProjectMembership, self).__init__(*args, **kwargs)
2057

    
2058
    def _set_history_item(self, reason, date=None):
2059
        if isinstance(reason, basestring):
2060
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2061

    
2062
        history_item = ProjectMembershipHistory(
2063
            serial=self.id,
2064
            person=self.person_id,
2065
            project=self.project_id,
2066
            date=date or datetime.now(),
2067
            reason=reason)
2068
        history_item.save()
2069
        serial = history_item.id
2070

    
2071
    def can_accept(self):
2072
        return self.state == self.REQUESTED
2073

    
2074
    def accept(self):
2075
        if not self.can_accept():
2076
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2077
            raise AssertionError(m)
2078

    
2079
        now = datetime.now()
2080
        self.acceptance_date = now
2081
        self._set_history_item(reason='ACCEPT', date=now)
2082
        self.state = self.ACCEPTED
2083
        self.save()
2084

    
2085
    def can_leave(self):
2086
        return self.state in self.ACCEPTED_STATES
2087

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

    
2094
        self.leave_request_date = datetime.now()
2095
        self.state = self.LEAVE_REQUESTED
2096
        self.save()
2097

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

    
2101
    def leave_request_deny(self):
2102
        if not self.can_deny_leave():
2103
            m = _("%s: attempt to deny 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_cancel_leave(self):
2112
        return self.state == self.LEAVE_REQUESTED
2113

    
2114
    def leave_request_cancel(self):
2115
        if not self.can_cancel_leave():
2116
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2117
                self, self.state)
2118
            raise AssertionError(m)
2119

    
2120
        self.leave_request_date = None
2121
        self.state = self.ACCEPTED
2122
        self.save()
2123

    
2124
    def can_remove(self):
2125
        return self.state in self.ACCEPTED_STATES
2126

    
2127
    def remove(self):
2128
        if not self.can_remove():
2129
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2130
            raise AssertionError(m)
2131

    
2132
        self._set_history_item(reason='REMOVE')
2133
        self.delete()
2134

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

    
2138
    def reject(self):
2139
        if not self.can_reject():
2140
            m = _("%s: attempt to reject 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='REJECT')
2146
        self.delete()
2147

    
2148
    def can_cancel(self):
2149
        return self.state == self.REQUESTED
2150

    
2151
    def cancel(self):
2152
        if not self.can_cancel():
2153
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2154
            raise AssertionError(m)
2155

    
2156
        # rejected requests don't need sync,
2157
        # because they were never effected
2158
        self._set_history_item(reason='CANCEL')
2159
        self.delete()
2160

    
2161

    
2162
class Serial(models.Model):
2163
    serial = models.AutoField(primary_key=True)
2164

    
2165

    
2166
class ProjectMembershipHistory(models.Model):
2167
    reasons_list = ['ACCEPT', 'REJECT', 'REMOVE']
2168
    reasons = dict((k, v) for v, k in enumerate(reasons_list))
2169

    
2170
    person = models.BigIntegerField()
2171
    project = models.BigIntegerField()
2172
    date = models.DateTimeField(auto_now_add=True)
2173
    reason = models.IntegerField()
2174
    serial = models.BigIntegerField()
2175

    
2176

    
2177
### SIGNALS ###
2178
################
2179

    
2180
def create_astakos_user(u):
2181
    try:
2182
        AstakosUser.objects.get(user_ptr=u.pk)
2183
    except AstakosUser.DoesNotExist:
2184
        extended_user = AstakosUser(user_ptr_id=u.pk)
2185
        extended_user.__dict__.update(u.__dict__)
2186
        extended_user.save()
2187
        if not extended_user.has_auth_provider('local'):
2188
            extended_user.add_auth_provider('local')
2189
    except BaseException, e:
2190
        logger.exception(e)
2191

    
2192

    
2193
def fix_superusers():
2194
    # Associate superusers with AstakosUser
2195
    admins = User.objects.filter(is_superuser=True)
2196
    for u in admins:
2197
        create_astakos_user(u)
2198

    
2199

    
2200
def user_post_save(sender, instance, created, **kwargs):
2201
    if not created:
2202
        return
2203
    create_astakos_user(instance)
2204
post_save.connect(user_post_save, sender=User)
2205

    
2206

    
2207
def astakosuser_post_save(sender, instance, created, **kwargs):
2208
    pass
2209

    
2210
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2211

    
2212

    
2213
def resource_post_save(sender, instance, created, **kwargs):
2214
    pass
2215

    
2216
post_save.connect(resource_post_save, sender=Resource)
2217

    
2218

    
2219
def renew_token(sender, instance, **kwargs):
2220
    if not instance.auth_token:
2221
        instance.renew_token()
2222
pre_save.connect(renew_token, sender=AstakosUser)
2223
pre_save.connect(renew_token, sender=Component)