Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 0446cc58

History | View | Annotate | Download (74 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 time import asctime
42
from datetime import datetime, timedelta
43
from base64 import b64encode
44
from urllib import quote
45
from random import randint
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
class Component(models.Model):
97
    name = models.CharField(_('Name'), max_length=255, unique=True,
98
                            db_index=True)
99
    url = models.CharField(_('Component url'), max_length=255, null=True,
100
                           help_text=_("URL the component is accessible from"))
101
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
102
                                  null=True, blank=True)
103
    auth_token_created = models.DateTimeField(_('Token creation date'),
104
                                              null=True)
105
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
106
                                              null=True)
107

    
108
    def renew_token(self, expiration_date=None):
109
        md5 = hashlib.md5()
110
        md5.update(self.name.encode('ascii', 'ignore'))
111
        md5.update(self.url.encode('ascii', 'ignore'))
112
        md5.update(asctime())
113

    
114
        self.auth_token = b64encode(md5.digest())
115
        self.auth_token_created = datetime.now()
116
        if expiration_date:
117
            self.auth_token_expires = expiration_date
118
        else:
119
            self.auth_token_expires = None
120

    
121
    def __str__(self):
122
        return self.name
123

    
124
    @classmethod
125
    def catalog(cls, orderfor=None):
126
        catalog = {}
127
        components = list(cls.objects.all())
128
        default_metadata = presentation.COMPONENTS
129
        metadata = {}
130

    
131
        for component in components:
132
            d = {'url': component.url,
133
                 'name': component.name}
134
            if component.name in default_metadata:
135
                metadata[component.name] = default_metadata.get(component.name)
136
                metadata[component.name].update(d)
137
            else:
138
                metadata[component.name] = d
139

    
140

    
141
        def component_by_order(s):
142
            return s[1].get('order')
143

    
144
        def component_by_dashboard_order(s):
145
            return s[1].get('dashboard').get('order')
146

    
147
        metadata = dict_merge(metadata,
148
                              astakos_settings.COMPONENTS_META)
149

    
150
        for component, info in metadata.iteritems():
151
            default_meta = presentation.component_defaults(component)
152
            base_meta = metadata.get(component, {})
153
            settings_meta = astakos_settings.COMPONENTS_META.get(component, {})
154
            component_meta = dict_merge(default_meta, base_meta)
155
            meta = dict_merge(component_meta, settings_meta)
156
            catalog[component] = meta
157

    
158
        order_key = component_by_order
159
        if orderfor == 'dashboard':
160
            order_key = component_by_dashboard_order
161

    
162
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
163
                                             key=order_key))
164
        return ordered_catalog
165

    
166

    
167
_presentation_data = {}
168

    
169

    
170
def get_presentation(resource):
171
    global _presentation_data
172
    resource_presentation = _presentation_data.get(resource, {})
173
    if not resource_presentation:
174
        resources_presentation = presentation.RESOURCES.get('resources', {})
175
        resource_presentation = resources_presentation.get(resource, {})
176
        _presentation_data[resource] = resource_presentation
177
    return resource_presentation
178

    
179

    
180
class Service(models.Model):
181
    component = models.ForeignKey(Component)
182
    name = models.CharField(max_length=255, unique=True)
183
    type = models.CharField(max_length=255)
184

    
185

    
186
class Endpoint(models.Model):
187
    service = models.ForeignKey(Service, related_name='endpoints')
188

    
189

    
190
class EndpointData(models.Model):
191
    endpoint = models.ForeignKey(Endpoint, related_name='data')
192
    key = models.CharField(max_length=255)
193
    value = models.CharField(max_length=1024)
194

    
195
    class Meta:
196
        unique_together = (('endpoint', 'key'),)
197

    
198

    
199
class Resource(models.Model):
200
    name = models.CharField(_('Name'), max_length=255, unique=True)
201
    desc = models.TextField(_('Description'), null=True)
202
    service_type = models.CharField(_('Type'), max_length=255)
203
    unit = models.CharField(_('Unit'), null=True, max_length=255)
204
    uplimit = intDecimalField(default=0)
205
    allow_in_projects = models.BooleanField(default=True)
206

    
207
    objects = ForUpdateManager()
208

    
209
    def __str__(self):
210
        return self.name
211

    
212
    def full_name(self):
213
        return str(self)
214

    
215
    def get_info(self):
216
        return {'service_type': self.service_type,
217
                'description': self.desc,
218
                'unit': self.unit,
219
                'allow_in_projects': self.allow_in_projects,
220
                }
221

    
222
    @property
223
    def group(self):
224
        default = self.name
225
        return get_presentation(str(self)).get('group', default)
226

    
227
    @property
228
    def help_text(self):
229
        default = "%s resource" % self.name
230
        return get_presentation(str(self)).get('help_text', default)
231

    
232
    @property
233
    def help_text_input_each(self):
234
        default = "%s resource" % self.name
235
        return get_presentation(str(self)).get('help_text_input_each', default)
236

    
237
    @property
238
    def is_abbreviation(self):
239
        return get_presentation(str(self)).get('is_abbreviation', False)
240

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

    
246
    @property
247
    def placeholder(self):
248
        return get_presentation(str(self)).get('placeholder', self.unit)
249

    
250
    @property
251
    def verbose_name(self):
252
        return get_presentation(str(self)).get('verbose_name', self.name)
253

    
254
    @property
255
    def display_name(self):
256
        name = self.verbose_name
257
        if self.is_abbreviation:
258
            name = name.upper()
259
        return name
260

    
261
    @property
262
    def pluralized_display_name(self):
263
        if not self.unit:
264
            return '%ss' % self.display_name
265
        return self.display_name
266

    
267
def get_resource_names():
268
    _RESOURCE_NAMES = []
269
    resources = Resource.objects.select_related('service').all()
270
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
271
    return _RESOURCE_NAMES
272

    
273

    
274
class AstakosUserManager(UserManager):
275

    
276
    def get_auth_provider_user(self, provider, **kwargs):
277
        """
278
        Retrieve AstakosUser instance associated with the specified third party
279
        id.
280
        """
281
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
282
                          kwargs.iteritems()))
283
        return self.get(auth_providers__module=provider, **kwargs)
284

    
285
    def get_by_email(self, email):
286
        return self.get(email=email)
287

    
288
    def get_by_identifier(self, email_or_username, **kwargs):
289
        try:
290
            return self.get(email__iexact=email_or_username, **kwargs)
291
        except AstakosUser.DoesNotExist:
292
            return self.get(username__iexact=email_or_username, **kwargs)
293

    
294
    def user_exists(self, email_or_username, **kwargs):
295
        qemail = Q(email__iexact=email_or_username)
296
        qusername = Q(username__iexact=email_or_username)
297
        qextra = Q(**kwargs)
298
        return self.filter((qemail | qusername) & qextra).exists()
299

    
300
    def verified_user_exists(self, email_or_username):
301
        return self.user_exists(email_or_username, email_verified=True)
302

    
303
    def verified(self):
304
        return self.filter(email_verified=True)
305

    
306
    def uuid_catalog(self, l=None):
307
        """
308
        Returns a uuid to username mapping for the uuids appearing in l.
309
        If l is None returns the mapping for all existing users.
310
        """
311
        q = self.filter(uuid__in=l) if l != None else self
312
        return dict(q.values_list('uuid', 'username'))
313

    
314
    def displayname_catalog(self, l=None):
315
        """
316
        Returns a username to uuid mapping for the usernames appearing in l.
317
        If l is None returns the mapping for all existing users.
318
        """
319
        if l is not None:
320
            lmap = dict((x.lower(), x) for x in l)
321
            q = self.filter(username__in=lmap.keys())
322
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
323
        else:
324
            q = self
325
            values = self.values_list('username', 'uuid')
326
        return dict(values)
327

    
328

    
329

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

    
337
    #for invitations
338
    user_level = astakos_settings.DEFAULT_USER_LEVEL
339
    level = models.IntegerField(_('Inviter level'), default=user_level)
340
    invitations = models.IntegerField(
341
        _('Invitations left'), default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
342

    
343
    auth_token = models.CharField(_('Authentication Token'),
344
                                  max_length=32,
345
                                  null=True,
346
                                  blank=True,
347
                                  help_text = _('Renew your authentication '
348
                                                'token. Make sure to set the new '
349
                                                'token in any client you may be '
350
                                                'using, to preserve its '
351
                                                'functionality.'))
352
    auth_token_created = models.DateTimeField(_('Token creation date'),
353
                                              null=True)
354
    auth_token_expires = models.DateTimeField(
355
        _('Token expiration date'), null=True)
356

    
357
    updated = models.DateTimeField(_('Update date'))
358

    
359
    # Arbitrary text to identify the reason user got deactivated.
360
    # To be used as a reference from administrators.
361
    deactivated_reason = models.TextField(
362
        _('Reason the user was disabled for'),
363
        default=None, null=True)
364
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
365
                                          blank=True)
366

    
367
    has_credits = models.BooleanField(_('Has credits?'), default=False)
368

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

    
372
    # user email is verified
373
    email_verified = models.BooleanField(_('Email verified?'), default=False)
374

    
375
    # unique string used in user email verification url
376
    verification_code = models.CharField(max_length=255, null=True,
377
                                         blank=False, unique=True)
378

    
379
    # date user email verified
380
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
381
                                       blank=True)
382

    
383
    # email verification notice was sent to the user at this time
384
    activation_sent = models.DateTimeField(_('Activation sent date'),
385
                                           null=True, blank=True)
386

    
387
    # user got rejected during moderation process
388
    is_rejected = models.BooleanField(_('Account rejected'),
389
                                      default=False)
390
    # reason user got rejected
391
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
392
                                       blank=True)
393
    # moderation status
394
    moderated = models.BooleanField(_('User moderated'), default=False)
395
    # date user moderated (either accepted or rejected)
396
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
397
                                        blank=True, null=True)
398
    # a snapshot of user instance the time got moderated
399
    moderated_data = models.TextField(null=True, default=None, blank=True)
400
    # a string which identifies how the user got moderated
401
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
402
                                       default=None, null=True, blank=True)
403
    # the email used to accept the user
404
    accepted_email = models.EmailField(null=True, default=None, blank=True)
405

    
406
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
407
                                           default=False)
408
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
409
                                             null=True, blank=True)
410
    # permanent unique user identifier
411
    uuid = models.CharField(max_length=255, null=True, blank=False,
412
                            unique=True)
413

    
414
    policy = models.ManyToManyField(
415
        Resource, null=True, through='AstakosUserQuota')
416

    
417
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
418
                                           default=False, db_index=True)
419

    
420
    objects = AstakosUserManager()
421
    forupdate = ForUpdateManager()
422

    
423
    def __init__(self, *args, **kwargs):
424
        super(AstakosUser, self).__init__(*args, **kwargs)
425
        if not self.id:
426
            self.is_active = False
427

    
428
    @property
429
    def realname(self):
430
        return '%s %s' % (self.first_name, self.last_name)
431

    
432
    @property
433
    def log_display(self):
434
        """
435
        Should be used in all logger.* calls that refer to a user so that
436
        user display is consistent across log entries.
437
        """
438
        return '%s::%s' % (self.uuid, self.email)
439

    
440
    @realname.setter
441
    def realname(self, value):
442
        parts = value.split(' ')
443
        if len(parts) == 2:
444
            self.first_name = parts[0]
445
            self.last_name = parts[1]
446
        else:
447
            self.last_name = parts[0]
448

    
449
    def add_permission(self, pname):
450
        if self.has_perm(pname):
451
            return
452
        p, created = Permission.objects.get_or_create(
453
                                    codename=pname,
454
                                    name=pname.capitalize(),
455
                                    content_type=get_content_type())
456
        self.user_permissions.add(p)
457

    
458
    def remove_permission(self, pname):
459
        if self.has_perm(pname):
460
            return
461
        p = Permission.objects.get(codename=pname,
462
                                   content_type=get_content_type())
463
        self.user_permissions.remove(p)
464

    
465
    def add_group(self, gname):
466
        group, _ = Group.objects.get_or_create(name=gname)
467
        self.groups.add(group)
468

    
469
    def is_project_admin(self, application_id=None):
470
        return self.uuid in astakos_settings.PROJECT_ADMINS
471

    
472
    @property
473
    def invitation(self):
474
        try:
475
            return Invitation.objects.get(username=self.email)
476
        except Invitation.DoesNotExist:
477
            return None
478

    
479
    @property
480
    def policies(self):
481
        return self.astakosuserquota_set.select_related().all()
482

    
483
    def get_resource_policy(self, resource):
484
        resource = Resource.objects.get(name=resource)
485
        default_capacity = resource.uplimit
486
        try:
487
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
488
            return policy, default_capacity
489
        except AstakosUserQuota.DoesNotExist:
490
            return None, default_capacity
491

    
492
    def update_uuid(self):
493
        while not self.uuid:
494
            uuid_val = str(uuid.uuid4())
495
            try:
496
                AstakosUser.objects.get(uuid=uuid_val)
497
            except AstakosUser.DoesNotExist, e:
498
                self.uuid = uuid_val
499
        return self.uuid
500

    
501
    def save(self, update_timestamps=True, **kwargs):
502
        if update_timestamps:
503
            if not self.id:
504
                self.date_joined = datetime.now()
505
            self.updated = datetime.now()
506

    
507
        self.update_uuid()
508

    
509
        if not self.verification_code:
510
            self.renew_verification_code()
511

    
512
        # username currently matches email
513
        if self.username != self.email.lower():
514
            self.username = self.email.lower()
515

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

    
518
    def renew_verification_code(self):
519
        self.verification_code = str(uuid.uuid4())
520
        logger.info("Verification code renewed for %s" % self.log_display)
521

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

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

    
538
    def token_expired(self):
539
        return self.auth_token_expires < datetime.now()
540

    
541
    def flush_sessions(self, current_key=None):
542
        q = self.sessions
543
        if current_key:
544
            q = q.exclude(session_key=current_key)
545

    
546
        keys = q.values_list('session_key', flat=True)
547
        if keys:
548
            msg = 'Flushing sessions: %s' % ','.join(keys)
549
            logger.log(astakos_settings.LOGGING_LEVEL, msg, [])
550
        engine = import_module(settings.SESSION_ENGINE)
551
        for k in keys:
552
            s = engine.SessionStore(k)
553
            s.flush()
554

    
555
    def __unicode__(self):
556
        return '%s (%s)' % (self.realname, self.email)
557

    
558
    def conflicting_email(self):
559
        q = AstakosUser.objects.exclude(username=self.username)
560
        q = q.filter(email__iexact=self.email)
561
        if q.count() != 0:
562
            return True
563
        return False
564

    
565
    def email_change_is_pending(self):
566
        return self.emailchanges.count() > 0
567

    
568
    @property
569
    def status_display(self):
570
        msg = ""
571
        append = None
572
        if self.is_active:
573
            msg = "Accepted/Active"
574
        if self.is_rejected:
575
            msg = "Rejected"
576
            if self.rejected_reason:
577
                msg += " (%s)" % self.rejected_reason
578
        if not self.email_verified:
579
            msg = "Pending email verification"
580
        if not self.moderated:
581
            msg = "Pending moderation"
582
        if not self.is_active and self.email_verified:
583
            msg = "Accepted/Inactive"
584
            if self.deactivated_reason:
585
                msg += " (%s)" % (self.deactivated_reason)
586

    
587
        if self.moderated and not self.is_rejected:
588
            if self.accepted_policy == 'manual':
589
                msg += " (manually accepted)"
590
            else:
591
                msg += " (accepted policy: %s)" % \
592
                        self.accepted_policy
593
        return msg
594

    
595
    @property
596
    def signed_terms(self):
597
        term = get_latest_terms()
598
        if not term:
599
            return True
600
        if not self.has_signed_terms:
601
            return False
602
        if not self.date_signed_terms:
603
            return False
604
        if self.date_signed_terms < term.date:
605
            self.has_signed_terms = False
606
            self.date_signed_terms = None
607
            self.save()
608
            return False
609
        return True
610

    
611
    def set_invitations_level(self):
612
        """
613
        Update user invitation level
614
        """
615
        level = self.invitation.inviter.level + 1
616
        self.level = level
617
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
618

    
619
    def can_change_password(self):
620
        return self.has_auth_provider('local', auth_backend='astakos')
621

    
622
    def can_change_email(self):
623
        if not self.has_auth_provider('local'):
624
            return True
625

    
626
        local = self.get_auth_provider('local')._instance
627
        return local.auth_backend == 'astakos'
628

    
629
    # Auth providers related methods
630
    def get_auth_provider(self, module=None, identifier=None, **filters):
631
        if not module:
632
            return self.auth_providers.active()[0].settings
633

    
634
        params = {'module': module}
635
        if identifier:
636
            params['identifier'] = identifier
637
        params.update(filters)
638
        return self.auth_providers.active().get(**params).settings
639

    
640
    def has_auth_provider(self, provider, **kwargs):
641
        return bool(self.auth_providers.active().filter(module=provider,
642
                                                        **kwargs).count())
643

    
644
    def get_required_providers(self, **kwargs):
645
        return auth.REQUIRED_PROVIDERS.keys()
646

    
647
    def missing_required_providers(self):
648
        required = self.get_required_providers()
649
        missing = []
650
        for provider in required:
651
            if not self.has_auth_provider(provider):
652
                missing.append(auth.get_provider(provider, self))
653
        return missing
654

    
655
    def get_available_auth_providers(self, **filters):
656
        """
657
        Returns a list of providers available for add by the user.
658
        """
659
        modules = astakos_settings.IM_MODULES
660
        providers = []
661
        for p in modules:
662
            providers.append(auth.get_provider(p, self))
663
        available = []
664

    
665
        for p in providers:
666
            if p.get_add_policy:
667
                available.append(p)
668
        return available
669

    
670
    def get_disabled_auth_providers(self, **filters):
671
        providers = self.get_auth_providers(**filters)
672
        disabled = []
673
        for p in providers:
674
            if not p.get_login_policy:
675
                disabled.append(p)
676
        return disabled
677

    
678
    def get_enabled_auth_providers(self, **filters):
679
        providers = self.get_auth_providers(**filters)
680
        enabled = []
681
        for p in providers:
682
            if p.get_login_policy:
683
                enabled.append(p)
684
        return enabled
685

    
686
    def get_auth_providers(self, **filters):
687
        providers = []
688
        for provider in self.auth_providers.active(**filters):
689
            if provider.settings.module_enabled:
690
                providers.append(provider.settings)
691

    
692
        modules = astakos_settings.IM_MODULES
693

    
694
        def key(p):
695
            if not p.module in modules:
696
                return 100
697
            return modules.index(p.module)
698

    
699
        providers = sorted(providers, key=key)
700
        return providers
701

    
702
    # URL methods
703
    @property
704
    def auth_providers_display(self):
705
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
706
                         self.get_enabled_auth_providers()])
707

    
708
    def add_auth_provider(self, module='local', identifier=None, **params):
709
        provider = auth.get_provider(module, self, identifier, **params)
710
        provider.add_to_user()
711

    
712
    def get_resend_activation_url(self):
713
        return reverse('send_activation', kwargs={'user_id': self.pk})
714

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

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

    
727
    def get_inactive_message(self, provider_module, identifier=None):
728
        provider = self.get_auth_provider(provider_module, identifier)
729

    
730
        msg_extra = ''
731
        message = ''
732

    
733
        msg_inactive = provider.get_account_inactive_msg
734
        msg_pending = provider.get_pending_activation_msg
735
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
736
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
737
        msg_pending_mod = provider.get_pending_moderation_msg
738
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
739
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
740

    
741
        if not self.email_verified:
742
            message = msg_pending
743
            url = self.get_resend_activation_url()
744
            msg_extra = msg_pending_help + \
745
                        u' ' + \
746
                        '<a href="%s">%s?</a>' % (url, msg_resend)
747
        else:
748
            if not self.moderated:
749
                message = msg_pending_mod
750
            else:
751
                if self.is_rejected:
752
                    message = msg_rejected
753
                else:
754
                    message = msg_inactive
755

    
756
        return mark_safe(message + u' ' + msg_extra)
757

    
758
    def owns_application(self, application):
759
        return application.owner == self
760

    
761
    def owns_project(self, project):
762
        return project.application.owner == self
763

    
764
    def is_associated(self, project):
765
        try:
766
            m = ProjectMembership.objects.get(person=self, project=project)
767
            return m.state in ProjectMembership.ASSOCIATED_STATES
768
        except ProjectMembership.DoesNotExist:
769
            return False
770

    
771
    def get_membership(self, project):
772
        try:
773
            return ProjectMembership.objects.get(
774
                project=project,
775
                person=self)
776
        except ProjectMembership.DoesNotExist:
777
            return None
778

    
779
    def membership_display(self, project):
780
        m = self.get_membership(project)
781
        if m is None:
782
            return _('Not a member')
783
        else:
784
            return m.user_friendly_state_display()
785

    
786
    def non_owner_can_view(self, maybe_project):
787
        if self.is_project_admin():
788
            return True
789
        if maybe_project is None:
790
            return False
791
        project = maybe_project
792
        if self.is_associated(project):
793
            return True
794
        if project.is_deactivated():
795
            return False
796
        return True
797

    
798

    
799
class AstakosUserAuthProviderManager(models.Manager):
800

    
801
    def active(self, **filters):
802
        return self.filter(active=True, **filters)
803

    
804
    def remove_unverified_providers(self, provider, **filters):
805
        try:
806
            existing = self.filter(module=provider, user__email_verified=False,
807
                                   **filters)
808
            for p in existing:
809
                p.user.delete()
810
        except:
811
            pass
812

    
813
    def unverified(self, provider, **filters):
814
        try:
815
            return self.get(module=provider, user__email_verified=False,
816
                            **filters).settings
817
        except AstakosUserAuthProvider.DoesNotExist:
818
            return None
819

    
820
    def verified(self, provider, **filters):
821
        try:
822
            return self.get(module=provider, user__email_verified=True,
823
                            **filters).settings
824
        except AstakosUserAuthProvider.DoesNotExist:
825
            return None
826

    
827

    
828
class AuthProviderPolicyProfileManager(models.Manager):
829

    
830
    def active(self):
831
        return self.filter(active=True)
832

    
833
    def for_user(self, user, provider):
834
        policies = {}
835
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
836
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
837
        exclusive_q = exclusive_q1 | exclusive_q2
838

    
839
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
840
            policies.update(profile.policies)
841

    
842
        user_groups = user.groups.all().values('pk')
843
        for profile in self.active().filter(groups__in=user_groups).filter(
844
                exclusive_q):
845
            policies.update(profile.policies)
846
        return policies
847

    
848
    def add_policy(self, name, provider, group_or_user, exclusive=False,
849
                   **policies):
850
        is_group = isinstance(group_or_user, Group)
851
        profile, created = self.get_or_create(name=name, provider=provider,
852
                                              is_exclusive=exclusive)
853
        profile.is_exclusive = exclusive
854
        profile.save()
855
        if is_group:
856
            profile.groups.add(group_or_user)
857
        else:
858
            profile.users.add(group_or_user)
859
        profile.set_policies(policies)
860
        profile.save()
861
        return profile
862

    
863

    
864
class AuthProviderPolicyProfile(models.Model):
865
    name = models.CharField(_('Name'), max_length=255, blank=False,
866
                            null=False, db_index=True)
867
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
868
                                null=False)
869

    
870
    # apply policies to all providers excluding the one set in provider field
871
    is_exclusive = models.BooleanField(default=False)
872

    
873
    policy_add = models.NullBooleanField(null=True, default=None)
874
    policy_remove = models.NullBooleanField(null=True, default=None)
875
    policy_create = models.NullBooleanField(null=True, default=None)
876
    policy_login = models.NullBooleanField(null=True, default=None)
877
    policy_limit = models.IntegerField(null=True, default=None)
878
    policy_required = models.NullBooleanField(null=True, default=None)
879
    policy_automoderate = models.NullBooleanField(null=True, default=None)
880
    policy_switch = models.NullBooleanField(null=True, default=None)
881

    
882
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
883
                     'automoderate')
884

    
885
    priority = models.IntegerField(null=False, default=1)
886
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
887
    users = models.ManyToManyField(AstakosUser,
888
                                   related_name='authpolicy_profiles')
889
    active = models.BooleanField(default=True)
890

    
891
    objects = AuthProviderPolicyProfileManager()
892

    
893
    class Meta:
894
        ordering = ['priority']
895

    
896
    @property
897
    def policies(self):
898
        policies = {}
899
        for pkey in self.POLICY_FIELDS:
900
            value = getattr(self, 'policy_%s' % pkey, None)
901
            if value is None:
902
                continue
903
            policies[pkey] = value
904
        return policies
905

    
906
    def set_policies(self, policies_dict):
907
        for key, value in policies_dict.iteritems():
908
            if key in self.POLICY_FIELDS:
909
                setattr(self, 'policy_%s' % key, value)
910
        return self.policies
911

    
912

    
913
class AstakosUserAuthProvider(models.Model):
914
    """
915
    Available user authentication methods.
916
    """
917
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
918
                                   null=True, default=None)
919
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
920
    module = models.CharField(_('Provider'), max_length=255, blank=False,
921
                                default='local')
922
    identifier = models.CharField(_('Third-party identifier'),
923
                                              max_length=255, null=True,
924
                                              blank=True)
925
    active = models.BooleanField(default=True)
926
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
927
                                   default='astakos')
928
    info_data = models.TextField(default="", null=True, blank=True)
929
    created = models.DateTimeField('Creation date', auto_now_add=True)
930

    
931
    objects = AstakosUserAuthProviderManager()
932

    
933
    class Meta:
934
        unique_together = (('identifier', 'module', 'user'), )
935
        ordering = ('module', 'created')
936

    
937
    def __init__(self, *args, **kwargs):
938
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
939
        try:
940
            self.info = json.loads(self.info_data)
941
            if not self.info:
942
                self.info = {}
943
        except Exception, e:
944
            self.info = {}
945

    
946
        for key,value in self.info.iteritems():
947
            setattr(self, 'info_%s' % key, value)
948

    
949
    @property
950
    def settings(self):
951
        extra_data = {}
952

    
953
        info_data = {}
954
        if self.info_data:
955
            info_data = json.loads(self.info_data)
956

    
957
        extra_data['info'] = info_data
958

    
959
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
960
            extra_data[key] = getattr(self, key)
961

    
962
        extra_data['instance'] = self
963
        return auth.get_provider(self.module, self.user,
964
                                           self.identifier, **extra_data)
965

    
966
    def __repr__(self):
967
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
968

    
969
    def __unicode__(self):
970
        if self.identifier:
971
            return "%s:%s" % (self.module, self.identifier)
972
        if self.auth_backend:
973
            return "%s:%s" % (self.module, self.auth_backend)
974
        return self.module
975

    
976
    def save(self, *args, **kwargs):
977
        self.info_data = json.dumps(self.info)
978
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
979

    
980

    
981
class ExtendedManager(models.Manager):
982
    def _update_or_create(self, **kwargs):
983
        assert kwargs, \
984
            'update_or_create() must be passed at least one keyword argument'
985
        obj, created = self.get_or_create(**kwargs)
986
        defaults = kwargs.pop('defaults', {})
987
        if created:
988
            return obj, True, False
989
        else:
990
            try:
991
                params = dict(
992
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
993
                params.update(defaults)
994
                for attr, val in params.items():
995
                    if hasattr(obj, attr):
996
                        setattr(obj, attr, val)
997
                sid = transaction.savepoint()
998
                obj.save(force_update=True)
999
                transaction.savepoint_commit(sid)
1000
                return obj, False, True
1001
            except IntegrityError, e:
1002
                transaction.savepoint_rollback(sid)
1003
                try:
1004
                    return self.get(**kwargs), False, False
1005
                except self.model.DoesNotExist:
1006
                    raise e
1007

    
1008
    update_or_create = _update_or_create
1009

    
1010

    
1011
class AstakosUserQuota(models.Model):
1012
    objects = ExtendedManager()
1013
    capacity = intDecimalField()
1014
    resource = models.ForeignKey(Resource)
1015
    user = models.ForeignKey(AstakosUser)
1016

    
1017
    class Meta:
1018
        unique_together = ("resource", "user")
1019

    
1020

    
1021
class ApprovalTerms(models.Model):
1022
    """
1023
    Model for approval terms
1024
    """
1025

    
1026
    date = models.DateTimeField(
1027
        _('Issue date'), db_index=True, auto_now_add=True)
1028
    location = models.CharField(_('Terms location'), max_length=255)
1029

    
1030

    
1031
class Invitation(models.Model):
1032
    """
1033
    Model for registring invitations
1034
    """
1035
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1036
                                null=True)
1037
    realname = models.CharField(_('Real name'), max_length=255)
1038
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1039
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1040
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1041
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1042
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1043

    
1044
    def __init__(self, *args, **kwargs):
1045
        super(Invitation, self).__init__(*args, **kwargs)
1046
        if not self.id:
1047
            self.code = _generate_invitation_code()
1048

    
1049
    def consume(self):
1050
        self.is_consumed = True
1051
        self.consumed = datetime.now()
1052
        self.save()
1053

    
1054
    def __unicode__(self):
1055
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1056

    
1057

    
1058
class EmailChangeManager(models.Manager):
1059

    
1060
    @transaction.commit_on_success
1061
    def change_email(self, activation_key):
1062
        """
1063
        Validate an activation key and change the corresponding
1064
        ``User`` if valid.
1065

1066
        If the key is valid and has not expired, return the ``User``
1067
        after activating.
1068

1069
        If the key is not valid or has expired, return ``None``.
1070

1071
        If the key is valid but the ``User`` is already active,
1072
        return ``None``.
1073

1074
        After successful email change the activation record is deleted.
1075

1076
        Throws ValueError if there is already
1077
        """
1078
        try:
1079
            email_change = self.model.objects.get(
1080
                activation_key=activation_key)
1081
            if email_change.activation_key_expired():
1082
                email_change.delete()
1083
                raise EmailChange.DoesNotExist
1084
            # is there an active user with this address?
1085
            try:
1086
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1087
            except AstakosUser.DoesNotExist:
1088
                pass
1089
            else:
1090
                raise ValueError(_('The new email address is reserved.'))
1091
            # update user
1092
            user = AstakosUser.objects.get(pk=email_change.user_id)
1093
            old_email = user.email
1094
            user.email = email_change.new_email_address
1095
            user.save()
1096
            email_change.delete()
1097
            msg = "User %s changed email from %s to %s" % (user.log_display,
1098
                                                           old_email,
1099
                                                           user.email)
1100
            logger.log(astakos_settings.LOGGING_LEVEL, msg)
1101
            return user
1102
        except EmailChange.DoesNotExist:
1103
            raise ValueError(_('Invalid activation key.'))
1104

    
1105

    
1106
class EmailChange(models.Model):
1107
    new_email_address = models.EmailField(
1108
        _(u'new e-mail address'),
1109
        help_text=_('Provide a new email address. Until you verify the new '
1110
                    'address by following the activation link that will be '
1111
                    'sent to it, your old email address will remain active.'))
1112
    user = models.ForeignKey(
1113
        AstakosUser, unique=True, related_name='emailchanges')
1114
    requested_at = models.DateTimeField(auto_now_add=True)
1115
    activation_key = models.CharField(
1116
        max_length=40, unique=True, db_index=True)
1117

    
1118
    objects = EmailChangeManager()
1119

    
1120
    def get_url(self):
1121
        return reverse('email_change_confirm',
1122
                      kwargs={'activation_key': self.activation_key})
1123

    
1124
    def activation_key_expired(self):
1125
        expiration_date = timedelta(days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1126
        return self.requested_at + expiration_date < datetime.now()
1127

    
1128

    
1129
class AdditionalMail(models.Model):
1130
    """
1131
    Model for registring invitations
1132
    """
1133
    owner = models.ForeignKey(AstakosUser)
1134
    email = models.EmailField()
1135

    
1136

    
1137
def _generate_invitation_code():
1138
    while True:
1139
        code = randint(1, 2L ** 63 - 1)
1140
        try:
1141
            Invitation.objects.get(code=code)
1142
            # An invitation with this code already exists, try again
1143
        except Invitation.DoesNotExist:
1144
            return code
1145

    
1146

    
1147
def get_latest_terms():
1148
    try:
1149
        term = ApprovalTerms.objects.order_by('-id')[0]
1150
        return term
1151
    except IndexError:
1152
        pass
1153
    return None
1154

    
1155

    
1156
class PendingThirdPartyUser(models.Model):
1157
    """
1158
    Model for registring successful third party user authentications
1159
    """
1160
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1161
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1162
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1163
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1164
                                  null=True)
1165
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1166
                                 null=True)
1167
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1168
                                   null=True)
1169
    username = models.CharField(_('username'), max_length=30, unique=True,
1170
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1171
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1172
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1173
    info = models.TextField(default="", null=True, blank=True)
1174

    
1175
    class Meta:
1176
        unique_together = ("provider", "third_party_identifier")
1177

    
1178
    def get_user_instance(self):
1179
        """
1180
        Create a new AstakosUser instance based on details provided when user
1181
        initially signed up.
1182
        """
1183
        d = copy.copy(self.__dict__)
1184
        d.pop('_state', None)
1185
        d.pop('id', None)
1186
        d.pop('token', None)
1187
        d.pop('created', None)
1188
        d.pop('info', None)
1189
        d.pop('affiliation', None)
1190
        d.pop('provider', None)
1191
        d.pop('third_party_identifier', None)
1192
        user = AstakosUser(**d)
1193

    
1194
        return user
1195

    
1196
    @property
1197
    def realname(self):
1198
        return '%s %s' %(self.first_name, self.last_name)
1199

    
1200
    @realname.setter
1201
    def realname(self, value):
1202
        parts = value.split(' ')
1203
        if len(parts) == 2:
1204
            self.first_name = parts[0]
1205
            self.last_name = parts[1]
1206
        else:
1207
            self.last_name = parts[0]
1208

    
1209
    def save(self, **kwargs):
1210
        if not self.id:
1211
            # set username
1212
            while not self.username:
1213
                username =  uuid.uuid4().hex[:30]
1214
                try:
1215
                    AstakosUser.objects.get(username = username)
1216
                except AstakosUser.DoesNotExist, e:
1217
                    self.username = username
1218
        super(PendingThirdPartyUser, self).save(**kwargs)
1219

    
1220
    def generate_token(self):
1221
        self.password = self.third_party_identifier
1222
        self.last_login = datetime.now()
1223
        self.token = default_token_generator.make_token(self)
1224

    
1225
    def existing_user(self):
1226
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1227
                                         auth_providers__identifier=self.third_party_identifier)
1228

    
1229
    def get_provider(self, user):
1230
        params = {
1231
            'info_data': self.info,
1232
            'affiliation': self.affiliation
1233
        }
1234
        return auth.get_provider(self.provider, user,
1235
                                 self.third_party_identifier, **params)
1236

    
1237
class SessionCatalog(models.Model):
1238
    session_key = models.CharField(_('session key'), max_length=40)
1239
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1240

    
1241

    
1242
class UserSetting(models.Model):
1243
    user = models.ForeignKey(AstakosUser)
1244
    setting = models.CharField(max_length=255)
1245
    value = models.IntegerField()
1246

    
1247
    objects = ForUpdateManager()
1248

    
1249
    class Meta:
1250
        unique_together = ("user", "setting")
1251

    
1252

    
1253
### PROJECTS ###
1254
################
1255

    
1256
class ChainManager(ForUpdateManager):
1257

    
1258
    def search_by_name(self, *search_strings):
1259
        projects = Project.objects.search_by_name(*search_strings)
1260
        chains = [p.id for p in projects]
1261
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1262
        apps = (app for app in apps if app.is_latest())
1263
        app_chains = [app.chain for app in apps if app.chain not in chains]
1264
        return chains + app_chains
1265

    
1266
    def all_full_state(self):
1267
        chains = self.all()
1268
        cids = [c.chain for c in chains]
1269
        projects = Project.objects.select_related('application').in_bulk(cids)
1270

    
1271
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1272
        chain_latest = dict(objs.values_list('chain', 'latest'))
1273

    
1274
        objs = ProjectApplication.objects.select_related('applicant')
1275
        apps = objs.in_bulk(chain_latest.values())
1276

    
1277
        d = {}
1278
        for chain in chains:
1279
            pk = chain.pk
1280
            project = projects.get(pk, None)
1281
            app = apps[chain_latest[pk]]
1282
            d[chain.pk] = chain.get_state(project, app)
1283

    
1284
        return d
1285

    
1286
    def of_project(self, project):
1287
        if project is None:
1288
            return None
1289
        try:
1290
            return self.get(chain=project.id)
1291
        except Chain.DoesNotExist:
1292
            raise AssertionError('project with no chain')
1293

    
1294

    
1295
class Chain(models.Model):
1296
    chain  =   models.AutoField(primary_key=True)
1297

    
1298
    def __str__(self):
1299
        return "%s" % (self.chain,)
1300

    
1301
    objects = ChainManager()
1302

    
1303
    PENDING            = 0
1304
    DENIED             = 3
1305
    DISMISSED          = 4
1306
    CANCELLED          = 5
1307

    
1308
    APPROVED           = 10
1309
    APPROVED_PENDING   = 11
1310
    SUSPENDED          = 12
1311
    SUSPENDED_PENDING  = 13
1312
    TERMINATED         = 14
1313
    TERMINATED_PENDING = 15
1314

    
1315
    PENDING_STATES = [PENDING,
1316
                      APPROVED_PENDING,
1317
                      SUSPENDED_PENDING,
1318
                      TERMINATED_PENDING,
1319
                      ]
1320

    
1321
    MODIFICATION_STATES = [APPROVED_PENDING,
1322
                           SUSPENDED_PENDING,
1323
                           TERMINATED_PENDING,
1324
                           ]
1325

    
1326
    RELEVANT_STATES = [PENDING,
1327
                       DENIED,
1328
                       APPROVED,
1329
                       APPROVED_PENDING,
1330
                       SUSPENDED,
1331
                       SUSPENDED_PENDING,
1332
                       TERMINATED_PENDING,
1333
                       ]
1334

    
1335
    SKIP_STATES = [DISMISSED,
1336
                   CANCELLED,
1337
                   TERMINATED]
1338

    
1339
    STATE_DISPLAY = {
1340
        PENDING            : _("Pending"),
1341
        DENIED             : _("Denied"),
1342
        DISMISSED          : _("Dismissed"),
1343
        CANCELLED          : _("Cancelled"),
1344
        APPROVED           : _("Active"),
1345
        APPROVED_PENDING   : _("Active - Pending"),
1346
        SUSPENDED          : _("Suspended"),
1347
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1348
        TERMINATED         : _("Terminated"),
1349
        TERMINATED_PENDING : _("Terminated - Pending"),
1350
        }
1351

    
1352

    
1353
    @classmethod
1354
    def _chain_state(cls, project_state, app_state):
1355
        s = CHAIN_STATE.get((project_state, app_state), None)
1356
        if s is None:
1357
            raise AssertionError('inconsistent chain state')
1358
        return s
1359

    
1360
    @classmethod
1361
    def chain_state(cls, project, app):
1362
        p_state = project.state if project else None
1363
        return cls._chain_state(p_state, app.state)
1364

    
1365
    @classmethod
1366
    def state_display(cls, s):
1367
        if s is None:
1368
            return _("Unknown")
1369
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1370

    
1371
    def last_application(self):
1372
        return self.chained_apps.order_by('-id')[0]
1373

    
1374
    def get_project(self):
1375
        try:
1376
            return self.chained_project
1377
        except Project.DoesNotExist:
1378
            return None
1379

    
1380
    def get_elements(self):
1381
        project = self.get_project()
1382
        app = self.last_application()
1383
        return project, app
1384

    
1385
    def get_state(self, project, app):
1386
        s = self.chain_state(project, app)
1387
        return s, project, app
1388

    
1389
    def full_state(self):
1390
        project, app = self.get_elements()
1391
        return self.get_state(project, app)
1392

    
1393

    
1394
def new_chain():
1395
    c = Chain.objects.create()
1396
    return c
1397

    
1398

    
1399
class ProjectApplicationManager(ForUpdateManager):
1400

    
1401
    def user_visible_projects(self, *filters, **kw_filters):
1402
        model = self.model
1403
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1404

    
1405
    def user_visible_by_chain(self, flt):
1406
        model = self.model
1407
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1408
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1409
        by_chain = dict(pending.annotate(models.Max('id')))
1410
        by_chain.update(approved.annotate(models.Max('id')))
1411
        return self.filter(flt, id__in=by_chain.values())
1412

    
1413
    def user_accessible_projects(self, user):
1414
        """
1415
        Return projects accessed by specified user.
1416
        """
1417
        if user.is_project_admin():
1418
            participates_filters = Q()
1419
        else:
1420
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1421
                                   Q(project__projectmembership__person=user)
1422

    
1423
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1424

    
1425
    def search_by_name(self, *search_strings):
1426
        q = Q()
1427
        for s in search_strings:
1428
            q = q | Q(name__icontains=s)
1429
        return self.filter(q)
1430

    
1431
    def latest_of_chain(self, chain_id):
1432
        try:
1433
            return self.filter(chain=chain_id).order_by('-id')[0]
1434
        except IndexError:
1435
            return None
1436

    
1437

    
1438
class ProjectApplication(models.Model):
1439
    applicant               =   models.ForeignKey(
1440
                                    AstakosUser,
1441
                                    related_name='projects_applied',
1442
                                    db_index=True)
1443

    
1444
    PENDING     =    0
1445
    APPROVED    =    1
1446
    REPLACED    =    2
1447
    DENIED      =    3
1448
    DISMISSED   =    4
1449
    CANCELLED   =    5
1450

    
1451
    state                   =   models.IntegerField(default=PENDING,
1452
                                                    db_index=True)
1453

    
1454
    owner                   =   models.ForeignKey(
1455
                                    AstakosUser,
1456
                                    related_name='projects_owned',
1457
                                    db_index=True)
1458

    
1459
    chain                   =   models.ForeignKey(Chain,
1460
                                                  related_name='chained_apps',
1461
                                                  db_column='chain')
1462
    precursor_application   =   models.ForeignKey('ProjectApplication',
1463
                                                  null=True,
1464
                                                  blank=True)
1465

    
1466
    name                    =   models.CharField(max_length=80)
1467
    homepage                =   models.URLField(max_length=255, null=True,
1468
                                                verify_exists=False)
1469
    description             =   models.TextField(null=True, blank=True)
1470
    start_date              =   models.DateTimeField(null=True, blank=True)
1471
    end_date                =   models.DateTimeField()
1472
    member_join_policy      =   models.IntegerField()
1473
    member_leave_policy     =   models.IntegerField()
1474
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1475
    resource_grants         =   models.ManyToManyField(
1476
                                    Resource,
1477
                                    null=True,
1478
                                    blank=True,
1479
                                    through='ProjectResourceGrant')
1480
    comments                =   models.TextField(null=True, blank=True)
1481
    issue_date              =   models.DateTimeField(auto_now_add=True)
1482
    response_date           =   models.DateTimeField(null=True, blank=True)
1483
    response                =   models.TextField(null=True, blank=True)
1484

    
1485
    objects                 =   ProjectApplicationManager()
1486

    
1487
    # Compiled queries
1488
    Q_PENDING  = Q(state=PENDING)
1489
    Q_APPROVED = Q(state=APPROVED)
1490
    Q_DENIED   = Q(state=DENIED)
1491

    
1492
    class Meta:
1493
        unique_together = ("chain", "id")
1494

    
1495
    def __unicode__(self):
1496
        return "%s applied by %s" % (self.name, self.applicant)
1497

    
1498
    # TODO: Move to a more suitable place
1499
    APPLICATION_STATE_DISPLAY = {
1500
        PENDING  : _('Pending review'),
1501
        APPROVED : _('Approved'),
1502
        REPLACED : _('Replaced'),
1503
        DENIED   : _('Denied'),
1504
        DISMISSED: _('Dismissed'),
1505
        CANCELLED: _('Cancelled')
1506
    }
1507

    
1508
    @property
1509
    def log_display(self):
1510
        return "application %s (%s) for project %s" % (
1511
            self.id, self.name, self.chain)
1512

    
1513
    def state_display(self):
1514
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1515

    
1516
    def project_state_display(self):
1517
        try:
1518
            project = self.project
1519
            return project.state_display()
1520
        except Project.DoesNotExist:
1521
            return self.state_display()
1522

    
1523
    def add_resource_policy(self, resource, uplimit):
1524
        """Raises ObjectDoesNotExist, IntegrityError"""
1525
        q = self.projectresourcegrant_set
1526
        resource = Resource.objects.get(name=resource)
1527
        q.create(resource=resource, member_capacity=uplimit)
1528

    
1529
    def members_count(self):
1530
        return self.project.approved_memberships.count()
1531

    
1532
    @property
1533
    def grants(self):
1534
        return self.projectresourcegrant_set.values('member_capacity',
1535
                                                    'resource__name')
1536

    
1537
    @property
1538
    def resource_policies(self):
1539
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1540

    
1541
    def set_resource_policies(self, policies):
1542
        for resource, uplimit in policies:
1543
            self.add_resource_policy(resource, uplimit)
1544

    
1545
    def pending_modifications_incl_me(self):
1546
        q = self.chained_applications()
1547
        q = q.filter(Q(state=self.PENDING))
1548
        return q
1549

    
1550
    def last_pending_incl_me(self):
1551
        try:
1552
            return self.pending_modifications_incl_me().order_by('-id')[0]
1553
        except IndexError:
1554
            return None
1555

    
1556
    def pending_modifications(self):
1557
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1558

    
1559
    def last_pending(self):
1560
        try:
1561
            return self.pending_modifications().order_by('-id')[0]
1562
        except IndexError:
1563
            return None
1564

    
1565
    def is_modification(self):
1566
        # if self.state != self.PENDING:
1567
        #     return False
1568
        parents = self.chained_applications().filter(id__lt=self.id)
1569
        parents = parents.filter(state__in=[self.APPROVED])
1570
        return parents.count() > 0
1571

    
1572
    def chained_applications(self):
1573
        return ProjectApplication.objects.filter(chain=self.chain)
1574

    
1575
    def is_latest(self):
1576
        return self.chained_applications().order_by('-id')[0] == self
1577

    
1578
    def has_pending_modifications(self):
1579
        return bool(self.last_pending())
1580

    
1581
    def denied_modifications(self):
1582
        q = self.chained_applications()
1583
        q = q.filter(Q(state=self.DENIED))
1584
        q = q.filter(~Q(id=self.id))
1585
        return q
1586

    
1587
    def last_denied(self):
1588
        try:
1589
            return self.denied_modifications().order_by('-id')[0]
1590
        except IndexError:
1591
            return None
1592

    
1593
    def has_denied_modifications(self):
1594
        return bool(self.last_denied())
1595

    
1596
    def is_applied(self):
1597
        try:
1598
            self.project
1599
            return True
1600
        except Project.DoesNotExist:
1601
            return False
1602

    
1603
    def get_project(self):
1604
        try:
1605
            return Project.objects.get(id=self.chain)
1606
        except Project.DoesNotExist:
1607
            return None
1608

    
1609
    def project_exists(self):
1610
        return self.get_project() is not None
1611

    
1612
    def can_cancel(self):
1613
        return self.state == self.PENDING
1614

    
1615
    def cancel(self):
1616
        if not self.can_cancel():
1617
            m = _("cannot cancel: application '%s' in state '%s'") % (
1618
                    self.id, self.state)
1619
            raise AssertionError(m)
1620

    
1621
        self.state = self.CANCELLED
1622
        self.save()
1623

    
1624
    def can_dismiss(self):
1625
        return self.state == self.DENIED
1626

    
1627
    def dismiss(self):
1628
        if not self.can_dismiss():
1629
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1630
                    self.id, self.state)
1631
            raise AssertionError(m)
1632

    
1633
        self.state = self.DISMISSED
1634
        self.save()
1635

    
1636
    def can_deny(self):
1637
        return self.state == self.PENDING
1638

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

    
1645
        self.state = self.DENIED
1646
        self.response_date = datetime.now()
1647
        self.response = reason
1648
        self.save()
1649

    
1650
    def can_approve(self):
1651
        return self.state == self.PENDING
1652

    
1653
    def approve(self, reason):
1654
        if not self.can_approve():
1655
            m = _("cannot approve: project '%s' in state '%s'") % (
1656
                    self.name, self.state)
1657
            raise AssertionError(m) # invalid argument
1658

    
1659
        now = datetime.now()
1660
        self.state = self.APPROVED
1661
        self.response_date = now
1662
        self.response = reason
1663
        self.save()
1664

    
1665
        project = self.get_project()
1666
        if project is None:
1667
            project = Project(id=self.chain)
1668

    
1669
        project.name = self.name
1670
        project.application = self
1671
        project.last_approval_date = now
1672
        project.save()
1673
        return project
1674

    
1675
    @property
1676
    def member_join_policy_display(self):
1677
        policy = self.member_join_policy
1678
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1679

    
1680
    @property
1681
    def member_leave_policy_display(self):
1682
        policy = self.member_leave_policy
1683
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1684

    
1685
class ProjectResourceGrant(models.Model):
1686

    
1687
    resource                =   models.ForeignKey(Resource)
1688
    project_application     =   models.ForeignKey(ProjectApplication,
1689
                                                  null=True)
1690
    project_capacity        =   intDecimalField(null=True)
1691
    member_capacity         =   intDecimalField(default=0)
1692

    
1693
    objects = ExtendedManager()
1694

    
1695
    class Meta:
1696
        unique_together = ("resource", "project_application")
1697

    
1698
    def display_member_capacity(self):
1699
        if self.member_capacity:
1700
            if self.resource.unit:
1701
                return ProjectResourceGrant.display_filesize(
1702
                    self.member_capacity)
1703
            else:
1704
                if math.isinf(self.member_capacity):
1705
                    return 'Unlimited'
1706
                else:
1707
                    return self.member_capacity
1708
        else:
1709
            return 'Unlimited'
1710

    
1711
    def __str__(self):
1712
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1713
                                        self.display_member_capacity())
1714

    
1715
    @classmethod
1716
    def display_filesize(cls, value):
1717
        try:
1718
            value = float(value)
1719
        except:
1720
            return
1721
        else:
1722
            if math.isinf(value):
1723
                return 'Unlimited'
1724
            if value > 1:
1725
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1726
                                [0, 0, 0, 0, 0, 0])
1727
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1728
                quotient = float(value) / 1024**exponent
1729
                unit, value_decimals = unit_list[exponent]
1730
                format_string = '{0:.%sf} {1}' % (value_decimals)
1731
                return format_string.format(quotient, unit)
1732
            if value == 0:
1733
                return '0 bytes'
1734
            if value == 1:
1735
                return '1 byte'
1736
            else:
1737
               return '0'
1738

    
1739

    
1740
class ProjectManager(ForUpdateManager):
1741

    
1742
    def terminated_projects(self):
1743
        q = self.model.Q_TERMINATED
1744
        return self.filter(q)
1745

    
1746
    def not_terminated_projects(self):
1747
        q = ~self.model.Q_TERMINATED
1748
        return self.filter(q)
1749

    
1750
    def deactivated_projects(self):
1751
        q = self.model.Q_DEACTIVATED
1752
        return self.filter(q)
1753

    
1754
    def expired_projects(self):
1755
        q = (~Q(state=Project.TERMINATED) &
1756
              Q(application__end_date__lt=datetime.now()))
1757
        return self.filter(q)
1758

    
1759
    def search_by_name(self, *search_strings):
1760
        q = Q()
1761
        for s in search_strings:
1762
            q = q | Q(name__icontains=s)
1763
        return self.filter(q)
1764

    
1765

    
1766
class Project(models.Model):
1767

    
1768
    id                          =   models.OneToOneField(Chain,
1769
                                                      related_name='chained_project',
1770
                                                      db_column='id',
1771
                                                      primary_key=True)
1772

    
1773
    application                 =   models.OneToOneField(
1774
                                            ProjectApplication,
1775
                                            related_name='project')
1776
    last_approval_date          =   models.DateTimeField(null=True)
1777

    
1778
    members                     =   models.ManyToManyField(
1779
                                            AstakosUser,
1780
                                            through='ProjectMembership')
1781

    
1782
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1783
    deactivation_date           =   models.DateTimeField(null=True)
1784

    
1785
    creation_date               =   models.DateTimeField(auto_now_add=True)
1786
    name                        =   models.CharField(
1787
                                            max_length=80,
1788
                                            null=True,
1789
                                            db_index=True,
1790
                                            unique=True)
1791

    
1792
    APPROVED    = 1
1793
    SUSPENDED   = 10
1794
    TERMINATED  = 100
1795

    
1796
    state                       =   models.IntegerField(default=APPROVED,
1797
                                                        db_index=True)
1798

    
1799
    objects     =   ProjectManager()
1800

    
1801
    # Compiled queries
1802
    Q_TERMINATED  = Q(state=TERMINATED)
1803
    Q_SUSPENDED   = Q(state=SUSPENDED)
1804
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1805

    
1806
    def __str__(self):
1807
        return uenc(_("<project %s '%s'>") %
1808
                    (self.id, udec(self.application.name)))
1809

    
1810
    __repr__ = __str__
1811

    
1812
    def __unicode__(self):
1813
        return _("<project %s '%s'>") % (self.id, self.application.name)
1814

    
1815
    STATE_DISPLAY = {
1816
        APPROVED   : 'Active',
1817
        SUSPENDED  : 'Suspended',
1818
        TERMINATED : 'Terminated'
1819
        }
1820

    
1821
    def state_display(self):
1822
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1823

    
1824
    def expiration_info(self):
1825
        return (str(self.id), self.name, self.state_display(),
1826
                str(self.application.end_date))
1827

    
1828
    def is_deactivated(self, reason=None):
1829
        if reason is not None:
1830
            return self.state == reason
1831

    
1832
        return self.state != self.APPROVED
1833

    
1834
    ### Deactivation calls
1835

    
1836
    def terminate(self):
1837
        self.deactivation_reason = 'TERMINATED'
1838
        self.deactivation_date = datetime.now()
1839
        self.state = self.TERMINATED
1840
        self.name = None
1841
        self.save()
1842

    
1843
    def suspend(self):
1844
        self.deactivation_reason = 'SUSPENDED'
1845
        self.deactivation_date = datetime.now()
1846
        self.state = self.SUSPENDED
1847
        self.save()
1848

    
1849
    def resume(self):
1850
        self.deactivation_reason = None
1851
        self.deactivation_date = None
1852
        self.state = self.APPROVED
1853
        self.save()
1854

    
1855
    ### Logical checks
1856

    
1857
    def is_inconsistent(self):
1858
        now = datetime.now()
1859
        dates = [self.creation_date,
1860
                 self.last_approval_date,
1861
                 self.deactivation_date]
1862
        return any([date > now for date in dates])
1863

    
1864
    def is_approved(self):
1865
        return self.state == self.APPROVED
1866

    
1867
    @property
1868
    def is_alive(self):
1869
        return not self.is_terminated
1870

    
1871
    @property
1872
    def is_terminated(self):
1873
        return self.is_deactivated(self.TERMINATED)
1874

    
1875
    @property
1876
    def is_suspended(self):
1877
        return self.is_deactivated(self.SUSPENDED)
1878

    
1879
    def violates_resource_grants(self):
1880
        return False
1881

    
1882
    def violates_members_limit(self, adding=0):
1883
        application = self.application
1884
        limit = application.limit_on_members_number
1885
        if limit is None:
1886
            return False
1887
        return (len(self.approved_members) + adding > limit)
1888

    
1889

    
1890
    ### Other
1891

    
1892
    def count_pending_memberships(self):
1893
        memb_set = self.projectmembership_set
1894
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1895
        return memb_count
1896

    
1897
    def count_actually_accepted_memberships(self):
1898
        memb_set = self.projectmembership_set
1899
        memb_count = memb_set.filter(state=ProjectMembership.LEAVE_REQUESTED)
1900
        memb_count = memb_set.filter(state=ProjectMembership.ACCEPTED).count()
1901
        return memb_count
1902

    
1903
    def members_count(self):
1904
        return self.approved_memberships.count()
1905

    
1906
    @property
1907
    def approved_memberships(self):
1908
        query = ProjectMembership.Q_ACCEPTED_STATES
1909
        return self.projectmembership_set.filter(query)
1910

    
1911
    @property
1912
    def approved_members(self):
1913
        return [m.person for m in self.approved_memberships]
1914

    
1915

    
1916
CHAIN_STATE = {
1917
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1918
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1919
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1920
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1921
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1922

    
1923
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1924
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1925
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1926
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1927
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1928

    
1929
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1930
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1931
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1932
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1933
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1934

    
1935
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1936
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1937
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1938
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1939
    }
1940

    
1941

    
1942
class ProjectMembershipManager(ForUpdateManager):
1943

    
1944
    def any_accepted(self):
1945
        q = self.model.Q_ACTUALLY_ACCEPTED
1946
        return self.filter(q)
1947

    
1948
    def actually_accepted(self):
1949
        q = self.model.Q_ACTUALLY_ACCEPTED
1950
        return self.filter(q)
1951

    
1952
    def requested(self):
1953
        return self.filter(state=ProjectMembership.REQUESTED)
1954

    
1955
    def suspended(self):
1956
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1957

    
1958
class ProjectMembership(models.Model):
1959

    
1960
    person              =   models.ForeignKey(AstakosUser)
1961
    request_date        =   models.DateTimeField(auto_now_add=True)
1962
    project             =   models.ForeignKey(Project)
1963

    
1964
    REQUESTED           =   0
1965
    ACCEPTED            =   1
1966
    LEAVE_REQUESTED     =   5
1967
    # User deactivation
1968
    USER_SUSPENDED      =   10
1969

    
1970
    REMOVED             =   200
1971

    
1972
    ASSOCIATED_STATES   =   set([REQUESTED,
1973
                                 ACCEPTED,
1974
                                 LEAVE_REQUESTED,
1975
                                 USER_SUSPENDED,
1976
                                 ])
1977

    
1978
    ACCEPTED_STATES     =   set([ACCEPTED,
1979
                                 LEAVE_REQUESTED,
1980
                                 USER_SUSPENDED,
1981
                                 ])
1982

    
1983
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1984

    
1985
    state               =   models.IntegerField(default=REQUESTED,
1986
                                                db_index=True)
1987
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
1988
    leave_request_date  =   models.DateTimeField(null=True)
1989

    
1990
    objects     =   ProjectMembershipManager()
1991

    
1992
    # Compiled queries
1993
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1994
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1995

    
1996
    MEMBERSHIP_STATE_DISPLAY = {
1997
        REQUESTED           : _('Requested'),
1998
        ACCEPTED            : _('Accepted'),
1999
        LEAVE_REQUESTED     : _('Leave Requested'),
2000
        USER_SUSPENDED      : _('Suspended'),
2001
        REMOVED             : _('Pending removal'),
2002
        }
2003

    
2004
    USER_FRIENDLY_STATE_DISPLAY = {
2005
        REQUESTED           : _('Join requested'),
2006
        ACCEPTED            : _('Accepted member'),
2007
        LEAVE_REQUESTED     : _('Requested to leave'),
2008
        USER_SUSPENDED      : _('Suspended member'),
2009
        REMOVED             : _('Pending removal'),
2010
        }
2011

    
2012
    def state_display(self):
2013
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2014

    
2015
    def user_friendly_state_display(self):
2016
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2017

    
2018
    class Meta:
2019
        unique_together = ("person", "project")
2020
        #index_together = [["project", "state"]]
2021

    
2022
    def __str__(self):
2023
        return uenc(_("<'%s' membership in '%s'>") % (
2024
                self.person.username, self.project))
2025

    
2026
    __repr__ = __str__
2027

    
2028
    def __init__(self, *args, **kwargs):
2029
        self.state = self.REQUESTED
2030
        super(ProjectMembership, self).__init__(*args, **kwargs)
2031

    
2032
    def _set_history_item(self, reason, date=None):
2033
        if isinstance(reason, basestring):
2034
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2035

    
2036
        history_item = ProjectMembershipHistory(
2037
                            serial=self.id,
2038
                            person=self.person_id,
2039
                            project=self.project_id,
2040
                            date=date or datetime.now(),
2041
                            reason=reason)
2042
        history_item.save()
2043
        serial = history_item.id
2044

    
2045
    def can_accept(self):
2046
        return self.state == self.REQUESTED
2047

    
2048
    def accept(self):
2049
        if not self.can_accept():
2050
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2051
            raise AssertionError(m)
2052

    
2053
        now = datetime.now()
2054
        self.acceptance_date = now
2055
        self._set_history_item(reason='ACCEPT', date=now)
2056
        self.state = self.ACCEPTED
2057
        self.save()
2058

    
2059
    def can_leave(self):
2060
        return self.state in self.ACCEPTED_STATES
2061

    
2062
    def leave_request(self):
2063
        if not self.can_leave():
2064
            m = _("%s: attempt to request to leave in state '%s'") % (
2065
                self, self.state)
2066
            raise AssertionError(m)
2067

    
2068
        self.leave_request_date = datetime.now()
2069
        self.state = self.LEAVE_REQUESTED
2070
        self.save()
2071

    
2072
    def can_deny_leave(self):
2073
        return self.state == self.LEAVE_REQUESTED
2074

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

    
2081
        self.leave_request_date = None
2082
        self.state = self.ACCEPTED
2083
        self.save()
2084

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

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

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

    
2098
    def can_remove(self):
2099
        return self.state in self.ACCEPTED_STATES
2100

    
2101
    def remove(self):
2102
        if not self.can_remove():
2103
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2104
            raise AssertionError(m)
2105

    
2106
        self._set_history_item(reason='REMOVE')
2107
        self.delete()
2108

    
2109
    def can_reject(self):
2110
        return self.state == self.REQUESTED
2111

    
2112
    def reject(self):
2113
        if not self.can_reject():
2114
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2115
            raise AssertionError(m)
2116

    
2117
        # rejected requests don't need sync,
2118
        # because they were never effected
2119
        self._set_history_item(reason='REJECT')
2120
        self.delete()
2121

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

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

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

    
2135

    
2136
class Serial(models.Model):
2137
    serial  =   models.AutoField(primary_key=True)
2138

    
2139

    
2140
class ProjectMembershipHistory(models.Model):
2141
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2142
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2143

    
2144
    person  =   models.BigIntegerField()
2145
    project =   models.BigIntegerField()
2146
    date    =   models.DateTimeField(auto_now_add=True)
2147
    reason  =   models.IntegerField()
2148
    serial  =   models.BigIntegerField()
2149

    
2150
### SIGNALS ###
2151
################
2152

    
2153
def create_astakos_user(u):
2154
    try:
2155
        AstakosUser.objects.get(user_ptr=u.pk)
2156
    except AstakosUser.DoesNotExist:
2157
        extended_user = AstakosUser(user_ptr_id=u.pk)
2158
        extended_user.__dict__.update(u.__dict__)
2159
        extended_user.save()
2160
        if not extended_user.has_auth_provider('local'):
2161
            extended_user.add_auth_provider('local')
2162
    except BaseException, e:
2163
        logger.exception(e)
2164

    
2165
def fix_superusers():
2166
    # Associate superusers with AstakosUser
2167
    admins = User.objects.filter(is_superuser=True)
2168
    for u in admins:
2169
        create_astakos_user(u)
2170

    
2171
def user_post_save(sender, instance, created, **kwargs):
2172
    if not created:
2173
        return
2174
    create_astakos_user(instance)
2175
post_save.connect(user_post_save, sender=User)
2176

    
2177
def astakosuser_post_save(sender, instance, created, **kwargs):
2178
    pass
2179

    
2180
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2181

    
2182
def resource_post_save(sender, instance, created, **kwargs):
2183
    pass
2184

    
2185
post_save.connect(resource_post_save, sender=Resource)
2186

    
2187
def renew_token(sender, instance, **kwargs):
2188
    if not instance.auth_token:
2189
        instance.renew_token()
2190
pre_save.connect(renew_token, sender=AstakosUser)
2191
pre_save.connect(renew_token, sender=Component)