Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 3c22bad0

History | View | Annotate | Download (72.5 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 urlparse import urlparse
45
from urllib import quote
46
from random import randint
47

    
48
from django.db import models, IntegrityError, transaction
49
from django.contrib.auth.models import User, UserManager, Group, Permission
50
from django.utils.translation import ugettext as _
51
from django.core.exceptions import ValidationError
52
from django.db.models.signals import (
53
    pre_save, post_save, post_syncdb, post_delete)
54
from django.contrib.contenttypes.models import ContentType
55

    
56
from django.dispatch import Signal
57
from django.db.models import Q, Max
58
from django.core.urlresolvers import reverse
59
from django.utils.http import int_to_base36
60
from django.contrib.auth.tokens import default_token_generator
61
from django.conf import settings
62
from django.utils.importlib import import_module
63
from django.utils.safestring import mark_safe
64
from django.core.validators import email_re
65
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
66

    
67
from synnefo.lib.utils import dict_merge
68

    
69
from astakos.im.settings import (
70
    DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
71
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
72
    SITENAME, MODERATION_ENABLED,
73
    )
74
from astakos.im import settings as astakos_settings
75
from astakos.im import auth_providers as auth
76

    
77
import astakos.im.messages as astakos_messages
78
from snf_django.lib.db.managers import ForUpdateManager
79
from synnefo.lib.ordereddict import OrderedDict
80

    
81
from snf_django.lib.db.fields import intDecimalField
82
from synnefo.util.text import uenc, udec
83
from astakos.im import presentation
84

    
85
logger = logging.getLogger(__name__)
86

    
87
DEFAULT_CONTENT_TYPE = None
88
_content_type = None
89

    
90

    
91
def get_content_type():
92
    global _content_type
93
    if _content_type is not None:
94
        return _content_type
95

    
96
    try:
97
        content_type = ContentType.objects.get(app_label='im',
98
                                               model='astakosuser')
99
    except:
100
        content_type = DEFAULT_CONTENT_TYPE
101
    _content_type = content_type
102
    return content_type
103

    
104
inf = float('inf')
105

    
106

    
107
class Service(models.Model):
108
    name = models.CharField(_('Name'), max_length=255, unique=True,
109
                            db_index=True)
110
    url = models.CharField(_('Service url'), max_length=255, null=True,
111
                           help_text=_("URL the service is accessible from"))
112
    api_url = models.CharField(_('Service API url'), max_length=255, null=True)
113
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
114
                                  null=True, blank=True)
115
    auth_token_created = models.DateTimeField(_('Token creation date'),
116
                                              null=True)
117
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
118
                                              null=True)
119

    
120
    def renew_token(self, expiration_date=None):
121
        md5 = hashlib.md5()
122
        md5.update(self.name.encode('ascii', 'ignore'))
123
        md5.update(self.api_url.encode('ascii', 'ignore'))
124
        md5.update(asctime())
125

    
126
        self.auth_token = b64encode(md5.digest())
127
        self.auth_token_created = datetime.now()
128
        if expiration_date:
129
            self.auth_token_expires = expiration_date
130
        else:
131
            self.auth_token_expires = None
132

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

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

    
143
        for service in services:
144
            d = {'api_url': service.api_url,
145
                 'url': service.url,
146
                 'name': service.name}
147
            if service.name in default_metadata:
148
                metadata[service.name] = default_metadata.get(service.name)
149
                metadata[service.name].update(d)
150
            else:
151
                metadata[service.name] = d
152

    
153

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

    
157
        def service_by_dashbaord_order(s):
158
            return s[1].get('dashboard').get('order')
159

    
160
        metadata = dict_merge(metadata,
161
                              astakos_settings.SERVICES_META)
162

    
163
        for service, info in metadata.iteritems():
164
            default_meta = presentation.service_defaults(service)
165
            base_meta = metadata.get(service, {})
166
            settings_meta = astakos_settings.SERVICES_META.get(service, {})
167
            service_meta = dict_merge(default_meta, base_meta)
168
            meta = dict_merge(service_meta, settings_meta)
169
            catalog[service] = meta
170

    
171
        order_key = service_by_order
172
        if orderfor == 'dashboard':
173
            order_key = service_by_dashbaord_order
174

    
175
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
176
                                             key=order_key))
177
        return ordered_catalog
178

    
179

    
180
_presentation_data = {}
181

    
182

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

    
192

    
193
class Resource(models.Model):
194
    name = models.CharField(_('Name'), max_length=255, unique=True)
195
    desc = models.TextField(_('Description'), null=True)
196
    service = models.ForeignKey(Service)
197
    unit = models.CharField(_('Unit'), null=True, max_length=255)
198
    uplimit = intDecimalField(default=0)
199
    allow_in_projects = models.BooleanField(default=True)
200

    
201
    objects = ForUpdateManager()
202

    
203
    def __str__(self):
204
        return self.name
205

    
206
    def full_name(self):
207
        return str(self)
208

    
209
    def get_info(self):
210
        return {'service': str(self.service),
211
                'description': self.desc,
212
                'unit': self.unit,
213
                'allow_in_projects': self.allow_in_projects,
214
                }
215

    
216
    @property
217
    def group(self):
218
        default = self.name
219
        return get_presentation(str(self)).get('group', default)
220

    
221
    @property
222
    def help_text(self):
223
        default = "%s resource" % self.name
224
        return get_presentation(str(self)).get('help_text', default)
225

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

    
231
    @property
232
    def is_abbreviation(self):
233
        return get_presentation(str(self)).get('is_abbreviation', False)
234

    
235
    @property
236
    def report_desc(self):
237
        default = "%s resource" % self.name
238
        return get_presentation(str(self)).get('report_desc', default)
239

    
240
    @property
241
    def placeholder(self):
242
        return get_presentation(str(self)).get('placeholder', self.unit)
243

    
244
    @property
245
    def verbose_name(self):
246
        return get_presentation(str(self)).get('verbose_name', self.name)
247

    
248
    @property
249
    def display_name(self):
250
        name = self.verbose_name
251
        if self.is_abbreviation:
252
            name = name.upper()
253
        return name
254

    
255
    @property
256
    def pluralized_display_name(self):
257
        if not self.unit:
258
            return '%ss' % self.display_name
259
        return self.display_name
260

    
261
def get_resource_names():
262
    _RESOURCE_NAMES = []
263
    resources = Resource.objects.select_related('service').all()
264
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
265
    return _RESOURCE_NAMES
266

    
267

    
268
class AstakosUserManager(UserManager):
269

    
270
    def get_auth_provider_user(self, provider, **kwargs):
271
        """
272
        Retrieve AstakosUser instance associated with the specified third party
273
        id.
274
        """
275
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
276
                          kwargs.iteritems()))
277
        return self.get(auth_providers__module=provider, **kwargs)
278

    
279
    def get_by_email(self, email):
280
        return self.get(email=email)
281

    
282
    def get_by_identifier(self, email_or_username, **kwargs):
283
        try:
284
            return self.get(email__iexact=email_or_username, **kwargs)
285
        except AstakosUser.DoesNotExist:
286
            return self.get(username__iexact=email_or_username, **kwargs)
287

    
288
    def user_exists(self, email_or_username, **kwargs):
289
        qemail = Q(email__iexact=email_or_username)
290
        qusername = Q(username__iexact=email_or_username)
291
        qextra = Q(**kwargs)
292
        return self.filter((qemail | qusername) & qextra).exists()
293

    
294
    def verified_user_exists(self, email_or_username):
295
        return self.user_exists(email_or_username, email_verified=True)
296

    
297
    def verified(self):
298
        return self.filter(email_verified=True)
299

    
300
    def uuid_catalog(self, l=None):
301
        """
302
        Returns a uuid to username mapping for the uuids appearing in l.
303
        If l is None returns the mapping for all existing users.
304
        """
305
        q = self.filter(uuid__in=l) if l != None else self
306
        return dict(q.values_list('uuid', 'username'))
307

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

    
322

    
323

    
324
class AstakosUser(User):
325
    """
326
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
327
    """
328
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
329
                                   null=True)
330

    
331
    #for invitations
332
    user_level = DEFAULT_USER_LEVEL
333
    level = models.IntegerField(_('Inviter level'), default=user_level)
334
    invitations = models.IntegerField(
335
        _('Invitations left'), default=INVITATIONS_PER_LEVEL.get(user_level, 0))
336

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

    
351
    updated = models.DateTimeField(_('Update date'))
352

    
353
    # Arbitrary text to identify the reason user got deactivated.
354
    # To be used as a reference from administrators.
355
    deactivated_reason = models.TextField(
356
        _('Reason the user was disabled for'),
357
        default=None, null=True)
358
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
359
                                          blank=True)
360

    
361
    has_credits = models.BooleanField(_('Has credits?'), default=False)
362

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

    
366
    # user email is verified
367
    email_verified = models.BooleanField(_('Email verified?'), default=False)
368

    
369
    # unique string used in user email verification url
370
    verification_code = models.CharField(max_length=255, null=True,
371
                                         blank=False, unique=True)
372

    
373
    # date user email verified
374
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
375
                                       blank=True)
376

    
377
    # email verification notice was sent to the user at this time
378
    activation_sent = models.DateTimeField(_('Activation sent date'),
379
                                           null=True, blank=True)
380

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

    
400
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
401
                                           default=False)
402
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
403
                                             null=True, blank=True)
404
    # permanent unique user identifier
405
    uuid = models.CharField(max_length=255, null=True, blank=False,
406
                            unique=True)
407

    
408
    policy = models.ManyToManyField(
409
        Resource, null=True, through='AstakosUserQuota')
410

    
411
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
412
                                           default=False, db_index=True)
413

    
414
    objects = AstakosUserManager()
415
    forupdate = ForUpdateManager()
416

    
417
    def __init__(self, *args, **kwargs):
418
        super(AstakosUser, self).__init__(*args, **kwargs)
419
        if not self.id:
420
            self.is_active = False
421

    
422
    @property
423
    def realname(self):
424
        return '%s %s' % (self.first_name, self.last_name)
425

    
426
    @property
427
    def log_display(self):
428
        """
429
        Should be used in all logger.* calls that refer to a user so that
430
        user display is consistent across log entries.
431
        """
432
        return '%s::%s' % (self.uuid, self.email)
433

    
434
    @realname.setter
435
    def realname(self, value):
436
        parts = value.split(' ')
437
        if len(parts) == 2:
438
            self.first_name = parts[0]
439
            self.last_name = parts[1]
440
        else:
441
            self.last_name = parts[0]
442

    
443
    def add_permission(self, pname):
444
        if self.has_perm(pname):
445
            return
446
        p, created = Permission.objects.get_or_create(
447
                                    codename=pname,
448
                                    name=pname.capitalize(),
449
                                    content_type=get_content_type())
450
        self.user_permissions.add(p)
451

    
452
    def remove_permission(self, pname):
453
        if self.has_perm(pname):
454
            return
455
        p = Permission.objects.get(codename=pname,
456
                                   content_type=get_content_type())
457
        self.user_permissions.remove(p)
458

    
459
    def add_group(self, gname):
460
        group, _ = Group.objects.get_or_create(name=gname)
461
        self.groups.add(group)
462

    
463
    def is_project_admin(self, application_id=None):
464
        return self.uuid in astakos_settings.PROJECT_ADMINS
465

    
466
    @property
467
    def invitation(self):
468
        try:
469
            return Invitation.objects.get(username=self.email)
470
        except Invitation.DoesNotExist:
471
            return None
472

    
473
    @property
474
    def policies(self):
475
        return self.astakosuserquota_set.select_related().all()
476

    
477
    def get_resource_policy(self, resource):
478
        resource = Resource.objects.get(name=resource)
479
        default_capacity = resource.uplimit
480
        try:
481
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
482
            return policy, default_capacity
483
        except AstakosUserQuota.DoesNotExist:
484
            return None, default_capacity
485

    
486
    def update_uuid(self):
487
        while not self.uuid:
488
            uuid_val = str(uuid.uuid4())
489
            try:
490
                AstakosUser.objects.get(uuid=uuid_val)
491
            except AstakosUser.DoesNotExist, e:
492
                self.uuid = uuid_val
493
        return self.uuid
494

    
495
    def save(self, update_timestamps=True, **kwargs):
496
        if update_timestamps:
497
            if not self.id:
498
                self.date_joined = datetime.now()
499
            self.updated = datetime.now()
500

    
501
        self.update_uuid()
502
        # username currently matches email
503
        if self.username != self.email.lower():
504
            self.username = self.email.lower()
505

    
506
        super(AstakosUser, self).save(**kwargs)
507

    
508
    def renew_verification_code(self):
509
        self.verification_code = str(uuid.uuid4())
510
        logger.info("Verification code renewed for %s" % self.log_display)
511

    
512
    def renew_token(self, flush_sessions=False, current_key=None):
513
        md5 = hashlib.md5()
514
        md5.update(settings.SECRET_KEY)
515
        md5.update(self.username)
516
        md5.update(self.realname.encode('ascii', 'ignore'))
517
        md5.update(asctime())
518

    
519
        self.auth_token = b64encode(md5.digest())
520
        self.auth_token_created = datetime.now()
521
        self.auth_token_expires = self.auth_token_created + \
522
                                  timedelta(hours=AUTH_TOKEN_DURATION)
523
        if flush_sessions:
524
            self.flush_sessions(current_key)
525
        msg = 'Token renewed for %s' % self.log_display
526
        logger.log(LOGGING_LEVEL, msg)
527

    
528
    def flush_sessions(self, current_key=None):
529
        q = self.sessions
530
        if current_key:
531
            q = q.exclude(session_key=current_key)
532

    
533
        keys = q.values_list('session_key', flat=True)
534
        if keys:
535
            msg = 'Flushing sessions: %s' % ','.join(keys)
536
            logger.log(LOGGING_LEVEL, msg, [])
537
        engine = import_module(settings.SESSION_ENGINE)
538
        for k in keys:
539
            s = engine.SessionStore(k)
540
            s.flush()
541

    
542
    def __unicode__(self):
543
        return '%s (%s)' % (self.realname, self.email)
544

    
545
    def conflicting_email(self):
546
        q = AstakosUser.objects.exclude(username=self.username)
547
        q = q.filter(email__iexact=self.email)
548
        if q.count() != 0:
549
            return True
550
        return False
551

    
552
    def email_change_is_pending(self):
553
        return self.emailchanges.count() > 0
554

    
555
    @property
556
    def signed_terms(self):
557
        term = get_latest_terms()
558
        if not term:
559
            return True
560
        if not self.has_signed_terms:
561
            return False
562
        if not self.date_signed_terms:
563
            return False
564
        if self.date_signed_terms < term.date:
565
            self.has_signed_terms = False
566
            self.date_signed_terms = None
567
            self.save()
568
            return False
569
        return True
570

    
571
    def set_invitations_level(self):
572
        """
573
        Update user invitation level
574
        """
575
        level = self.invitation.inviter.level + 1
576
        self.level = level
577
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
578

    
579
    def can_change_password(self):
580
        return self.has_auth_provider('local', auth_backend='astakos')
581

    
582
    def can_change_email(self):
583
        if not self.has_auth_provider('local'):
584
            return True
585

    
586
        local = self.get_auth_provider('local')._instance
587
        return local.auth_backend == 'astakos'
588

    
589
    # Auth providers related methods
590
    def get_auth_provider(self, module=None, identifier=None, **filters):
591
        if not module:
592
            return self.auth_providers.active()[0].settings
593

    
594
        params = {'module': module}
595
        if identifier:
596
            params['identifier'] = identifier
597
        params.update(filters)
598
        return self.auth_providers.active().get(**params).settings
599

    
600
    def has_auth_provider(self, provider, **kwargs):
601
        return bool(self.auth_providers.active().filter(module=provider,
602
                                                        **kwargs).count())
603

    
604
    def get_required_providers(self, **kwargs):
605
        return auth.REQUIRED_PROVIDERS.keys()
606

    
607
    def missing_required_providers(self):
608
        required = self.get_required_providers()
609
        missing = []
610
        for provider in required:
611
            if not self.has_auth_provider(provider):
612
                missing.append(auth.get_provider(provider, self))
613
        return missing
614

    
615
    def get_available_auth_providers(self, **filters):
616
        """
617
        Returns a list of providers available for add by the user.
618
        """
619
        modules = astakos_settings.IM_MODULES
620
        providers = []
621
        for p in modules:
622
            providers.append(auth.get_provider(p, self))
623
        available = []
624

    
625
        for p in providers:
626
            if p.get_add_policy:
627
                available.append(p)
628
        return available
629

    
630
    def get_disabled_auth_providers(self, **filters):
631
        providers = self.get_auth_providers(**filters)
632
        disabled = []
633
        for p in providers:
634
            if not p.get_login_policy:
635
                disabled.append(p)
636
        return disabled
637

    
638
    def get_enabled_auth_providers(self, **filters):
639
        providers = self.get_auth_providers(**filters)
640
        enabled = []
641
        for p in providers:
642
            if p.get_login_policy:
643
                enabled.append(p)
644
        return enabled
645

    
646
    def get_auth_providers(self, **filters):
647
        providers = []
648
        for provider in self.auth_providers.active(**filters):
649
            if provider.settings.module_enabled:
650
                providers.append(provider.settings)
651

    
652
        modules = astakos_settings.IM_MODULES
653

    
654
        def key(p):
655
            if not p.module in modules:
656
                return 100
657
            return modules.index(p.module)
658

    
659
        providers = sorted(providers, key=key)
660
        return providers
661

    
662
    # URL methods
663
    @property
664
    def auth_providers_display(self):
665
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
666
                         self.get_enabled_auth_providers()])
667

    
668
    def add_auth_provider(self, module='local', identifier=None, **params):
669
        provider = auth.get_provider(module, self, identifier, **params)
670
        provider.add_to_user()
671

    
672
    def get_resend_activation_url(self):
673
        return reverse('send_activation', kwargs={'user_id': self.pk})
674

    
675
    def get_activation_url(self, nxt=False):
676
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
677
                                 quote(self.verification_code))
678
        if nxt:
679
            url += "&next=%s" % quote(nxt)
680
        return url
681

    
682
    def get_password_reset_url(self, token_generator=default_token_generator):
683
        return reverse('astakos.im.target.local.password_reset_confirm',
684
                          kwargs={'uidb36':int_to_base36(self.id),
685
                                  'token':token_generator.make_token(self)})
686

    
687
    def get_inactive_message(self, provider_module, identifier=None):
688
        provider = self.get_auth_provider(provider_module, identifier)
689

    
690
        msg_extra = ''
691
        message = ''
692

    
693
        msg_inactive = provider.get_account_inactive_msg
694
        msg_pending = provider.get_pending_activation_msg
695
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
696
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
697
        msg_pending_mod = provider.get_pending_moderation_msg
698
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
699
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
700

    
701
        if not self.email_verified:
702
            message = msg_pending
703
            url = self.get_resend_activation_url()
704
            msg_extra = msg_pending_help + \
705
                        u' ' + \
706
                        '<a href="%s">%s?</a>' % (url, msg_resend)
707
        else:
708
            if not self.moderated:
709
                message = msg_pending_mod
710
            else:
711
                if self.is_rejected:
712
                    message = msg_rejected
713
                else:
714
                    message = msg_inactive
715

    
716
        return mark_safe(message + u' ' + msg_extra)
717

    
718
    def owns_application(self, application):
719
        return application.owner == self
720

    
721
    def owns_project(self, project):
722
        return project.application.owner == self
723

    
724
    def is_associated(self, project):
725
        try:
726
            m = ProjectMembership.objects.get(person=self, project=project)
727
            return m.state in ProjectMembership.ASSOCIATED_STATES
728
        except ProjectMembership.DoesNotExist:
729
            return False
730

    
731
    def get_membership(self, project):
732
        try:
733
            return ProjectMembership.objects.get(
734
                project=project,
735
                person=self)
736
        except ProjectMembership.DoesNotExist:
737
            return None
738

    
739
    def membership_display(self, project):
740
        m = self.get_membership(project)
741
        if m is None:
742
            return _('Not a member')
743
        else:
744
            return m.user_friendly_state_display()
745

    
746
    def non_owner_can_view(self, maybe_project):
747
        if self.is_project_admin():
748
            return True
749
        if maybe_project is None:
750
            return False
751
        project = maybe_project
752
        if self.is_associated(project):
753
            return True
754
        if project.is_deactivated():
755
            return False
756
        return True
757

    
758
    def settings(self):
759
        return UserSetting.objects.filter(user=self)
760

    
761

    
762
class AstakosUserAuthProviderManager(models.Manager):
763

    
764
    def active(self, **filters):
765
        return self.filter(active=True, **filters)
766

    
767
    def remove_unverified_providers(self, provider, **filters):
768
        try:
769
            existing = self.filter(module=provider, user__email_verified=False,
770
                                   **filters)
771
            for p in existing:
772
                p.user.delete()
773
        except:
774
            pass
775

    
776
    def unverified(self, provider, **filters):
777
        try:
778
            return self.get(module=provider, user__email_verified=False,
779
                            **filters).settings
780
        except AstakosUserAuthProvider.DoesNotExist:
781
            return None
782

    
783
    def verified(self, provider, **filters):
784
        try:
785
            return self.get(module=provider, user__email_verified=True,
786
                            **filters).settings
787
        except AstakosUserAuthProvider.DoesNotExist:
788
            return None
789

    
790

    
791
class AuthProviderPolicyProfileManager(models.Manager):
792

    
793
    def active(self):
794
        return self.filter(active=True)
795

    
796
    def for_user(self, user, provider):
797
        policies = {}
798
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
799
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
800
        exclusive_q = exclusive_q1 | exclusive_q2
801

    
802
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
803
            policies.update(profile.policies)
804

    
805
        user_groups = user.groups.all().values('pk')
806
        for profile in self.active().filter(groups__in=user_groups).filter(
807
                exclusive_q):
808
            policies.update(profile.policies)
809
        return policies
810

    
811
    def add_policy(self, name, provider, group_or_user, exclusive=False,
812
                   **policies):
813
        is_group = isinstance(group_or_user, Group)
814
        profile, created = self.get_or_create(name=name, provider=provider,
815
                                              is_exclusive=exclusive)
816
        profile.is_exclusive = exclusive
817
        profile.save()
818
        if is_group:
819
            profile.groups.add(group_or_user)
820
        else:
821
            profile.users.add(group_or_user)
822
        profile.set_policies(policies)
823
        profile.save()
824
        return profile
825

    
826

    
827
class AuthProviderPolicyProfile(models.Model):
828
    name = models.CharField(_('Name'), max_length=255, blank=False,
829
                            null=False, db_index=True)
830
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
831
                                null=False)
832

    
833
    # apply policies to all providers excluding the one set in provider field
834
    is_exclusive = models.BooleanField(default=False)
835

    
836
    policy_add = models.NullBooleanField(null=True, default=None)
837
    policy_remove = models.NullBooleanField(null=True, default=None)
838
    policy_create = models.NullBooleanField(null=True, default=None)
839
    policy_login = models.NullBooleanField(null=True, default=None)
840
    policy_limit = models.IntegerField(null=True, default=None)
841
    policy_required = models.NullBooleanField(null=True, default=None)
842
    policy_automoderate = models.NullBooleanField(null=True, default=None)
843
    policy_switch = models.NullBooleanField(null=True, default=None)
844

    
845
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
846
                     'automoderate')
847

    
848
    priority = models.IntegerField(null=False, default=1)
849
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
850
    users = models.ManyToManyField(AstakosUser,
851
                                   related_name='authpolicy_profiles')
852
    active = models.BooleanField(default=True)
853

    
854
    objects = AuthProviderPolicyProfileManager()
855

    
856
    class Meta:
857
        ordering = ['priority']
858

    
859
    @property
860
    def policies(self):
861
        policies = {}
862
        for pkey in self.POLICY_FIELDS:
863
            value = getattr(self, 'policy_%s' % pkey, None)
864
            if value is None:
865
                continue
866
            policies[pkey] = value
867
        return policies
868

    
869
    def set_policies(self, policies_dict):
870
        for key, value in policies_dict.iteritems():
871
            if key in self.POLICY_FIELDS:
872
                setattr(self, 'policy_%s' % key, value)
873
        return self.policies
874

    
875

    
876
class AstakosUserAuthProvider(models.Model):
877
    """
878
    Available user authentication methods.
879
    """
880
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
881
                                   null=True, default=None)
882
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
883
    module = models.CharField(_('Provider'), max_length=255, blank=False,
884
                                default='local')
885
    identifier = models.CharField(_('Third-party identifier'),
886
                                              max_length=255, null=True,
887
                                              blank=True)
888
    active = models.BooleanField(default=True)
889
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
890
                                   default='astakos')
891
    info_data = models.TextField(default="", null=True, blank=True)
892
    created = models.DateTimeField('Creation date', auto_now_add=True)
893

    
894
    objects = AstakosUserAuthProviderManager()
895

    
896
    class Meta:
897
        unique_together = (('identifier', 'module', 'user'), )
898
        ordering = ('module', 'created')
899

    
900
    def __init__(self, *args, **kwargs):
901
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
902
        try:
903
            self.info = json.loads(self.info_data)
904
            if not self.info:
905
                self.info = {}
906
        except Exception, e:
907
            self.info = {}
908

    
909
        for key,value in self.info.iteritems():
910
            setattr(self, 'info_%s' % key, value)
911

    
912
    @property
913
    def settings(self):
914
        extra_data = {}
915

    
916
        info_data = {}
917
        if self.info_data:
918
            info_data = json.loads(self.info_data)
919

    
920
        extra_data['info'] = info_data
921

    
922
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
923
            extra_data[key] = getattr(self, key)
924

    
925
        extra_data['instance'] = self
926
        return auth.get_provider(self.module, self.user,
927
                                           self.identifier, **extra_data)
928

    
929
    def __repr__(self):
930
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
931

    
932
    def __unicode__(self):
933
        if self.identifier:
934
            return "%s:%s" % (self.module, self.identifier)
935
        if self.auth_backend:
936
            return "%s:%s" % (self.module, self.auth_backend)
937
        return self.module
938

    
939
    def save(self, *args, **kwargs):
940
        self.info_data = json.dumps(self.info)
941
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
942

    
943

    
944
class ExtendedManager(models.Manager):
945
    def _update_or_create(self, **kwargs):
946
        assert kwargs, \
947
            'update_or_create() must be passed at least one keyword argument'
948
        obj, created = self.get_or_create(**kwargs)
949
        defaults = kwargs.pop('defaults', {})
950
        if created:
951
            return obj, True, False
952
        else:
953
            try:
954
                params = dict(
955
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
956
                params.update(defaults)
957
                for attr, val in params.items():
958
                    if hasattr(obj, attr):
959
                        setattr(obj, attr, val)
960
                sid = transaction.savepoint()
961
                obj.save(force_update=True)
962
                transaction.savepoint_commit(sid)
963
                return obj, False, True
964
            except IntegrityError, e:
965
                transaction.savepoint_rollback(sid)
966
                try:
967
                    return self.get(**kwargs), False, False
968
                except self.model.DoesNotExist:
969
                    raise e
970

    
971
    update_or_create = _update_or_create
972

    
973

    
974
class AstakosUserQuota(models.Model):
975
    objects = ExtendedManager()
976
    capacity = intDecimalField()
977
    resource = models.ForeignKey(Resource)
978
    user = models.ForeignKey(AstakosUser)
979

    
980
    class Meta:
981
        unique_together = ("resource", "user")
982

    
983

    
984
class ApprovalTerms(models.Model):
985
    """
986
    Model for approval terms
987
    """
988

    
989
    date = models.DateTimeField(
990
        _('Issue date'), db_index=True, auto_now_add=True)
991
    location = models.CharField(_('Terms location'), max_length=255)
992

    
993

    
994
class Invitation(models.Model):
995
    """
996
    Model for registring invitations
997
    """
998
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
999
                                null=True)
1000
    realname = models.CharField(_('Real name'), max_length=255)
1001
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1002
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1003
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1004
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1005
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1006

    
1007
    def __init__(self, *args, **kwargs):
1008
        super(Invitation, self).__init__(*args, **kwargs)
1009
        if not self.id:
1010
            self.code = _generate_invitation_code()
1011

    
1012
    def consume(self):
1013
        self.is_consumed = True
1014
        self.consumed = datetime.now()
1015
        self.save()
1016

    
1017
    def __unicode__(self):
1018
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1019

    
1020

    
1021
class EmailChangeManager(models.Manager):
1022

    
1023
    @transaction.commit_on_success
1024
    def change_email(self, activation_key):
1025
        """
1026
        Validate an activation key and change the corresponding
1027
        ``User`` if valid.
1028

1029
        If the key is valid and has not expired, return the ``User``
1030
        after activating.
1031

1032
        If the key is not valid or has expired, return ``None``.
1033

1034
        If the key is valid but the ``User`` is already active,
1035
        return ``None``.
1036

1037
        After successful email change the activation record is deleted.
1038

1039
        Throws ValueError if there is already
1040
        """
1041
        try:
1042
            email_change = self.model.objects.get(
1043
                activation_key=activation_key)
1044
            if email_change.activation_key_expired():
1045
                email_change.delete()
1046
                raise EmailChange.DoesNotExist
1047
            # is there an active user with this address?
1048
            try:
1049
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1050
            except AstakosUser.DoesNotExist:
1051
                pass
1052
            else:
1053
                raise ValueError(_('The new email address is reserved.'))
1054
            # update user
1055
            user = AstakosUser.objects.get(pk=email_change.user_id)
1056
            old_email = user.email
1057
            user.email = email_change.new_email_address
1058
            user.save()
1059
            email_change.delete()
1060
            msg = "User %s changed email from %s to %s" % (user.log_display,
1061
                                                           old_email,
1062
                                                           user.email)
1063
            logger.log(LOGGING_LEVEL, msg)
1064
            return user
1065
        except EmailChange.DoesNotExist:
1066
            raise ValueError(_('Invalid activation key.'))
1067

    
1068

    
1069
class EmailChange(models.Model):
1070
    new_email_address = models.EmailField(
1071
        _(u'new e-mail address'),
1072
        help_text=_('Provide a new email address. Until you verify the new '
1073
                    'address by following the activation link that will be '
1074
                    'sent to it, your old email address will remain active.'))
1075
    user = models.ForeignKey(
1076
        AstakosUser, unique=True, related_name='emailchanges')
1077
    requested_at = models.DateTimeField(auto_now_add=True)
1078
    activation_key = models.CharField(
1079
        max_length=40, unique=True, db_index=True)
1080

    
1081
    objects = EmailChangeManager()
1082

    
1083
    def get_url(self):
1084
        return reverse('email_change_confirm',
1085
                      kwargs={'activation_key': self.activation_key})
1086

    
1087
    def activation_key_expired(self):
1088
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1089
        return self.requested_at + expiration_date < datetime.now()
1090

    
1091

    
1092
class AdditionalMail(models.Model):
1093
    """
1094
    Model for registring invitations
1095
    """
1096
    owner = models.ForeignKey(AstakosUser)
1097
    email = models.EmailField()
1098

    
1099

    
1100
def _generate_invitation_code():
1101
    while True:
1102
        code = randint(1, 2L ** 63 - 1)
1103
        try:
1104
            Invitation.objects.get(code=code)
1105
            # An invitation with this code already exists, try again
1106
        except Invitation.DoesNotExist:
1107
            return code
1108

    
1109

    
1110
def get_latest_terms():
1111
    try:
1112
        term = ApprovalTerms.objects.order_by('-id')[0]
1113
        return term
1114
    except IndexError:
1115
        pass
1116
    return None
1117

    
1118

    
1119
class PendingThirdPartyUser(models.Model):
1120
    """
1121
    Model for registring successful third party user authentications
1122
    """
1123
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1124
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1125
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1126
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1127
                                  null=True)
1128
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1129
                                 null=True)
1130
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1131
                                   null=True)
1132
    username = models.CharField(_('username'), max_length=30, unique=True,
1133
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1134
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1135
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1136
    info = models.TextField(default="", null=True, blank=True)
1137

    
1138
    class Meta:
1139
        unique_together = ("provider", "third_party_identifier")
1140

    
1141
    def get_user_instance(self):
1142
        """
1143
        Create a new AstakosUser instance based on details provided when user
1144
        initially signed up.
1145
        """
1146
        d = copy.copy(self.__dict__)
1147
        d.pop('_state', None)
1148
        d.pop('id', None)
1149
        d.pop('token', None)
1150
        d.pop('created', None)
1151
        d.pop('info', None)
1152
        d.pop('affiliation', None)
1153
        d.pop('provider', None)
1154
        d.pop('third_party_identifier', None)
1155
        user = AstakosUser(**d)
1156

    
1157
        return user
1158

    
1159
    @property
1160
    def realname(self):
1161
        return '%s %s' %(self.first_name, self.last_name)
1162

    
1163
    @realname.setter
1164
    def realname(self, value):
1165
        parts = value.split(' ')
1166
        if len(parts) == 2:
1167
            self.first_name = parts[0]
1168
            self.last_name = parts[1]
1169
        else:
1170
            self.last_name = parts[0]
1171

    
1172
    def save(self, **kwargs):
1173
        if not self.id:
1174
            # set username
1175
            while not self.username:
1176
                username =  uuid.uuid4().hex[:30]
1177
                try:
1178
                    AstakosUser.objects.get(username = username)
1179
                except AstakosUser.DoesNotExist, e:
1180
                    self.username = username
1181
        super(PendingThirdPartyUser, self).save(**kwargs)
1182

    
1183
    def generate_token(self):
1184
        self.password = self.third_party_identifier
1185
        self.last_login = datetime.now()
1186
        self.token = default_token_generator.make_token(self)
1187

    
1188
    def existing_user(self):
1189
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1190
                                         auth_providers__identifier=self.third_party_identifier)
1191

    
1192
    def get_provider(self, user):
1193
        params = {
1194
            'info_data': self.info,
1195
            'affiliation': self.affiliation
1196
        }
1197
        return auth.get_provider(self.provider, user,
1198
                                 self.third_party_identifier, **params)
1199

    
1200
class SessionCatalog(models.Model):
1201
    session_key = models.CharField(_('session key'), max_length=40)
1202
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1203

    
1204

    
1205
class UserSetting(models.Model):
1206
    user = models.ForeignKey(AstakosUser)
1207
    setting = models.CharField(max_length=255)
1208
    value = models.IntegerField()
1209

    
1210
    objects = ForUpdateManager()
1211

    
1212
    class Meta:
1213
        unique_together = ("user", "setting")
1214

    
1215

    
1216
### PROJECTS ###
1217
################
1218

    
1219
class ChainManager(ForUpdateManager):
1220

    
1221
    def search_by_name(self, *search_strings):
1222
        projects = Project.objects.search_by_name(*search_strings)
1223
        chains = [p.id for p in projects]
1224
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1225
        apps = (app for app in apps if app.is_latest())
1226
        app_chains = [app.chain for app in apps if app.chain not in chains]
1227
        return chains + app_chains
1228

    
1229
    def all_full_state(self):
1230
        chains = self.all()
1231
        cids = [c.chain for c in chains]
1232
        projects = Project.objects.select_related('application').in_bulk(cids)
1233

    
1234
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1235
        chain_latest = dict(objs.values_list('chain', 'latest'))
1236

    
1237
        objs = ProjectApplication.objects.select_related('applicant')
1238
        apps = objs.in_bulk(chain_latest.values())
1239

    
1240
        d = {}
1241
        for chain in chains:
1242
            pk = chain.pk
1243
            project = projects.get(pk, None)
1244
            app = apps[chain_latest[pk]]
1245
            d[chain.pk] = chain.get_state(project, app)
1246

    
1247
        return d
1248

    
1249
    def of_project(self, project):
1250
        if project is None:
1251
            return None
1252
        try:
1253
            return self.get(chain=project.id)
1254
        except Chain.DoesNotExist:
1255
            raise AssertionError('project with no chain')
1256

    
1257

    
1258
class Chain(models.Model):
1259
    chain  =   models.AutoField(primary_key=True)
1260

    
1261
    def __str__(self):
1262
        return "%s" % (self.chain,)
1263

    
1264
    objects = ChainManager()
1265

    
1266
    PENDING            = 0
1267
    DENIED             = 3
1268
    DISMISSED          = 4
1269
    CANCELLED          = 5
1270

    
1271
    APPROVED           = 10
1272
    APPROVED_PENDING   = 11
1273
    SUSPENDED          = 12
1274
    SUSPENDED_PENDING  = 13
1275
    TERMINATED         = 14
1276
    TERMINATED_PENDING = 15
1277

    
1278
    PENDING_STATES = [PENDING,
1279
                      APPROVED_PENDING,
1280
                      SUSPENDED_PENDING,
1281
                      TERMINATED_PENDING,
1282
                      ]
1283

    
1284
    MODIFICATION_STATES = [APPROVED_PENDING,
1285
                           SUSPENDED_PENDING,
1286
                           TERMINATED_PENDING,
1287
                           ]
1288

    
1289
    RELEVANT_STATES = [PENDING,
1290
                       DENIED,
1291
                       APPROVED,
1292
                       APPROVED_PENDING,
1293
                       SUSPENDED,
1294
                       SUSPENDED_PENDING,
1295
                       TERMINATED_PENDING,
1296
                       ]
1297

    
1298
    SKIP_STATES = [DISMISSED,
1299
                   CANCELLED,
1300
                   TERMINATED]
1301

    
1302
    STATE_DISPLAY = {
1303
        PENDING            : _("Pending"),
1304
        DENIED             : _("Denied"),
1305
        DISMISSED          : _("Dismissed"),
1306
        CANCELLED          : _("Cancelled"),
1307
        APPROVED           : _("Active"),
1308
        APPROVED_PENDING   : _("Active - Pending"),
1309
        SUSPENDED          : _("Suspended"),
1310
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1311
        TERMINATED         : _("Terminated"),
1312
        TERMINATED_PENDING : _("Terminated - Pending"),
1313
        }
1314

    
1315

    
1316
    @classmethod
1317
    def _chain_state(cls, project_state, app_state):
1318
        s = CHAIN_STATE.get((project_state, app_state), None)
1319
        if s is None:
1320
            raise AssertionError('inconsistent chain state')
1321
        return s
1322

    
1323
    @classmethod
1324
    def chain_state(cls, project, app):
1325
        p_state = project.state if project else None
1326
        return cls._chain_state(p_state, app.state)
1327

    
1328
    @classmethod
1329
    def state_display(cls, s):
1330
        if s is None:
1331
            return _("Unknown")
1332
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1333

    
1334
    def last_application(self):
1335
        return self.chained_apps.order_by('-id')[0]
1336

    
1337
    def get_project(self):
1338
        try:
1339
            return self.chained_project
1340
        except Project.DoesNotExist:
1341
            return None
1342

    
1343
    def get_elements(self):
1344
        project = self.get_project()
1345
        app = self.last_application()
1346
        return project, app
1347

    
1348
    def get_state(self, project, app):
1349
        s = self.chain_state(project, app)
1350
        return s, project, app
1351

    
1352
    def full_state(self):
1353
        project, app = self.get_elements()
1354
        return self.get_state(project, app)
1355

    
1356

    
1357
def new_chain():
1358
    c = Chain.objects.create()
1359
    return c
1360

    
1361

    
1362
class ProjectApplicationManager(ForUpdateManager):
1363

    
1364
    def user_visible_projects(self, *filters, **kw_filters):
1365
        model = self.model
1366
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1367

    
1368
    def user_visible_by_chain(self, flt):
1369
        model = self.model
1370
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1371
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1372
        by_chain = dict(pending.annotate(models.Max('id')))
1373
        by_chain.update(approved.annotate(models.Max('id')))
1374
        return self.filter(flt, id__in=by_chain.values())
1375

    
1376
    def user_accessible_projects(self, user):
1377
        """
1378
        Return projects accessed by specified user.
1379
        """
1380
        if user.is_project_admin():
1381
            participates_filters = Q()
1382
        else:
1383
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1384
                                   Q(project__projectmembership__person=user)
1385

    
1386
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1387

    
1388
    def search_by_name(self, *search_strings):
1389
        q = Q()
1390
        for s in search_strings:
1391
            q = q | Q(name__icontains=s)
1392
        return self.filter(q)
1393

    
1394
    def latest_of_chain(self, chain_id):
1395
        try:
1396
            return self.filter(chain=chain_id).order_by('-id')[0]
1397
        except IndexError:
1398
            return None
1399

    
1400

    
1401
class ProjectApplication(models.Model):
1402
    applicant               =   models.ForeignKey(
1403
                                    AstakosUser,
1404
                                    related_name='projects_applied',
1405
                                    db_index=True)
1406

    
1407
    PENDING     =    0
1408
    APPROVED    =    1
1409
    REPLACED    =    2
1410
    DENIED      =    3
1411
    DISMISSED   =    4
1412
    CANCELLED   =    5
1413

    
1414
    state                   =   models.IntegerField(default=PENDING,
1415
                                                    db_index=True)
1416

    
1417
    owner                   =   models.ForeignKey(
1418
                                    AstakosUser,
1419
                                    related_name='projects_owned',
1420
                                    db_index=True)
1421

    
1422
    chain                   =   models.ForeignKey(Chain,
1423
                                                  related_name='chained_apps',
1424
                                                  db_column='chain')
1425
    precursor_application   =   models.ForeignKey('ProjectApplication',
1426
                                                  null=True,
1427
                                                  blank=True)
1428

    
1429
    name                    =   models.CharField(max_length=80)
1430
    homepage                =   models.URLField(max_length=255, null=True,
1431
                                                verify_exists=False)
1432
    description             =   models.TextField(null=True, blank=True)
1433
    start_date              =   models.DateTimeField(null=True, blank=True)
1434
    end_date                =   models.DateTimeField()
1435
    member_join_policy      =   models.IntegerField()
1436
    member_leave_policy     =   models.IntegerField()
1437
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1438
    resource_grants         =   models.ManyToManyField(
1439
                                    Resource,
1440
                                    null=True,
1441
                                    blank=True,
1442
                                    through='ProjectResourceGrant')
1443
    comments                =   models.TextField(null=True, blank=True)
1444
    issue_date              =   models.DateTimeField(auto_now_add=True)
1445
    response_date           =   models.DateTimeField(null=True, blank=True)
1446
    response                =   models.TextField(null=True, blank=True)
1447

    
1448
    objects                 =   ProjectApplicationManager()
1449

    
1450
    # Compiled queries
1451
    Q_PENDING  = Q(state=PENDING)
1452
    Q_APPROVED = Q(state=APPROVED)
1453
    Q_DENIED   = Q(state=DENIED)
1454

    
1455
    class Meta:
1456
        unique_together = ("chain", "id")
1457

    
1458
    def __unicode__(self):
1459
        return "%s applied by %s" % (self.name, self.applicant)
1460

    
1461
    # TODO: Move to a more suitable place
1462
    APPLICATION_STATE_DISPLAY = {
1463
        PENDING  : _('Pending review'),
1464
        APPROVED : _('Approved'),
1465
        REPLACED : _('Replaced'),
1466
        DENIED   : _('Denied'),
1467
        DISMISSED: _('Dismissed'),
1468
        CANCELLED: _('Cancelled')
1469
    }
1470

    
1471
    @property
1472
    def log_display(self):
1473
        return "application %s (%s) for project %s" % (
1474
            self.id, self.name, self.chain)
1475

    
1476
    def state_display(self):
1477
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1478

    
1479
    def project_state_display(self):
1480
        try:
1481
            project = self.project
1482
            return project.state_display()
1483
        except Project.DoesNotExist:
1484
            return self.state_display()
1485

    
1486
    def add_resource_policy(self, resource, uplimit):
1487
        """Raises ObjectDoesNotExist, IntegrityError"""
1488
        q = self.projectresourcegrant_set
1489
        resource = Resource.objects.get(name=resource)
1490
        q.create(resource=resource, member_capacity=uplimit)
1491

    
1492
    def members_count(self):
1493
        return self.project.approved_memberships.count()
1494

    
1495
    @property
1496
    def grants(self):
1497
        return self.projectresourcegrant_set.values('member_capacity',
1498
                                                    'resource__name')
1499

    
1500
    @property
1501
    def resource_policies(self):
1502
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1503

    
1504
    def set_resource_policies(self, policies):
1505
        for resource, uplimit in policies:
1506
            self.add_resource_policy(resource, uplimit)
1507

    
1508
    def pending_modifications_incl_me(self):
1509
        q = self.chained_applications()
1510
        q = q.filter(Q(state=self.PENDING))
1511
        return q
1512

    
1513
    def last_pending_incl_me(self):
1514
        try:
1515
            return self.pending_modifications_incl_me().order_by('-id')[0]
1516
        except IndexError:
1517
            return None
1518

    
1519
    def pending_modifications(self):
1520
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1521

    
1522
    def last_pending(self):
1523
        try:
1524
            return self.pending_modifications().order_by('-id')[0]
1525
        except IndexError:
1526
            return None
1527

    
1528
    def is_modification(self):
1529
        # if self.state != self.PENDING:
1530
        #     return False
1531
        parents = self.chained_applications().filter(id__lt=self.id)
1532
        parents = parents.filter(state__in=[self.APPROVED])
1533
        return parents.count() > 0
1534

    
1535
    def chained_applications(self):
1536
        return ProjectApplication.objects.filter(chain=self.chain)
1537

    
1538
    def is_latest(self):
1539
        return self.chained_applications().order_by('-id')[0] == self
1540

    
1541
    def has_pending_modifications(self):
1542
        return bool(self.last_pending())
1543

    
1544
    def denied_modifications(self):
1545
        q = self.chained_applications()
1546
        q = q.filter(Q(state=self.DENIED))
1547
        q = q.filter(~Q(id=self.id))
1548
        return q
1549

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

    
1556
    def has_denied_modifications(self):
1557
        return bool(self.last_denied())
1558

    
1559
    def is_applied(self):
1560
        try:
1561
            self.project
1562
            return True
1563
        except Project.DoesNotExist:
1564
            return False
1565

    
1566
    def get_project(self):
1567
        try:
1568
            return Project.objects.get(id=self.chain)
1569
        except Project.DoesNotExist:
1570
            return None
1571

    
1572
    def project_exists(self):
1573
        return self.get_project() is not None
1574

    
1575
    def can_cancel(self):
1576
        return self.state == self.PENDING
1577

    
1578
    def cancel(self):
1579
        if not self.can_cancel():
1580
            m = _("cannot cancel: application '%s' in state '%s'") % (
1581
                    self.id, self.state)
1582
            raise AssertionError(m)
1583

    
1584
        self.state = self.CANCELLED
1585
        self.save()
1586

    
1587
    def can_dismiss(self):
1588
        return self.state == self.DENIED
1589

    
1590
    def dismiss(self):
1591
        if not self.can_dismiss():
1592
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1593
                    self.id, self.state)
1594
            raise AssertionError(m)
1595

    
1596
        self.state = self.DISMISSED
1597
        self.save()
1598

    
1599
    def can_deny(self):
1600
        return self.state == self.PENDING
1601

    
1602
    def deny(self, reason):
1603
        if not self.can_deny():
1604
            m = _("cannot deny: application '%s' in state '%s'") % (
1605
                    self.id, self.state)
1606
            raise AssertionError(m)
1607

    
1608
        self.state = self.DENIED
1609
        self.response_date = datetime.now()
1610
        self.response = reason
1611
        self.save()
1612

    
1613
    def can_approve(self):
1614
        return self.state == self.PENDING
1615

    
1616
    def approve(self, reason):
1617
        if not self.can_approve():
1618
            m = _("cannot approve: project '%s' in state '%s'") % (
1619
                    self.name, self.state)
1620
            raise AssertionError(m) # invalid argument
1621

    
1622
        now = datetime.now()
1623
        self.state = self.APPROVED
1624
        self.response_date = now
1625
        self.response = reason
1626
        self.save()
1627

    
1628
        project = self.get_project()
1629
        if project is None:
1630
            project = Project(id=self.chain)
1631

    
1632
        project.name = self.name
1633
        project.application = self
1634
        project.last_approval_date = now
1635
        project.save()
1636
        return project
1637

    
1638
    @property
1639
    def member_join_policy_display(self):
1640
        policy = self.member_join_policy
1641
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1642

    
1643
    @property
1644
    def member_leave_policy_display(self):
1645
        policy = self.member_leave_policy
1646
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1647

    
1648
class ProjectResourceGrant(models.Model):
1649

    
1650
    resource                =   models.ForeignKey(Resource)
1651
    project_application     =   models.ForeignKey(ProjectApplication,
1652
                                                  null=True)
1653
    project_capacity        =   intDecimalField(null=True)
1654
    member_capacity         =   intDecimalField(default=0)
1655

    
1656
    objects = ExtendedManager()
1657

    
1658
    class Meta:
1659
        unique_together = ("resource", "project_application")
1660

    
1661
    def display_member_capacity(self):
1662
        if self.member_capacity:
1663
            if self.resource.unit:
1664
                return ProjectResourceGrant.display_filesize(
1665
                    self.member_capacity)
1666
            else:
1667
                if math.isinf(self.member_capacity):
1668
                    return 'Unlimited'
1669
                else:
1670
                    return self.member_capacity
1671
        else:
1672
            return 'Unlimited'
1673

    
1674
    def __str__(self):
1675
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1676
                                        self.display_member_capacity())
1677

    
1678
    @classmethod
1679
    def display_filesize(cls, value):
1680
        try:
1681
            value = float(value)
1682
        except:
1683
            return
1684
        else:
1685
            if math.isinf(value):
1686
                return 'Unlimited'
1687
            if value > 1:
1688
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1689
                                [0, 0, 0, 0, 0, 0])
1690
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1691
                quotient = float(value) / 1024**exponent
1692
                unit, value_decimals = unit_list[exponent]
1693
                format_string = '{0:.%sf} {1}' % (value_decimals)
1694
                return format_string.format(quotient, unit)
1695
            if value == 0:
1696
                return '0 bytes'
1697
            if value == 1:
1698
                return '1 byte'
1699
            else:
1700
               return '0'
1701

    
1702

    
1703
class ProjectManager(ForUpdateManager):
1704

    
1705
    def terminated_projects(self):
1706
        q = self.model.Q_TERMINATED
1707
        return self.filter(q)
1708

    
1709
    def not_terminated_projects(self):
1710
        q = ~self.model.Q_TERMINATED
1711
        return self.filter(q)
1712

    
1713
    def deactivated_projects(self):
1714
        q = self.model.Q_DEACTIVATED
1715
        return self.filter(q)
1716

    
1717
    def expired_projects(self):
1718
        q = (~Q(state=Project.TERMINATED) &
1719
              Q(application__end_date__lt=datetime.now()))
1720
        return self.filter(q)
1721

    
1722
    def search_by_name(self, *search_strings):
1723
        q = Q()
1724
        for s in search_strings:
1725
            q = q | Q(name__icontains=s)
1726
        return self.filter(q)
1727

    
1728

    
1729
class Project(models.Model):
1730

    
1731
    id                          =   models.OneToOneField(Chain,
1732
                                                      related_name='chained_project',
1733
                                                      db_column='id',
1734
                                                      primary_key=True)
1735

    
1736
    application                 =   models.OneToOneField(
1737
                                            ProjectApplication,
1738
                                            related_name='project')
1739
    last_approval_date          =   models.DateTimeField(null=True)
1740

    
1741
    members                     =   models.ManyToManyField(
1742
                                            AstakosUser,
1743
                                            through='ProjectMembership')
1744

    
1745
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1746
    deactivation_date           =   models.DateTimeField(null=True)
1747

    
1748
    creation_date               =   models.DateTimeField(auto_now_add=True)
1749
    name                        =   models.CharField(
1750
                                            max_length=80,
1751
                                            null=True,
1752
                                            db_index=True,
1753
                                            unique=True)
1754

    
1755
    APPROVED    = 1
1756
    SUSPENDED   = 10
1757
    TERMINATED  = 100
1758

    
1759
    state                       =   models.IntegerField(default=APPROVED,
1760
                                                        db_index=True)
1761

    
1762
    objects     =   ProjectManager()
1763

    
1764
    # Compiled queries
1765
    Q_TERMINATED  = Q(state=TERMINATED)
1766
    Q_SUSPENDED   = Q(state=SUSPENDED)
1767
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1768

    
1769
    def __str__(self):
1770
        return uenc(_("<project %s '%s'>") %
1771
                    (self.id, udec(self.application.name)))
1772

    
1773
    __repr__ = __str__
1774

    
1775
    def __unicode__(self):
1776
        return _("<project %s '%s'>") % (self.id, self.application.name)
1777

    
1778
    STATE_DISPLAY = {
1779
        APPROVED   : 'Active',
1780
        SUSPENDED  : 'Suspended',
1781
        TERMINATED : 'Terminated'
1782
        }
1783

    
1784
    def state_display(self):
1785
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1786

    
1787
    def expiration_info(self):
1788
        return (str(self.id), self.name, self.state_display(),
1789
                str(self.application.end_date))
1790

    
1791
    def is_deactivated(self, reason=None):
1792
        if reason is not None:
1793
            return self.state == reason
1794

    
1795
        return self.state != self.APPROVED
1796

    
1797
    ### Deactivation calls
1798

    
1799
    def terminate(self):
1800
        self.deactivation_reason = 'TERMINATED'
1801
        self.deactivation_date = datetime.now()
1802
        self.state = self.TERMINATED
1803
        self.name = None
1804
        self.save()
1805

    
1806
    def suspend(self):
1807
        self.deactivation_reason = 'SUSPENDED'
1808
        self.deactivation_date = datetime.now()
1809
        self.state = self.SUSPENDED
1810
        self.save()
1811

    
1812
    def resume(self):
1813
        self.deactivation_reason = None
1814
        self.deactivation_date = None
1815
        self.state = self.APPROVED
1816
        self.save()
1817

    
1818
    ### Logical checks
1819

    
1820
    def is_inconsistent(self):
1821
        now = datetime.now()
1822
        dates = [self.creation_date,
1823
                 self.last_approval_date,
1824
                 self.deactivation_date]
1825
        return any([date > now for date in dates])
1826

    
1827
    def is_approved(self):
1828
        return self.state == self.APPROVED
1829

    
1830
    @property
1831
    def is_alive(self):
1832
        return not self.is_terminated
1833

    
1834
    @property
1835
    def is_terminated(self):
1836
        return self.is_deactivated(self.TERMINATED)
1837

    
1838
    @property
1839
    def is_suspended(self):
1840
        return self.is_deactivated(self.SUSPENDED)
1841

    
1842
    def violates_resource_grants(self):
1843
        return False
1844

    
1845
    def violates_members_limit(self, adding=0):
1846
        application = self.application
1847
        limit = application.limit_on_members_number
1848
        if limit is None:
1849
            return False
1850
        return (len(self.approved_members) + adding > limit)
1851

    
1852

    
1853
    ### Other
1854

    
1855
    def count_pending_memberships(self):
1856
        memb_set = self.projectmembership_set
1857
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1858
        return memb_count
1859

    
1860
    def members_count(self):
1861
        return self.approved_memberships.count()
1862

    
1863
    @property
1864
    def approved_memberships(self):
1865
        query = ProjectMembership.Q_ACCEPTED_STATES
1866
        return self.projectmembership_set.filter(query)
1867

    
1868
    @property
1869
    def approved_members(self):
1870
        return [m.person for m in self.approved_memberships]
1871

    
1872

    
1873
CHAIN_STATE = {
1874
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1875
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1876
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1877
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1878
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1879

    
1880
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1881
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1882
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1883
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1884
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1885

    
1886
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1887
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1888
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1889
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1890
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1891

    
1892
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1893
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1894
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1895
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1896
    }
1897

    
1898

    
1899
class ProjectMembershipManager(ForUpdateManager):
1900

    
1901
    def any_accepted(self):
1902
        q = self.model.Q_ACTUALLY_ACCEPTED
1903
        return self.filter(q)
1904

    
1905
    def actually_accepted(self):
1906
        q = self.model.Q_ACTUALLY_ACCEPTED
1907
        return self.filter(q)
1908

    
1909
    def requested(self):
1910
        return self.filter(state=ProjectMembership.REQUESTED)
1911

    
1912
    def suspended(self):
1913
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1914

    
1915
class ProjectMembership(models.Model):
1916

    
1917
    person              =   models.ForeignKey(AstakosUser)
1918
    request_date        =   models.DateTimeField(auto_now_add=True)
1919
    project             =   models.ForeignKey(Project)
1920

    
1921
    REQUESTED           =   0
1922
    ACCEPTED            =   1
1923
    LEAVE_REQUESTED     =   5
1924
    # User deactivation
1925
    USER_SUSPENDED      =   10
1926

    
1927
    REMOVED             =   200
1928

    
1929
    ASSOCIATED_STATES   =   set([REQUESTED,
1930
                                 ACCEPTED,
1931
                                 LEAVE_REQUESTED,
1932
                                 USER_SUSPENDED,
1933
                                 ])
1934

    
1935
    ACCEPTED_STATES     =   set([ACCEPTED,
1936
                                 LEAVE_REQUESTED,
1937
                                 USER_SUSPENDED,
1938
                                 ])
1939

    
1940
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1941

    
1942
    state               =   models.IntegerField(default=REQUESTED,
1943
                                                db_index=True)
1944
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
1945
    leave_request_date  =   models.DateTimeField(null=True)
1946

    
1947
    objects     =   ProjectMembershipManager()
1948

    
1949
    # Compiled queries
1950
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1951
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1952

    
1953
    MEMBERSHIP_STATE_DISPLAY = {
1954
        REQUESTED           : _('Requested'),
1955
        ACCEPTED            : _('Accepted'),
1956
        LEAVE_REQUESTED     : _('Leave Requested'),
1957
        USER_SUSPENDED      : _('Suspended'),
1958
        REMOVED             : _('Pending removal'),
1959
        }
1960

    
1961
    USER_FRIENDLY_STATE_DISPLAY = {
1962
        REQUESTED           : _('Join requested'),
1963
        ACCEPTED            : _('Accepted member'),
1964
        LEAVE_REQUESTED     : _('Requested to leave'),
1965
        USER_SUSPENDED      : _('Suspended member'),
1966
        REMOVED             : _('Pending removal'),
1967
        }
1968

    
1969
    def state_display(self):
1970
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1971

    
1972
    def user_friendly_state_display(self):
1973
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1974

    
1975
    class Meta:
1976
        unique_together = ("person", "project")
1977
        #index_together = [["project", "state"]]
1978

    
1979
    def __str__(self):
1980
        return uenc(_("<'%s' membership in '%s'>") % (
1981
                self.person.username, self.project))
1982

    
1983
    __repr__ = __str__
1984

    
1985
    def __init__(self, *args, **kwargs):
1986
        self.state = self.REQUESTED
1987
        super(ProjectMembership, self).__init__(*args, **kwargs)
1988

    
1989
    def _set_history_item(self, reason, date=None):
1990
        if isinstance(reason, basestring):
1991
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1992

    
1993
        history_item = ProjectMembershipHistory(
1994
                            serial=self.id,
1995
                            person=self.person_id,
1996
                            project=self.project_id,
1997
                            date=date or datetime.now(),
1998
                            reason=reason)
1999
        history_item.save()
2000
        serial = history_item.id
2001

    
2002
    def can_accept(self):
2003
        return self.state == self.REQUESTED
2004

    
2005
    def accept(self):
2006
        if not self.can_accept():
2007
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2008
            raise AssertionError(m)
2009

    
2010
        now = datetime.now()
2011
        self.acceptance_date = now
2012
        self._set_history_item(reason='ACCEPT', date=now)
2013
        self.state = self.ACCEPTED
2014
        self.save()
2015

    
2016
    def can_leave(self):
2017
        return self.state in self.ACCEPTED_STATES
2018

    
2019
    def leave_request(self):
2020
        if not self.can_leave():
2021
            m = _("%s: attempt to request to leave in state '%s'") % (
2022
                self, self.state)
2023
            raise AssertionError(m)
2024

    
2025
        self.leave_request_date = datetime.now()
2026
        self.state = self.LEAVE_REQUESTED
2027
        self.save()
2028

    
2029
    def can_deny_leave(self):
2030
        return self.state == self.LEAVE_REQUESTED
2031

    
2032
    def leave_request_deny(self):
2033
        if not self.can_deny_leave():
2034
            m = _("%s: attempt to deny leave request in state '%s'") % (
2035
                self, self.state)
2036
            raise AssertionError(m)
2037

    
2038
        self.leave_request_date = None
2039
        self.state = self.ACCEPTED
2040
        self.save()
2041

    
2042
    def can_cancel_leave(self):
2043
        return self.state == self.LEAVE_REQUESTED
2044

    
2045
    def leave_request_cancel(self):
2046
        if not self.can_cancel_leave():
2047
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2048
                self, self.state)
2049
            raise AssertionError(m)
2050

    
2051
        self.leave_request_date = None
2052
        self.state = self.ACCEPTED
2053
        self.save()
2054

    
2055
    def can_remove(self):
2056
        return self.state in self.ACCEPTED_STATES
2057

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

    
2063
        self._set_history_item(reason='REMOVE')
2064
        self.delete()
2065

    
2066
    def can_reject(self):
2067
        return self.state == self.REQUESTED
2068

    
2069
    def reject(self):
2070
        if not self.can_reject():
2071
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2072
            raise AssertionError(m)
2073

    
2074
        # rejected requests don't need sync,
2075
        # because they were never effected
2076
        self._set_history_item(reason='REJECT')
2077
        self.delete()
2078

    
2079
    def can_cancel(self):
2080
        return self.state == self.REQUESTED
2081

    
2082
    def cancel(self):
2083
        if not self.can_cancel():
2084
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2085
            raise AssertionError(m)
2086

    
2087
        # rejected requests don't need sync,
2088
        # because they were never effected
2089
        self._set_history_item(reason='CANCEL')
2090
        self.delete()
2091

    
2092

    
2093
class Serial(models.Model):
2094
    serial  =   models.AutoField(primary_key=True)
2095

    
2096

    
2097
class ProjectMembershipHistory(models.Model):
2098
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2099
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2100

    
2101
    person  =   models.BigIntegerField()
2102
    project =   models.BigIntegerField()
2103
    date    =   models.DateTimeField(auto_now_add=True)
2104
    reason  =   models.IntegerField()
2105
    serial  =   models.BigIntegerField()
2106

    
2107
### SIGNALS ###
2108
################
2109

    
2110
def create_astakos_user(u):
2111
    try:
2112
        AstakosUser.objects.get(user_ptr=u.pk)
2113
    except AstakosUser.DoesNotExist:
2114
        extended_user = AstakosUser(user_ptr_id=u.pk)
2115
        extended_user.__dict__.update(u.__dict__)
2116
        extended_user.save()
2117
        if not extended_user.has_auth_provider('local'):
2118
            extended_user.add_auth_provider('local')
2119
    except BaseException, e:
2120
        logger.exception(e)
2121

    
2122
def fix_superusers():
2123
    # Associate superusers with AstakosUser
2124
    admins = User.objects.filter(is_superuser=True)
2125
    for u in admins:
2126
        create_astakos_user(u)
2127

    
2128
def user_post_save(sender, instance, created, **kwargs):
2129
    if not created:
2130
        return
2131
    create_astakos_user(instance)
2132
post_save.connect(user_post_save, sender=User)
2133

    
2134
def astakosuser_post_save(sender, instance, created, **kwargs):
2135
    pass
2136

    
2137
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2138

    
2139
def resource_post_save(sender, instance, created, **kwargs):
2140
    pass
2141

    
2142
post_save.connect(resource_post_save, sender=Resource)
2143

    
2144
def renew_token(sender, instance, **kwargs):
2145
    if not instance.auth_token:
2146
        instance.renew_token()
2147
pre_save.connect(renew_token, sender=AstakosUser)
2148
pre_save.connect(renew_token, sender=Service)