Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 3848b521

History | View | Annotate | Download (74.3 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 accepted(self):
319
        return self.filter(moderated=True, is_rejected=False)
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_accepted(self):
486
        return self.moderated and not self.is_rejected
487

    
488
    def is_project_admin(self, application_id=None):
489
        return self.uuid in astakos_settings.PROJECT_ADMINS
490

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

    
498
    @property
499
    def policies(self):
500
        return self.astakosuserquota_set.select_related().all()
501

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

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

    
520
    def save(self, update_timestamps=True, **kwargs):
521
        if update_timestamps:
522
            if not self.id:
523
                self.date_joined = datetime.now()
524
            self.updated = datetime.now()
525

    
526
        self.update_uuid()
527

    
528
        if not self.verification_code:
529
            self.renew_verification_code()
530

    
531
        # username currently matches email
532
        if self.username != self.email.lower():
533
            self.username = self.email.lower()
534

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

    
537
    def renew_verification_code(self):
538
        self.verification_code = str(uuid.uuid4())
539
        logger.info("Verification code renewed for %s" % self.log_display)
540

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

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

    
560
    def token_expired(self):
561
        return self.auth_token_expires < datetime.now()
562

    
563
    def flush_sessions(self, current_key=None):
564
        q = self.sessions
565
        if current_key:
566
            q = q.exclude(session_key=current_key)
567

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

    
577
    def __unicode__(self):
578
        return '%s (%s)' % (self.realname, self.email)
579

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

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

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

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

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

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

    
641
    def can_change_password(self):
642
        return self.has_auth_provider('local', auth_backend='astakos')
643

    
644
    def can_change_email(self):
645
        if not self.has_auth_provider('local'):
646
            return True
647

    
648
        local = self.get_auth_provider('local')._instance
649
        return local.auth_backend == 'astakos'
650

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

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

    
662
    def has_auth_provider(self, provider, **kwargs):
663
        return bool(self.auth_providers.active().filter(module=provider,
664
                                                        **kwargs).count())
665

    
666
    def get_required_providers(self, **kwargs):
667
        return auth.REQUIRED_PROVIDERS.keys()
668

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

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

    
687
        for p in providers:
688
            if p.get_add_policy:
689
                available.append(p)
690
        return available
691

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

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

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

    
714
        modules = astakos_settings.IM_MODULES
715

    
716
        def key(p):
717
            if not p.module in modules:
718
                return 100
719
            return modules.index(p.module)
720

    
721
        providers = sorted(providers, key=key)
722
        return providers
723

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

    
730
    def add_auth_provider(self, module='local', identifier=None, **params):
731
        provider = auth.get_provider(module, self, identifier, **params)
732
        provider.add_to_user()
733

    
734
    def get_resend_activation_url(self):
735
        return reverse('send_activation', kwargs={'user_id': self.pk})
736

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

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

    
749
    def get_inactive_message(self, provider_module, identifier=None):
750
        provider = self.get_auth_provider(provider_module, identifier)
751

    
752
        msg_extra = ''
753
        message = ''
754

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

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

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

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

    
783
    def owns_project(self, project):
784
        return project.application.owner == self
785

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

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

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

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

    
820

    
821
class AstakosUserAuthProviderManager(models.Manager):
822

    
823
    def active(self, **filters):
824
        return self.filter(active=True, **filters)
825

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

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

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

    
849

    
850
class AuthProviderPolicyProfileManager(models.Manager):
851

    
852
    def active(self):
853
        return self.filter(active=True)
854

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

    
861
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
862
            policies.update(profile.policies)
863

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

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

    
885

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

    
892
    # apply policies to all providers excluding the one set in provider field
893
    is_exclusive = models.BooleanField(default=False)
894

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

    
904
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
905
                     'automoderate')
906

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

    
913
    objects = AuthProviderPolicyProfileManager()
914

    
915
    class Meta:
916
        ordering = ['priority']
917

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

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

    
934

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

    
953
    objects = AstakosUserAuthProviderManager()
954

    
955
    class Meta:
956
        unique_together = (('identifier', 'module', 'user'), )
957
        ordering = ('module', 'created')
958

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

    
968
        for key,value in self.info.iteritems():
969
            setattr(self, 'info_%s' % key, value)
970

    
971
    @property
972
    def settings(self):
973
        extra_data = {}
974

    
975
        info_data = {}
976
        if self.info_data:
977
            info_data = json.loads(self.info_data)
978

    
979
        extra_data['info'] = info_data
980

    
981
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
982
            extra_data[key] = getattr(self, key)
983

    
984
        extra_data['instance'] = self
985
        return auth.get_provider(self.module, self.user,
986
                                           self.identifier, **extra_data)
987

    
988
    def __repr__(self):
989
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
990

    
991
    def __unicode__(self):
992
        if self.identifier:
993
            return "%s:%s" % (self.module, self.identifier)
994
        if self.auth_backend:
995
            return "%s:%s" % (self.module, self.auth_backend)
996
        return self.module
997

    
998
    def save(self, *args, **kwargs):
999
        self.info_data = json.dumps(self.info)
1000
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1001

    
1002

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

    
1030
    update_or_create = _update_or_create
1031

    
1032

    
1033
class AstakosUserQuota(models.Model):
1034
    objects = ExtendedManager()
1035
    capacity = intDecimalField()
1036
    resource = models.ForeignKey(Resource)
1037
    user = models.ForeignKey(AstakosUser)
1038

    
1039
    class Meta:
1040
        unique_together = ("resource", "user")
1041

    
1042

    
1043
class ApprovalTerms(models.Model):
1044
    """
1045
    Model for approval terms
1046
    """
1047

    
1048
    date = models.DateTimeField(
1049
        _('Issue date'), db_index=True, auto_now_add=True)
1050
    location = models.CharField(_('Terms location'), max_length=255)
1051

    
1052

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

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

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

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

    
1079

    
1080
class EmailChangeManager(models.Manager):
1081

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

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

1091
        If the key is not valid or has expired, return ``None``.
1092

1093
        If the key is valid but the ``User`` is already active,
1094
        return ``None``.
1095

1096
        After successful email change the activation record is deleted.
1097

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

    
1127

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

    
1140
    objects = EmailChangeManager()
1141

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

    
1146
    def activation_key_expired(self):
1147
        expiration_date = timedelta(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(_('Third-party identifier'), max_length=255, null=True, blank=True)
1183
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1184
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1185
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1186
                                  null=True)
1187
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1188
                                 null=True)
1189
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1190
                                   null=True)
1191
    username = models.CharField(_('username'), max_length=30, unique=True,
1192
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1193
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1194
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1195
    info = models.TextField(default="", null=True, blank=True)
1196

    
1197
    class Meta:
1198
        unique_together = ("provider", "third_party_identifier")
1199

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

    
1216
        return user
1217

    
1218
    @property
1219
    def realname(self):
1220
        return '%s %s' %(self.first_name, self.last_name)
1221

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

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

    
1242
    def generate_token(self):
1243
        self.password = self.third_party_identifier
1244
        self.last_login = datetime.now()
1245
        self.token = default_token_generator.make_token(self)
1246

    
1247
    def existing_user(self):
1248
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1249
                                         auth_providers__identifier=self.third_party_identifier)
1250

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

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

    
1263

    
1264
class UserSetting(models.Model):
1265
    user = models.ForeignKey(AstakosUser)
1266
    setting = models.CharField(max_length=255)
1267
    value = models.IntegerField()
1268

    
1269
    objects = ForUpdateManager()
1270

    
1271
    class Meta:
1272
        unique_together = ("user", "setting")
1273

    
1274

    
1275
### PROJECTS ###
1276
################
1277

    
1278
class ChainManager(ForUpdateManager):
1279

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

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

    
1293
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1294
        chain_latest = dict(objs.values_list('chain', 'latest'))
1295

    
1296
        objs = ProjectApplication.objects.select_related('applicant')
1297
        apps = objs.in_bulk(chain_latest.values())
1298

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

    
1306
        return d
1307

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

    
1316

    
1317
class Chain(models.Model):
1318
    chain  =   models.AutoField(primary_key=True)
1319

    
1320
    def __str__(self):
1321
        return "%s" % (self.chain,)
1322

    
1323
    objects = ChainManager()
1324

    
1325
    PENDING            = 0
1326
    DENIED             = 3
1327
    DISMISSED          = 4
1328
    CANCELLED          = 5
1329

    
1330
    APPROVED           = 10
1331
    APPROVED_PENDING   = 11
1332
    SUSPENDED          = 12
1333
    SUSPENDED_PENDING  = 13
1334
    TERMINATED         = 14
1335
    TERMINATED_PENDING = 15
1336

    
1337
    PENDING_STATES = [PENDING,
1338
                      APPROVED_PENDING,
1339
                      SUSPENDED_PENDING,
1340
                      TERMINATED_PENDING,
1341
                      ]
1342

    
1343
    MODIFICATION_STATES = [APPROVED_PENDING,
1344
                           SUSPENDED_PENDING,
1345
                           TERMINATED_PENDING,
1346
                           ]
1347

    
1348
    RELEVANT_STATES = [PENDING,
1349
                       DENIED,
1350
                       APPROVED,
1351
                       APPROVED_PENDING,
1352
                       SUSPENDED,
1353
                       SUSPENDED_PENDING,
1354
                       TERMINATED_PENDING,
1355
                       ]
1356

    
1357
    SKIP_STATES = [DISMISSED,
1358
                   CANCELLED,
1359
                   TERMINATED]
1360

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

    
1374

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

    
1382
    @classmethod
1383
    def chain_state(cls, project, app):
1384
        p_state = project.state if project else None
1385
        return cls._chain_state(p_state, app.state)
1386

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

    
1393
    def last_application(self):
1394
        return self.chained_apps.order_by('-id')[0]
1395

    
1396
    def get_project(self):
1397
        try:
1398
            return self.chained_project
1399
        except Project.DoesNotExist:
1400
            return None
1401

    
1402
    def get_elements(self):
1403
        project = self.get_project()
1404
        app = self.last_application()
1405
        return project, app
1406

    
1407
    def get_state(self, project, app):
1408
        s = self.chain_state(project, app)
1409
        return s, project, app
1410

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

    
1415

    
1416
def new_chain():
1417
    c = Chain.objects.create()
1418
    return c
1419

    
1420

    
1421
class ProjectApplicationManager(ForUpdateManager):
1422

    
1423
    def user_visible_projects(self, *filters, **kw_filters):
1424
        model = self.model
1425
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1426

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

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

    
1445
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1446

    
1447
    def search_by_name(self, *search_strings):
1448
        q = Q()
1449
        for s in search_strings:
1450
            q = q | Q(name__icontains=s)
1451
        return self.filter(q)
1452

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

    
1459

    
1460
class ProjectApplication(models.Model):
1461
    applicant               =   models.ForeignKey(
1462
                                    AstakosUser,
1463
                                    related_name='projects_applied',
1464
                                    db_index=True)
1465

    
1466
    PENDING     =    0
1467
    APPROVED    =    1
1468
    REPLACED    =    2
1469
    DENIED      =    3
1470
    DISMISSED   =    4
1471
    CANCELLED   =    5
1472

    
1473
    state                   =   models.IntegerField(default=PENDING,
1474
                                                    db_index=True)
1475

    
1476
    owner                   =   models.ForeignKey(
1477
                                    AstakosUser,
1478
                                    related_name='projects_owned',
1479
                                    db_index=True)
1480

    
1481
    chain                   =   models.ForeignKey(Chain,
1482
                                                  related_name='chained_apps',
1483
                                                  db_column='chain')
1484
    precursor_application   =   models.ForeignKey('ProjectApplication',
1485
                                                  null=True,
1486
                                                  blank=True)
1487

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

    
1507
    objects                 =   ProjectApplicationManager()
1508

    
1509
    # Compiled queries
1510
    Q_PENDING  = Q(state=PENDING)
1511
    Q_APPROVED = Q(state=APPROVED)
1512
    Q_DENIED   = Q(state=DENIED)
1513

    
1514
    class Meta:
1515
        unique_together = ("chain", "id")
1516

    
1517
    def __unicode__(self):
1518
        return "%s applied by %s" % (self.name, self.applicant)
1519

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

    
1530
    @property
1531
    def log_display(self):
1532
        return "application %s (%s) for project %s" % (
1533
            self.id, self.name, self.chain)
1534

    
1535
    def state_display(self):
1536
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1537

    
1538
    def project_state_display(self):
1539
        try:
1540
            project = self.project
1541
            return project.state_display()
1542
        except Project.DoesNotExist:
1543
            return self.state_display()
1544

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

    
1551
    def members_count(self):
1552
        return self.project.approved_memberships.count()
1553

    
1554
    @property
1555
    def grants(self):
1556
        return self.projectresourcegrant_set.values('member_capacity',
1557
                                                    'resource__name')
1558

    
1559
    @property
1560
    def resource_policies(self):
1561
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1562

    
1563
    def set_resource_policies(self, policies):
1564
        for resource, uplimit in policies:
1565
            self.add_resource_policy(resource, uplimit)
1566

    
1567
    def pending_modifications_incl_me(self):
1568
        q = self.chained_applications()
1569
        q = q.filter(Q(state=self.PENDING))
1570
        return q
1571

    
1572
    def last_pending_incl_me(self):
1573
        try:
1574
            return self.pending_modifications_incl_me().order_by('-id')[0]
1575
        except IndexError:
1576
            return None
1577

    
1578
    def pending_modifications(self):
1579
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1580

    
1581
    def last_pending(self):
1582
        try:
1583
            return self.pending_modifications().order_by('-id')[0]
1584
        except IndexError:
1585
            return None
1586

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

    
1594
    def chained_applications(self):
1595
        return ProjectApplication.objects.filter(chain=self.chain)
1596

    
1597
    def is_latest(self):
1598
        return self.chained_applications().order_by('-id')[0] == self
1599

    
1600
    def has_pending_modifications(self):
1601
        return bool(self.last_pending())
1602

    
1603
    def denied_modifications(self):
1604
        q = self.chained_applications()
1605
        q = q.filter(Q(state=self.DENIED))
1606
        q = q.filter(~Q(id=self.id))
1607
        return q
1608

    
1609
    def last_denied(self):
1610
        try:
1611
            return self.denied_modifications().order_by('-id')[0]
1612
        except IndexError:
1613
            return None
1614

    
1615
    def has_denied_modifications(self):
1616
        return bool(self.last_denied())
1617

    
1618
    def is_applied(self):
1619
        try:
1620
            self.project
1621
            return True
1622
        except Project.DoesNotExist:
1623
            return False
1624

    
1625
    def get_project(self):
1626
        try:
1627
            return Project.objects.get(id=self.chain)
1628
        except Project.DoesNotExist:
1629
            return None
1630

    
1631
    def project_exists(self):
1632
        return self.get_project() is not None
1633

    
1634
    def can_cancel(self):
1635
        return self.state == self.PENDING
1636

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

    
1643
        self.state = self.CANCELLED
1644
        self.save()
1645

    
1646
    def can_dismiss(self):
1647
        return self.state == self.DENIED
1648

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

    
1655
        self.state = self.DISMISSED
1656
        self.save()
1657

    
1658
    def can_deny(self):
1659
        return self.state == self.PENDING
1660

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

    
1667
        self.state = self.DENIED
1668
        self.response_date = datetime.now()
1669
        self.response = reason
1670
        self.save()
1671

    
1672
    def can_approve(self):
1673
        return self.state == self.PENDING
1674

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

    
1681
        now = datetime.now()
1682
        self.state = self.APPROVED
1683
        self.response_date = now
1684
        self.response = reason
1685
        self.save()
1686

    
1687
        project = self.get_project()
1688
        if project is None:
1689
            project = Project(id=self.chain)
1690

    
1691
        project.name = self.name
1692
        project.application = self
1693
        project.last_approval_date = now
1694
        project.save()
1695
        if project.is_deactivated():
1696
            project.resume()
1697
        return project
1698

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

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

    
1709
class ProjectResourceGrant(models.Model):
1710

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

    
1717
    objects = ExtendedManager()
1718

    
1719
    class Meta:
1720
        unique_together = ("resource", "project_application")
1721

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

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

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

    
1763

    
1764
class ProjectManager(ForUpdateManager):
1765

    
1766
    def terminated_projects(self):
1767
        q = self.model.Q_TERMINATED
1768
        return self.filter(q)
1769

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

    
1774
    def deactivated_projects(self):
1775
        q = self.model.Q_DEACTIVATED
1776
        return self.filter(q)
1777

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

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

    
1789

    
1790
class Project(models.Model):
1791

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

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

    
1802
    members                     =   models.ManyToManyField(
1803
                                            AstakosUser,
1804
                                            through='ProjectMembership')
1805

    
1806
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1807
    deactivation_date           =   models.DateTimeField(null=True)
1808

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

    
1816
    APPROVED    = 1
1817
    SUSPENDED   = 10
1818
    TERMINATED  = 100
1819

    
1820
    state                       =   models.IntegerField(default=APPROVED,
1821
                                                        db_index=True)
1822

    
1823
    objects     =   ProjectManager()
1824

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

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

    
1834
    __repr__ = __str__
1835

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

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

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

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

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

    
1856
        return self.state != self.APPROVED
1857

    
1858
    ### Deactivation calls
1859

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

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

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

    
1879
    ### Logical checks
1880

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

    
1888
    def is_approved(self):
1889
        return self.state == self.APPROVED
1890

    
1891
    @property
1892
    def is_alive(self):
1893
        return not self.is_terminated
1894

    
1895
    @property
1896
    def is_terminated(self):
1897
        return self.is_deactivated(self.TERMINATED)
1898

    
1899
    @property
1900
    def is_suspended(self):
1901
        return self.is_deactivated(self.SUSPENDED)
1902

    
1903
    def violates_resource_grants(self):
1904
        return False
1905

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

    
1913

    
1914
    ### Other
1915

    
1916
    def count_pending_memberships(self):
1917
        return self.projectmembership_set.requested().count()
1918

    
1919
    def members_count(self):
1920
        return self.approved_memberships.count()
1921

    
1922
    @property
1923
    def approved_memberships(self):
1924
        query = ProjectMembership.Q_ACCEPTED_STATES
1925
        return self.projectmembership_set.filter(query)
1926

    
1927
    @property
1928
    def approved_members(self):
1929
        return [m.person for m in self.approved_memberships]
1930

    
1931

    
1932
CHAIN_STATE = {
1933
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1934
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1935
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1936
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1937
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1938

    
1939
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1940
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1941
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1942
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1943
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1944

    
1945
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1946
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1947
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1948
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1949
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1950

    
1951
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1952
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1953
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1954
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1955
    }
1956

    
1957

    
1958
class ProjectMembershipManager(ForUpdateManager):
1959

    
1960
    def any_accepted(self):
1961
        q = self.model.Q_ACCEPTED_STATES
1962
        return self.filter(q)
1963

    
1964
    def actually_accepted(self):
1965
        q = self.model.Q_ACTUALLY_ACCEPTED
1966
        return self.filter(q)
1967

    
1968
    def requested(self):
1969
        return self.filter(state=ProjectMembership.REQUESTED)
1970

    
1971
    def suspended(self):
1972
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1973

    
1974
class ProjectMembership(models.Model):
1975

    
1976
    person              =   models.ForeignKey(AstakosUser)
1977
    request_date        =   models.DateTimeField(auto_now_add=True)
1978
    project             =   models.ForeignKey(Project)
1979

    
1980
    REQUESTED           =   0
1981
    ACCEPTED            =   1
1982
    LEAVE_REQUESTED     =   5
1983
    # User deactivation
1984
    USER_SUSPENDED      =   10
1985

    
1986
    REMOVED             =   200
1987

    
1988
    ASSOCIATED_STATES   =   set([REQUESTED,
1989
                                 ACCEPTED,
1990
                                 LEAVE_REQUESTED,
1991
                                 USER_SUSPENDED,
1992
                                 ])
1993

    
1994
    ACCEPTED_STATES     =   set([ACCEPTED,
1995
                                 LEAVE_REQUESTED,
1996
                                 USER_SUSPENDED,
1997
                                 ])
1998

    
1999
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2000

    
2001
    state               =   models.IntegerField(default=REQUESTED,
2002
                                                db_index=True)
2003
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
2004
    leave_request_date  =   models.DateTimeField(null=True)
2005

    
2006
    objects     =   ProjectMembershipManager()
2007

    
2008
    # Compiled queries
2009
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
2010
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2011

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

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

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

    
2031
    def user_friendly_state_display(self):
2032
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2033

    
2034
    class Meta:
2035
        unique_together = ("person", "project")
2036
        #index_together = [["project", "state"]]
2037

    
2038
    def __str__(self):
2039
        return uenc(_("<'%s' membership in '%s'>") % (
2040
                self.person.username, self.project))
2041

    
2042
    __repr__ = __str__
2043

    
2044
    def __init__(self, *args, **kwargs):
2045
        self.state = self.REQUESTED
2046
        super(ProjectMembership, self).__init__(*args, **kwargs)
2047

    
2048
    def _set_history_item(self, reason, date=None):
2049
        if isinstance(reason, basestring):
2050
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2051

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

    
2061
    def can_accept(self):
2062
        return self.state == self.REQUESTED
2063

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

    
2069
        now = datetime.now()
2070
        self.acceptance_date = now
2071
        self._set_history_item(reason='ACCEPT', date=now)
2072
        self.state = self.ACCEPTED
2073
        self.save()
2074

    
2075
    def can_leave(self):
2076
        return self.state in self.ACCEPTED_STATES
2077

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

    
2084
        self.leave_request_date = datetime.now()
2085
        self.state = self.LEAVE_REQUESTED
2086
        self.save()
2087

    
2088
    def can_deny_leave(self):
2089
        return self.state == self.LEAVE_REQUESTED
2090

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

    
2097
        self.leave_request_date = None
2098
        self.state = self.ACCEPTED
2099
        self.save()
2100

    
2101
    def can_cancel_leave(self):
2102
        return self.state == self.LEAVE_REQUESTED
2103

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

    
2110
        self.leave_request_date = None
2111
        self.state = self.ACCEPTED
2112
        self.save()
2113

    
2114
    def can_remove(self):
2115
        return self.state in self.ACCEPTED_STATES
2116

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

    
2122
        self._set_history_item(reason='REMOVE')
2123
        self.delete()
2124

    
2125
    def can_reject(self):
2126
        return self.state == self.REQUESTED
2127

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

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

    
2138
    def can_cancel(self):
2139
        return self.state == self.REQUESTED
2140

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

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

    
2151

    
2152
class Serial(models.Model):
2153
    serial  =   models.AutoField(primary_key=True)
2154

    
2155

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

    
2160
    person  =   models.BigIntegerField()
2161
    project =   models.BigIntegerField()
2162
    date    =   models.DateTimeField(auto_now_add=True)
2163
    reason  =   models.IntegerField()
2164
    serial  =   models.BigIntegerField()
2165

    
2166
### SIGNALS ###
2167
################
2168

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

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

    
2187
def user_post_save(sender, instance, created, **kwargs):
2188
    if not created:
2189
        return
2190
    create_astakos_user(instance)
2191
post_save.connect(user_post_save, sender=User)
2192

    
2193
def astakosuser_post_save(sender, instance, created, **kwargs):
2194
    pass
2195

    
2196
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2197

    
2198
def resource_post_save(sender, instance, created, **kwargs):
2199
    pass
2200

    
2201
post_save.connect(resource_post_save, sender=Resource)
2202

    
2203
def renew_token(sender, instance, **kwargs):
2204
    if not instance.auth_token:
2205
        instance.renew_token()
2206
pre_save.connect(renew_token, sender=AstakosUser)
2207
pre_save.connect(renew_token, sender=Component)