Statistics
| Branch: | Tag: | Revision:

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

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

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

    
340

    
341

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

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

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

    
370
    updated = models.DateTimeField(_('Update date'))
371

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

    
380
    has_credits = models.BooleanField(_('Has credits?'), default=False)
381

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

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

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

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

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

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

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

    
427
    policy = models.ManyToManyField(
428
        Resource, null=True, through='AstakosUserQuota')
429

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

    
433
    objects = AstakosUserManager()
434
    forupdate = ForUpdateManager()
435

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

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

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

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

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

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

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

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

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

    
492
    @property
493
    def policies(self):
494
        return self.astakosuserquota_set.select_related().all()
495

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

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

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

    
520
        self.update_uuid()
521

    
522
        if not self.verification_code:
523
            self.renew_verification_code()
524

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

    
529
        super(AstakosUser, self).save(**kwargs)
530

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

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

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

    
554
    def token_expired(self):
555
        return self.auth_token_expires < datetime.now()
556

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

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

    
571
    def __unicode__(self):
572
        return '%s (%s)' % (self.realname, self.email)
573

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

    
581
    def email_change_is_pending(self):
582
        return self.emailchanges.count() > 0
583

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

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

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

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

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

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

    
642
        local = self.get_auth_provider('local')._instance
643
        return local.auth_backend == 'astakos'
644

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

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

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

    
660
    def get_required_providers(self, **kwargs):
661
        return auth.REQUIRED_PROVIDERS.keys()
662

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

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

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

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

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

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

    
708
        modules = astakos_settings.IM_MODULES
709

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

    
715
        providers = sorted(providers, key=key)
716
        return providers
717

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

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

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

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

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

    
743
    def get_inactive_message(self, provider_module, identifier=None):
744
        provider = self.get_auth_provider(provider_module, identifier)
745

    
746
        msg_extra = ''
747
        message = ''
748

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

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

    
772
        return mark_safe(message + u' ' + msg_extra)
773

    
774
    def owns_application(self, application):
775
        return application.owner == self
776

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

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

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

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

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

    
814

    
815
class AstakosUserAuthProviderManager(models.Manager):
816

    
817
    def active(self, **filters):
818
        return self.filter(active=True, **filters)
819

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

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

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

    
843

    
844
class AuthProviderPolicyProfileManager(models.Manager):
845

    
846
    def active(self):
847
        return self.filter(active=True)
848

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

    
855
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
856
            policies.update(profile.policies)
857

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

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

    
879

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

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

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

    
898
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
899
                     'automoderate')
900

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

    
907
    objects = AuthProviderPolicyProfileManager()
908

    
909
    class Meta:
910
        ordering = ['priority']
911

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

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

    
928

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

    
947
    objects = AstakosUserAuthProviderManager()
948

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

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

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

    
965
    @property
966
    def settings(self):
967
        extra_data = {}
968

    
969
        info_data = {}
970
        if self.info_data:
971
            info_data = json.loads(self.info_data)
972

    
973
        extra_data['info'] = info_data
974

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

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

    
982
    def __repr__(self):
983
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
984

    
985
    def __unicode__(self):
986
        if self.identifier:
987
            return "%s:%s" % (self.module, self.identifier)
988
        if self.auth_backend:
989
            return "%s:%s" % (self.module, self.auth_backend)
990
        return self.module
991

    
992
    def save(self, *args, **kwargs):
993
        self.info_data = json.dumps(self.info)
994
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
995

    
996

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

    
1024
    update_or_create = _update_or_create
1025

    
1026

    
1027
class AstakosUserQuota(models.Model):
1028
    objects = ExtendedManager()
1029
    capacity = intDecimalField()
1030
    resource = models.ForeignKey(Resource)
1031
    user = models.ForeignKey(AstakosUser)
1032

    
1033
    class Meta:
1034
        unique_together = ("resource", "user")
1035

    
1036

    
1037
class ApprovalTerms(models.Model):
1038
    """
1039
    Model for approval terms
1040
    """
1041

    
1042
    date = models.DateTimeField(
1043
        _('Issue date'), db_index=True, auto_now_add=True)
1044
    location = models.CharField(_('Terms location'), max_length=255)
1045

    
1046

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

    
1060
    def __init__(self, *args, **kwargs):
1061
        super(Invitation, self).__init__(*args, **kwargs)
1062
        if not self.id:
1063
            self.code = _generate_invitation_code()
1064

    
1065
    def consume(self):
1066
        self.is_consumed = True
1067
        self.consumed = datetime.now()
1068
        self.save()
1069

    
1070
    def __unicode__(self):
1071
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1072

    
1073

    
1074
class EmailChangeManager(models.Manager):
1075

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

1082
        If the key is valid and has not expired, return the ``User``
1083
        after activating.
1084

1085
        If the key is not valid or has expired, return ``None``.
1086

1087
        If the key is valid but the ``User`` is already active,
1088
        return ``None``.
1089

1090
        After successful email change the activation record is deleted.
1091

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

    
1121

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

    
1134
    objects = EmailChangeManager()
1135

    
1136
    def get_url(self):
1137
        return reverse('email_change_confirm',
1138
                      kwargs={'activation_key': self.activation_key})
1139

    
1140
    def activation_key_expired(self):
1141
        expiration_date = timedelta(days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1142
        return self.requested_at + expiration_date < datetime.now()
1143

    
1144

    
1145
class AdditionalMail(models.Model):
1146
    """
1147
    Model for registring invitations
1148
    """
1149
    owner = models.ForeignKey(AstakosUser)
1150
    email = models.EmailField()
1151

    
1152

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

    
1162

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

    
1171

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

    
1191
    class Meta:
1192
        unique_together = ("provider", "third_party_identifier")
1193

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

    
1210
        return user
1211

    
1212
    @property
1213
    def realname(self):
1214
        return '%s %s' %(self.first_name, self.last_name)
1215

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

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

    
1236
    def generate_token(self):
1237
        self.password = self.third_party_identifier
1238
        self.last_login = datetime.now()
1239
        self.token = default_token_generator.make_token(self)
1240

    
1241
    def existing_user(self):
1242
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1243
                                         auth_providers__identifier=self.third_party_identifier)
1244

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

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

    
1257

    
1258
class UserSetting(models.Model):
1259
    user = models.ForeignKey(AstakosUser)
1260
    setting = models.CharField(max_length=255)
1261
    value = models.IntegerField()
1262

    
1263
    objects = ForUpdateManager()
1264

    
1265
    class Meta:
1266
        unique_together = ("user", "setting")
1267

    
1268

    
1269
### PROJECTS ###
1270
################
1271

    
1272
class ChainManager(ForUpdateManager):
1273

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

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

    
1287
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1288
        chain_latest = dict(objs.values_list('chain', 'latest'))
1289

    
1290
        objs = ProjectApplication.objects.select_related('applicant')
1291
        apps = objs.in_bulk(chain_latest.values())
1292

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

    
1300
        return d
1301

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

    
1310

    
1311
class Chain(models.Model):
1312
    chain  =   models.AutoField(primary_key=True)
1313

    
1314
    def __str__(self):
1315
        return "%s" % (self.chain,)
1316

    
1317
    objects = ChainManager()
1318

    
1319
    PENDING            = 0
1320
    DENIED             = 3
1321
    DISMISSED          = 4
1322
    CANCELLED          = 5
1323

    
1324
    APPROVED           = 10
1325
    APPROVED_PENDING   = 11
1326
    SUSPENDED          = 12
1327
    SUSPENDED_PENDING  = 13
1328
    TERMINATED         = 14
1329
    TERMINATED_PENDING = 15
1330

    
1331
    PENDING_STATES = [PENDING,
1332
                      APPROVED_PENDING,
1333
                      SUSPENDED_PENDING,
1334
                      TERMINATED_PENDING,
1335
                      ]
1336

    
1337
    MODIFICATION_STATES = [APPROVED_PENDING,
1338
                           SUSPENDED_PENDING,
1339
                           TERMINATED_PENDING,
1340
                           ]
1341

    
1342
    RELEVANT_STATES = [PENDING,
1343
                       DENIED,
1344
                       APPROVED,
1345
                       APPROVED_PENDING,
1346
                       SUSPENDED,
1347
                       SUSPENDED_PENDING,
1348
                       TERMINATED_PENDING,
1349
                       ]
1350

    
1351
    SKIP_STATES = [DISMISSED,
1352
                   CANCELLED,
1353
                   TERMINATED]
1354

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

    
1368

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

    
1376
    @classmethod
1377
    def chain_state(cls, project, app):
1378
        p_state = project.state if project else None
1379
        return cls._chain_state(p_state, app.state)
1380

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

    
1387
    def last_application(self):
1388
        return self.chained_apps.order_by('-id')[0]
1389

    
1390
    def get_project(self):
1391
        try:
1392
            return self.chained_project
1393
        except Project.DoesNotExist:
1394
            return None
1395

    
1396
    def get_elements(self):
1397
        project = self.get_project()
1398
        app = self.last_application()
1399
        return project, app
1400

    
1401
    def get_state(self, project, app):
1402
        s = self.chain_state(project, app)
1403
        return s, project, app
1404

    
1405
    def full_state(self):
1406
        project, app = self.get_elements()
1407
        return self.get_state(project, app)
1408

    
1409

    
1410
def new_chain():
1411
    c = Chain.objects.create()
1412
    return c
1413

    
1414

    
1415
class ProjectApplicationManager(ForUpdateManager):
1416

    
1417
    def user_visible_projects(self, *filters, **kw_filters):
1418
        model = self.model
1419
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1420

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

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

    
1439
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1440

    
1441
    def search_by_name(self, *search_strings):
1442
        q = Q()
1443
        for s in search_strings:
1444
            q = q | Q(name__icontains=s)
1445
        return self.filter(q)
1446

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

    
1453

    
1454
class ProjectApplication(models.Model):
1455
    applicant               =   models.ForeignKey(
1456
                                    AstakosUser,
1457
                                    related_name='projects_applied',
1458
                                    db_index=True)
1459

    
1460
    PENDING     =    0
1461
    APPROVED    =    1
1462
    REPLACED    =    2
1463
    DENIED      =    3
1464
    DISMISSED   =    4
1465
    CANCELLED   =    5
1466

    
1467
    state                   =   models.IntegerField(default=PENDING,
1468
                                                    db_index=True)
1469

    
1470
    owner                   =   models.ForeignKey(
1471
                                    AstakosUser,
1472
                                    related_name='projects_owned',
1473
                                    db_index=True)
1474

    
1475
    chain                   =   models.ForeignKey(Chain,
1476
                                                  related_name='chained_apps',
1477
                                                  db_column='chain')
1478
    precursor_application   =   models.ForeignKey('ProjectApplication',
1479
                                                  null=True,
1480
                                                  blank=True)
1481

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

    
1501
    objects                 =   ProjectApplicationManager()
1502

    
1503
    # Compiled queries
1504
    Q_PENDING  = Q(state=PENDING)
1505
    Q_APPROVED = Q(state=APPROVED)
1506
    Q_DENIED   = Q(state=DENIED)
1507

    
1508
    class Meta:
1509
        unique_together = ("chain", "id")
1510

    
1511
    def __unicode__(self):
1512
        return "%s applied by %s" % (self.name, self.applicant)
1513

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

    
1524
    @property
1525
    def log_display(self):
1526
        return "application %s (%s) for project %s" % (
1527
            self.id, self.name, self.chain)
1528

    
1529
    def state_display(self):
1530
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1531

    
1532
    def project_state_display(self):
1533
        try:
1534
            project = self.project
1535
            return project.state_display()
1536
        except Project.DoesNotExist:
1537
            return self.state_display()
1538

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

    
1545
    def members_count(self):
1546
        return self.project.approved_memberships.count()
1547

    
1548
    @property
1549
    def grants(self):
1550
        return self.projectresourcegrant_set.values('member_capacity',
1551
                                                    'resource__name')
1552

    
1553
    @property
1554
    def resource_policies(self):
1555
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1556

    
1557
    def set_resource_policies(self, policies):
1558
        for resource, uplimit in policies:
1559
            self.add_resource_policy(resource, uplimit)
1560

    
1561
    def pending_modifications_incl_me(self):
1562
        q = self.chained_applications()
1563
        q = q.filter(Q(state=self.PENDING))
1564
        return q
1565

    
1566
    def last_pending_incl_me(self):
1567
        try:
1568
            return self.pending_modifications_incl_me().order_by('-id')[0]
1569
        except IndexError:
1570
            return None
1571

    
1572
    def pending_modifications(self):
1573
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1574

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

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

    
1588
    def chained_applications(self):
1589
        return ProjectApplication.objects.filter(chain=self.chain)
1590

    
1591
    def is_latest(self):
1592
        return self.chained_applications().order_by('-id')[0] == self
1593

    
1594
    def has_pending_modifications(self):
1595
        return bool(self.last_pending())
1596

    
1597
    def denied_modifications(self):
1598
        q = self.chained_applications()
1599
        q = q.filter(Q(state=self.DENIED))
1600
        q = q.filter(~Q(id=self.id))
1601
        return q
1602

    
1603
    def last_denied(self):
1604
        try:
1605
            return self.denied_modifications().order_by('-id')[0]
1606
        except IndexError:
1607
            return None
1608

    
1609
    def has_denied_modifications(self):
1610
        return bool(self.last_denied())
1611

    
1612
    def is_applied(self):
1613
        try:
1614
            self.project
1615
            return True
1616
        except Project.DoesNotExist:
1617
            return False
1618

    
1619
    def get_project(self):
1620
        try:
1621
            return Project.objects.get(id=self.chain)
1622
        except Project.DoesNotExist:
1623
            return None
1624

    
1625
    def project_exists(self):
1626
        return self.get_project() is not None
1627

    
1628
    def can_cancel(self):
1629
        return self.state == self.PENDING
1630

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

    
1637
        self.state = self.CANCELLED
1638
        self.save()
1639

    
1640
    def can_dismiss(self):
1641
        return self.state == self.DENIED
1642

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

    
1649
        self.state = self.DISMISSED
1650
        self.save()
1651

    
1652
    def can_deny(self):
1653
        return self.state == self.PENDING
1654

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

    
1661
        self.state = self.DENIED
1662
        self.response_date = datetime.now()
1663
        self.response = reason
1664
        self.save()
1665

    
1666
    def can_approve(self):
1667
        return self.state == self.PENDING
1668

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

    
1675
        now = datetime.now()
1676
        self.state = self.APPROVED
1677
        self.response_date = now
1678
        self.response = reason
1679
        self.save()
1680

    
1681
        project = self.get_project()
1682
        if project is None:
1683
            project = Project(id=self.chain)
1684

    
1685
        project.name = self.name
1686
        project.application = self
1687
        project.last_approval_date = now
1688
        project.save()
1689
        if project.is_deactivated():
1690
            project.resume()
1691
        return project
1692

    
1693
    @property
1694
    def member_join_policy_display(self):
1695
        policy = self.member_join_policy
1696
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1697

    
1698
    @property
1699
    def member_leave_policy_display(self):
1700
        policy = self.member_leave_policy
1701
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1702

    
1703
class ProjectResourceGrant(models.Model):
1704

    
1705
    resource                =   models.ForeignKey(Resource)
1706
    project_application     =   models.ForeignKey(ProjectApplication,
1707
                                                  null=True)
1708
    project_capacity        =   intDecimalField(null=True)
1709
    member_capacity         =   intDecimalField(default=0)
1710

    
1711
    objects = ExtendedManager()
1712

    
1713
    class Meta:
1714
        unique_together = ("resource", "project_application")
1715

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

    
1729
    def __str__(self):
1730
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1731
                                        self.display_member_capacity())
1732

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

    
1757

    
1758
class ProjectManager(ForUpdateManager):
1759

    
1760
    def terminated_projects(self):
1761
        q = self.model.Q_TERMINATED
1762
        return self.filter(q)
1763

    
1764
    def not_terminated_projects(self):
1765
        q = ~self.model.Q_TERMINATED
1766
        return self.filter(q)
1767

    
1768
    def deactivated_projects(self):
1769
        q = self.model.Q_DEACTIVATED
1770
        return self.filter(q)
1771

    
1772
    def expired_projects(self):
1773
        q = (~Q(state=Project.TERMINATED) &
1774
              Q(application__end_date__lt=datetime.now()))
1775
        return self.filter(q)
1776

    
1777
    def search_by_name(self, *search_strings):
1778
        q = Q()
1779
        for s in search_strings:
1780
            q = q | Q(name__icontains=s)
1781
        return self.filter(q)
1782

    
1783

    
1784
class Project(models.Model):
1785

    
1786
    id                          =   models.OneToOneField(Chain,
1787
                                                      related_name='chained_project',
1788
                                                      db_column='id',
1789
                                                      primary_key=True)
1790

    
1791
    application                 =   models.OneToOneField(
1792
                                            ProjectApplication,
1793
                                            related_name='project')
1794
    last_approval_date          =   models.DateTimeField(null=True)
1795

    
1796
    members                     =   models.ManyToManyField(
1797
                                            AstakosUser,
1798
                                            through='ProjectMembership')
1799

    
1800
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1801
    deactivation_date           =   models.DateTimeField(null=True)
1802

    
1803
    creation_date               =   models.DateTimeField(auto_now_add=True)
1804
    name                        =   models.CharField(
1805
                                            max_length=80,
1806
                                            null=True,
1807
                                            db_index=True,
1808
                                            unique=True)
1809

    
1810
    APPROVED    = 1
1811
    SUSPENDED   = 10
1812
    TERMINATED  = 100
1813

    
1814
    state                       =   models.IntegerField(default=APPROVED,
1815
                                                        db_index=True)
1816

    
1817
    objects     =   ProjectManager()
1818

    
1819
    # Compiled queries
1820
    Q_TERMINATED  = Q(state=TERMINATED)
1821
    Q_SUSPENDED   = Q(state=SUSPENDED)
1822
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1823

    
1824
    def __str__(self):
1825
        return uenc(_("<project %s '%s'>") %
1826
                    (self.id, udec(self.application.name)))
1827

    
1828
    __repr__ = __str__
1829

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

    
1833
    STATE_DISPLAY = {
1834
        APPROVED   : 'Active',
1835
        SUSPENDED  : 'Suspended',
1836
        TERMINATED : 'Terminated'
1837
        }
1838

    
1839
    def state_display(self):
1840
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1841

    
1842
    def expiration_info(self):
1843
        return (str(self.id), self.name, self.state_display(),
1844
                str(self.application.end_date))
1845

    
1846
    def is_deactivated(self, reason=None):
1847
        if reason is not None:
1848
            return self.state == reason
1849

    
1850
        return self.state != self.APPROVED
1851

    
1852
    ### Deactivation calls
1853

    
1854
    def terminate(self):
1855
        self.deactivation_reason = 'TERMINATED'
1856
        self.deactivation_date = datetime.now()
1857
        self.state = self.TERMINATED
1858
        self.name = None
1859
        self.save()
1860

    
1861
    def suspend(self):
1862
        self.deactivation_reason = 'SUSPENDED'
1863
        self.deactivation_date = datetime.now()
1864
        self.state = self.SUSPENDED
1865
        self.save()
1866

    
1867
    def resume(self):
1868
        self.deactivation_reason = None
1869
        self.deactivation_date = None
1870
        self.state = self.APPROVED
1871
        self.save()
1872

    
1873
    ### Logical checks
1874

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

    
1882
    def is_approved(self):
1883
        return self.state == self.APPROVED
1884

    
1885
    @property
1886
    def is_alive(self):
1887
        return not self.is_terminated
1888

    
1889
    @property
1890
    def is_terminated(self):
1891
        return self.is_deactivated(self.TERMINATED)
1892

    
1893
    @property
1894
    def is_suspended(self):
1895
        return self.is_deactivated(self.SUSPENDED)
1896

    
1897
    def violates_resource_grants(self):
1898
        return False
1899

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

    
1907

    
1908
    ### Other
1909

    
1910
    def count_pending_memberships(self):
1911
        return self.projectmembership_set.requested().count()
1912

    
1913
    def members_count(self):
1914
        return self.approved_memberships.count()
1915

    
1916
    @property
1917
    def approved_memberships(self):
1918
        query = ProjectMembership.Q_ACCEPTED_STATES
1919
        return self.projectmembership_set.filter(query)
1920

    
1921
    @property
1922
    def approved_members(self):
1923
        return [m.person for m in self.approved_memberships]
1924

    
1925

    
1926
CHAIN_STATE = {
1927
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1928
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1929
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1930
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1931
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1932

    
1933
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1934
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1935
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1936
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1937
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1938

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

    
1945
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1946
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1947
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1948
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1949
    }
1950

    
1951

    
1952
class ProjectMembershipManager(ForUpdateManager):
1953

    
1954
    def any_accepted(self):
1955
        q = self.model.Q_ACCEPTED_STATES
1956
        return self.filter(q)
1957

    
1958
    def actually_accepted(self):
1959
        q = self.model.Q_ACTUALLY_ACCEPTED
1960
        return self.filter(q)
1961

    
1962
    def requested(self):
1963
        return self.filter(state=ProjectMembership.REQUESTED)
1964

    
1965
    def suspended(self):
1966
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1967

    
1968
class ProjectMembership(models.Model):
1969

    
1970
    person              =   models.ForeignKey(AstakosUser)
1971
    request_date        =   models.DateTimeField(auto_now_add=True)
1972
    project             =   models.ForeignKey(Project)
1973

    
1974
    REQUESTED           =   0
1975
    ACCEPTED            =   1
1976
    LEAVE_REQUESTED     =   5
1977
    # User deactivation
1978
    USER_SUSPENDED      =   10
1979

    
1980
    REMOVED             =   200
1981

    
1982
    ASSOCIATED_STATES   =   set([REQUESTED,
1983
                                 ACCEPTED,
1984
                                 LEAVE_REQUESTED,
1985
                                 USER_SUSPENDED,
1986
                                 ])
1987

    
1988
    ACCEPTED_STATES     =   set([ACCEPTED,
1989
                                 LEAVE_REQUESTED,
1990
                                 USER_SUSPENDED,
1991
                                 ])
1992

    
1993
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1994

    
1995
    state               =   models.IntegerField(default=REQUESTED,
1996
                                                db_index=True)
1997
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
1998
    leave_request_date  =   models.DateTimeField(null=True)
1999

    
2000
    objects     =   ProjectMembershipManager()
2001

    
2002
    # Compiled queries
2003
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
2004
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2005

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

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

    
2022
    def state_display(self):
2023
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2024

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

    
2028
    class Meta:
2029
        unique_together = ("person", "project")
2030
        #index_together = [["project", "state"]]
2031

    
2032
    def __str__(self):
2033
        return uenc(_("<'%s' membership in '%s'>") % (
2034
                self.person.username, self.project))
2035

    
2036
    __repr__ = __str__
2037

    
2038
    def __init__(self, *args, **kwargs):
2039
        self.state = self.REQUESTED
2040
        super(ProjectMembership, self).__init__(*args, **kwargs)
2041

    
2042
    def _set_history_item(self, reason, date=None):
2043
        if isinstance(reason, basestring):
2044
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2045

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

    
2055
    def can_accept(self):
2056
        return self.state == self.REQUESTED
2057

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

    
2063
        now = datetime.now()
2064
        self.acceptance_date = now
2065
        self._set_history_item(reason='ACCEPT', date=now)
2066
        self.state = self.ACCEPTED
2067
        self.save()
2068

    
2069
    def can_leave(self):
2070
        return self.state in self.ACCEPTED_STATES
2071

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

    
2078
        self.leave_request_date = datetime.now()
2079
        self.state = self.LEAVE_REQUESTED
2080
        self.save()
2081

    
2082
    def can_deny_leave(self):
2083
        return self.state == self.LEAVE_REQUESTED
2084

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

    
2091
        self.leave_request_date = None
2092
        self.state = self.ACCEPTED
2093
        self.save()
2094

    
2095
    def can_cancel_leave(self):
2096
        return self.state == self.LEAVE_REQUESTED
2097

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

    
2104
        self.leave_request_date = None
2105
        self.state = self.ACCEPTED
2106
        self.save()
2107

    
2108
    def can_remove(self):
2109
        return self.state in self.ACCEPTED_STATES
2110

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

    
2116
        self._set_history_item(reason='REMOVE')
2117
        self.delete()
2118

    
2119
    def can_reject(self):
2120
        return self.state == self.REQUESTED
2121

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

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

    
2132
    def can_cancel(self):
2133
        return self.state == self.REQUESTED
2134

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

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

    
2145

    
2146
class Serial(models.Model):
2147
    serial  =   models.AutoField(primary_key=True)
2148

    
2149

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

    
2154
    person  =   models.BigIntegerField()
2155
    project =   models.BigIntegerField()
2156
    date    =   models.DateTimeField(auto_now_add=True)
2157
    reason  =   models.IntegerField()
2158
    serial  =   models.BigIntegerField()
2159

    
2160
### SIGNALS ###
2161
################
2162

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

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

    
2181
def user_post_save(sender, instance, created, **kwargs):
2182
    if not created:
2183
        return
2184
    create_astakos_user(instance)
2185
post_save.connect(user_post_save, sender=User)
2186

    
2187
def astakosuser_post_save(sender, instance, created, **kwargs):
2188
    pass
2189

    
2190
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2191

    
2192
def resource_post_save(sender, instance, created, **kwargs):
2193
    pass
2194

    
2195
post_save.connect(resource_post_save, sender=Resource)
2196

    
2197
def renew_token(sender, instance, **kwargs):
2198
    if not instance.auth_token:
2199
        instance.renew_token()
2200
pre_save.connect(renew_token, sender=AstakosUser)
2201
pre_save.connect(renew_token, sender=Component)