Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 8998f09a

History | View | Annotate | Download (73 kB)

1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import hashlib
35
import uuid
36
import logging
37
import json
38
import math
39
import copy
40

    
41
from time import asctime
42
from datetime import datetime, timedelta
43
from base64 import b64encode
44
from urllib import quote
45
from random import randint
46

    
47
from django.db import models, IntegrityError, transaction
48
from django.contrib.auth.models import User, UserManager, Group, Permission
49
from django.utils.translation import ugettext as _
50
from django.db.models.signals import pre_save, post_save
51
from django.contrib.contenttypes.models import ContentType
52

    
53
from django.db.models import Q, Max
54
from django.core.urlresolvers import reverse
55
from django.utils.http import int_to_base36
56
from django.contrib.auth.tokens import default_token_generator
57
from django.conf import settings
58
from django.utils.importlib import import_module
59
from django.utils.safestring import mark_safe
60

    
61
from synnefo.lib.utils import dict_merge
62

    
63
from astakos.im import settings as astakos_settings
64
from astakos.im import auth_providers as auth
65

    
66
import astakos.im.messages as astakos_messages
67
from snf_django.lib.db.managers import ForUpdateManager
68
from synnefo.lib.ordereddict import OrderedDict
69

    
70
from snf_django.lib.db.fields import intDecimalField
71
from synnefo.util.text import uenc, udec
72
from astakos.im import presentation
73

    
74
logger = logging.getLogger(__name__)
75

    
76
DEFAULT_CONTENT_TYPE = None
77
_content_type = None
78

    
79

    
80
def get_content_type():
81
    global _content_type
82
    if _content_type is not None:
83
        return _content_type
84

    
85
    try:
86
        content_type = ContentType.objects.get(app_label='im',
87
                                               model='astakosuser')
88
    except:
89
        content_type = DEFAULT_CONTENT_TYPE
90
    _content_type = content_type
91
    return content_type
92

    
93
inf = float('inf')
94

    
95

    
96
class Service(models.Model):
97
    name = models.CharField(_('Name'), max_length=255, unique=True,
98
                            db_index=True)
99
    url = models.CharField(_('Service url'), max_length=255, null=True,
100
                           help_text=_("URL the service is accessible from"))
101
    api_url = models.CharField(_('Service API url'), max_length=255, null=True)
102
    auth_token = models.CharField(_('Authentication Token'), max_length=32,
103
                                  null=True, blank=True)
104
    auth_token_created = models.DateTimeField(_('Token creation date'),
105
                                              null=True)
106
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
107
                                              null=True)
108

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

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

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

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

    
132
        for service in services:
133
            d = {'api_url': service.api_url,
134
                 'url': service.url,
135
                 'name': service.name}
136
            if service.name in default_metadata:
137
                metadata[service.name] = default_metadata.get(service.name)
138
                metadata[service.name].update(d)
139
            else:
140
                metadata[service.name] = d
141

    
142

    
143
        def service_by_order(s):
144
            return s[1].get('order')
145

    
146
        def service_by_dashbaord_order(s):
147
            return s[1].get('dashboard').get('order')
148

    
149
        metadata = dict_merge(metadata,
150
                              astakos_settings.SERVICES_META)
151

    
152
        for service, info in metadata.iteritems():
153
            default_meta = presentation.service_defaults(service)
154
            base_meta = metadata.get(service, {})
155
            settings_meta = astakos_settings.SERVICES_META.get(service, {})
156
            service_meta = dict_merge(default_meta, base_meta)
157
            meta = dict_merge(service_meta, settings_meta)
158
            catalog[service] = meta
159

    
160
        order_key = service_by_order
161
        if orderfor == 'dashboard':
162
            order_key = service_by_dashbaord_order
163

    
164
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
165
                                             key=order_key))
166
        return ordered_catalog
167

    
168

    
169
_presentation_data = {}
170

    
171

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

    
181

    
182
class Resource(models.Model):
183
    name = models.CharField(_('Name'), max_length=255, unique=True)
184
    desc = models.TextField(_('Description'), null=True)
185
    service = models.ForeignKey(Service)
186
    unit = models.CharField(_('Unit'), null=True, max_length=255)
187
    uplimit = intDecimalField(default=0)
188
    allow_in_projects = models.BooleanField(default=True)
189

    
190
    objects = ForUpdateManager()
191

    
192
    def __str__(self):
193
        return self.name
194

    
195
    def full_name(self):
196
        return str(self)
197

    
198
    def get_info(self):
199
        return {'service': str(self.service),
200
                'description': self.desc,
201
                'unit': self.unit,
202
                'allow_in_projects': self.allow_in_projects,
203
                }
204

    
205
    @property
206
    def group(self):
207
        default = self.name
208
        return get_presentation(str(self)).get('group', default)
209

    
210
    @property
211
    def help_text(self):
212
        default = "%s resource" % self.name
213
        return get_presentation(str(self)).get('help_text', default)
214

    
215
    @property
216
    def help_text_input_each(self):
217
        default = "%s resource" % self.name
218
        return get_presentation(str(self)).get('help_text_input_each', default)
219

    
220
    @property
221
    def is_abbreviation(self):
222
        return get_presentation(str(self)).get('is_abbreviation', False)
223

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

    
229
    @property
230
    def placeholder(self):
231
        return get_presentation(str(self)).get('placeholder', self.unit)
232

    
233
    @property
234
    def verbose_name(self):
235
        return get_presentation(str(self)).get('verbose_name', self.name)
236

    
237
    @property
238
    def display_name(self):
239
        name = self.verbose_name
240
        if self.is_abbreviation:
241
            name = name.upper()
242
        return name
243

    
244
    @property
245
    def pluralized_display_name(self):
246
        if not self.unit:
247
            return '%ss' % self.display_name
248
        return self.display_name
249

    
250
def get_resource_names():
251
    _RESOURCE_NAMES = []
252
    resources = Resource.objects.select_related('service').all()
253
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
254
    return _RESOURCE_NAMES
255

    
256

    
257
class AstakosUserManager(UserManager):
258

    
259
    def get_auth_provider_user(self, provider, **kwargs):
260
        """
261
        Retrieve AstakosUser instance associated with the specified third party
262
        id.
263
        """
264
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
265
                          kwargs.iteritems()))
266
        return self.get(auth_providers__module=provider, **kwargs)
267

    
268
    def get_by_email(self, email):
269
        return self.get(email=email)
270

    
271
    def get_by_identifier(self, email_or_username, **kwargs):
272
        try:
273
            return self.get(email__iexact=email_or_username, **kwargs)
274
        except AstakosUser.DoesNotExist:
275
            return self.get(username__iexact=email_or_username, **kwargs)
276

    
277
    def user_exists(self, email_or_username, **kwargs):
278
        qemail = Q(email__iexact=email_or_username)
279
        qusername = Q(username__iexact=email_or_username)
280
        qextra = Q(**kwargs)
281
        return self.filter((qemail | qusername) & qextra).exists()
282

    
283
    def verified_user_exists(self, email_or_username):
284
        return self.user_exists(email_or_username, email_verified=True)
285

    
286
    def verified(self):
287
        return self.filter(email_verified=True)
288

    
289
    def uuid_catalog(self, l=None):
290
        """
291
        Returns a uuid to username mapping for the uuids appearing in l.
292
        If l is None returns the mapping for all existing users.
293
        """
294
        q = self.filter(uuid__in=l) if l != None else self
295
        return dict(q.values_list('uuid', 'username'))
296

    
297
    def displayname_catalog(self, l=None):
298
        """
299
        Returns a username to uuid mapping for the usernames appearing in l.
300
        If l is None returns the mapping for all existing users.
301
        """
302
        if l is not None:
303
            lmap = dict((x.lower(), x) for x in l)
304
            q = self.filter(username__in=lmap.keys())
305
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
306
        else:
307
            q = self
308
            values = self.values_list('username', 'uuid')
309
        return dict(values)
310

    
311

    
312

    
313
class AstakosUser(User):
314
    """
315
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
316
    """
317
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
318
                                   null=True)
319

    
320
    #for invitations
321
    user_level = astakos_settings.DEFAULT_USER_LEVEL
322
    level = models.IntegerField(_('Inviter level'), default=user_level)
323
    invitations = models.IntegerField(
324
        _('Invitations left'), default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
325

    
326
    auth_token = models.CharField(_('Authentication Token'),
327
                                  max_length=32,
328
                                  null=True,
329
                                  blank=True,
330
                                  help_text = _('Renew your authentication '
331
                                                'token. Make sure to set the new '
332
                                                'token in any client you may be '
333
                                                'using, to preserve its '
334
                                                'functionality.'))
335
    auth_token_created = models.DateTimeField(_('Token creation date'),
336
                                              null=True)
337
    auth_token_expires = models.DateTimeField(
338
        _('Token expiration date'), null=True)
339

    
340
    updated = models.DateTimeField(_('Update date'))
341

    
342
    # Arbitrary text to identify the reason user got deactivated.
343
    # To be used as a reference from administrators.
344
    deactivated_reason = models.TextField(
345
        _('Reason the user was disabled for'),
346
        default=None, null=True)
347
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
348
                                          blank=True)
349

    
350
    has_credits = models.BooleanField(_('Has credits?'), default=False)
351

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

    
355
    # user email is verified
356
    email_verified = models.BooleanField(_('Email verified?'), default=False)
357

    
358
    # unique string used in user email verification url
359
    verification_code = models.CharField(max_length=255, null=True,
360
                                         blank=False, unique=True)
361

    
362
    # date user email verified
363
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
364
                                       blank=True)
365

    
366
    # email verification notice was sent to the user at this time
367
    activation_sent = models.DateTimeField(_('Activation sent date'),
368
                                           null=True, blank=True)
369

    
370
    # user got rejected during moderation process
371
    is_rejected = models.BooleanField(_('Account rejected'),
372
                                      default=False)
373
    # reason user got rejected
374
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
375
                                       blank=True)
376
    # moderation status
377
    moderated = models.BooleanField(_('User moderated'), default=False)
378
    # date user moderated (either accepted or rejected)
379
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
380
                                        blank=True, null=True)
381
    # a snapshot of user instance the time got moderated
382
    moderated_data = models.TextField(null=True, default=None, blank=True)
383
    # a string which identifies how the user got moderated
384
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
385
                                       default=None, null=True, blank=True)
386
    # the email used to accept the user
387
    accepted_email = models.EmailField(null=True, default=None, blank=True)
388

    
389
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
390
                                           default=False)
391
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
392
                                             null=True, blank=True)
393
    # permanent unique user identifier
394
    uuid = models.CharField(max_length=255, null=True, blank=False,
395
                            unique=True)
396

    
397
    policy = models.ManyToManyField(
398
        Resource, null=True, through='AstakosUserQuota')
399

    
400
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
401
                                           default=False, db_index=True)
402

    
403
    objects = AstakosUserManager()
404
    forupdate = ForUpdateManager()
405

    
406
    def __init__(self, *args, **kwargs):
407
        super(AstakosUser, self).__init__(*args, **kwargs)
408
        if not self.id:
409
            self.is_active = False
410

    
411
    @property
412
    def realname(self):
413
        return '%s %s' % (self.first_name, self.last_name)
414

    
415
    @property
416
    def log_display(self):
417
        """
418
        Should be used in all logger.* calls that refer to a user so that
419
        user display is consistent across log entries.
420
        """
421
        return '%s::%s' % (self.uuid, self.email)
422

    
423
    @realname.setter
424
    def realname(self, value):
425
        parts = value.split(' ')
426
        if len(parts) == 2:
427
            self.first_name = parts[0]
428
            self.last_name = parts[1]
429
        else:
430
            self.last_name = parts[0]
431

    
432
    def add_permission(self, pname):
433
        if self.has_perm(pname):
434
            return
435
        p, created = Permission.objects.get_or_create(
436
                                    codename=pname,
437
                                    name=pname.capitalize(),
438
                                    content_type=get_content_type())
439
        self.user_permissions.add(p)
440

    
441
    def remove_permission(self, pname):
442
        if self.has_perm(pname):
443
            return
444
        p = Permission.objects.get(codename=pname,
445
                                   content_type=get_content_type())
446
        self.user_permissions.remove(p)
447

    
448
    def add_group(self, gname):
449
        group, _ = Group.objects.get_or_create(name=gname)
450
        self.groups.add(group)
451

    
452
    def is_project_admin(self, application_id=None):
453
        return self.uuid in astakos_settings.PROJECT_ADMINS
454

    
455
    @property
456
    def invitation(self):
457
        try:
458
            return Invitation.objects.get(username=self.email)
459
        except Invitation.DoesNotExist:
460
            return None
461

    
462
    @property
463
    def policies(self):
464
        return self.astakosuserquota_set.select_related().all()
465

    
466
    def get_resource_policy(self, resource):
467
        resource = Resource.objects.get(name=resource)
468
        default_capacity = resource.uplimit
469
        try:
470
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
471
            return policy, default_capacity
472
        except AstakosUserQuota.DoesNotExist:
473
            return None, default_capacity
474

    
475
    def update_uuid(self):
476
        while not self.uuid:
477
            uuid_val = str(uuid.uuid4())
478
            try:
479
                AstakosUser.objects.get(uuid=uuid_val)
480
            except AstakosUser.DoesNotExist, e:
481
                self.uuid = uuid_val
482
        return self.uuid
483

    
484
    def save(self, update_timestamps=True, **kwargs):
485
        if update_timestamps:
486
            if not self.id:
487
                self.date_joined = datetime.now()
488
            self.updated = datetime.now()
489

    
490
        self.update_uuid()
491
        # username currently matches email
492
        if self.username != self.email.lower():
493
            self.username = self.email.lower()
494

    
495
        super(AstakosUser, self).save(**kwargs)
496

    
497
    def renew_verification_code(self):
498
        self.verification_code = str(uuid.uuid4())
499
        logger.info("Verification code renewed for %s" % self.log_display)
500

    
501
    def renew_token(self, flush_sessions=False, current_key=None):
502
        md5 = hashlib.md5()
503
        md5.update(settings.SECRET_KEY)
504
        md5.update(self.username)
505
        md5.update(self.realname.encode('ascii', 'ignore'))
506
        md5.update(asctime())
507

    
508
        self.auth_token = b64encode(md5.digest())
509
        self.auth_token_created = datetime.now()
510
        self.auth_token_expires = self.auth_token_created + \
511
                                  timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
512
        if flush_sessions:
513
            self.flush_sessions(current_key)
514
        msg = 'Token renewed for %s' % self.log_display
515
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
516

    
517
    def flush_sessions(self, current_key=None):
518
        q = self.sessions
519
        if current_key:
520
            q = q.exclude(session_key=current_key)
521

    
522
        keys = q.values_list('session_key', flat=True)
523
        if keys:
524
            msg = 'Flushing sessions: %s' % ','.join(keys)
525
            logger.log(astakos_settings.LOGGING_LEVEL, msg, [])
526
        engine = import_module(settings.SESSION_ENGINE)
527
        for k in keys:
528
            s = engine.SessionStore(k)
529
            s.flush()
530

    
531
    def __unicode__(self):
532
        return '%s (%s)' % (self.realname, self.email)
533

    
534
    def conflicting_email(self):
535
        q = AstakosUser.objects.exclude(username=self.username)
536
        q = q.filter(email__iexact=self.email)
537
        if q.count() != 0:
538
            return True
539
        return False
540

    
541
    def email_change_is_pending(self):
542
        return self.emailchanges.count() > 0
543

    
544
    @property
545
    def status_display(self):
546
        msg = ""
547
        append = None
548
        if self.is_active:
549
            msg = "Accepted/Active"
550
        if self.is_rejected:
551
            msg = "Rejected"
552
            if self.rejected_reason:
553
                msg += " (%s)" % self.rejected_reason
554
        if not self.email_verified:
555
            msg = "Pending email verification"
556
        if not self.moderated:
557
            msg = "Pending moderation"
558
        if not self.is_active and self.email_verified:
559
            msg = "Accepted/Inactive"
560
            if self.deactivated_reason:
561
                msg += " (%s)" % (self.deactivated_reason)
562

    
563
        if self.moderated and not self.is_rejected:
564
            if self.accepted_policy == 'manual':
565
                msg += " (manually accepted)"
566
            else:
567
                msg += " (accepted policy: %s)" % \
568
                        self.accepted_policy
569
        return msg
570

    
571
    @property
572
    def signed_terms(self):
573
        term = get_latest_terms()
574
        if not term:
575
            return True
576
        if not self.has_signed_terms:
577
            return False
578
        if not self.date_signed_terms:
579
            return False
580
        if self.date_signed_terms < term.date:
581
            self.has_signed_terms = False
582
            self.date_signed_terms = None
583
            self.save()
584
            return False
585
        return True
586

    
587
    def set_invitations_level(self):
588
        """
589
        Update user invitation level
590
        """
591
        level = self.invitation.inviter.level + 1
592
        self.level = level
593
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
594

    
595
    def can_change_password(self):
596
        return self.has_auth_provider('local', auth_backend='astakos')
597

    
598
    def can_change_email(self):
599
        if not self.has_auth_provider('local'):
600
            return True
601

    
602
        local = self.get_auth_provider('local')._instance
603
        return local.auth_backend == 'astakos'
604

    
605
    # Auth providers related methods
606
    def get_auth_provider(self, module=None, identifier=None, **filters):
607
        if not module:
608
            return self.auth_providers.active()[0].settings
609

    
610
        params = {'module': module}
611
        if identifier:
612
            params['identifier'] = identifier
613
        params.update(filters)
614
        return self.auth_providers.active().get(**params).settings
615

    
616
    def has_auth_provider(self, provider, **kwargs):
617
        return bool(self.auth_providers.active().filter(module=provider,
618
                                                        **kwargs).count())
619

    
620
    def get_required_providers(self, **kwargs):
621
        return auth.REQUIRED_PROVIDERS.keys()
622

    
623
    def missing_required_providers(self):
624
        required = self.get_required_providers()
625
        missing = []
626
        for provider in required:
627
            if not self.has_auth_provider(provider):
628
                missing.append(auth.get_provider(provider, self))
629
        return missing
630

    
631
    def get_available_auth_providers(self, **filters):
632
        """
633
        Returns a list of providers available for add by the user.
634
        """
635
        modules = astakos_settings.IM_MODULES
636
        providers = []
637
        for p in modules:
638
            providers.append(auth.get_provider(p, self))
639
        available = []
640

    
641
        for p in providers:
642
            if p.get_add_policy:
643
                available.append(p)
644
        return available
645

    
646
    def get_disabled_auth_providers(self, **filters):
647
        providers = self.get_auth_providers(**filters)
648
        disabled = []
649
        for p in providers:
650
            if not p.get_login_policy:
651
                disabled.append(p)
652
        return disabled
653

    
654
    def get_enabled_auth_providers(self, **filters):
655
        providers = self.get_auth_providers(**filters)
656
        enabled = []
657
        for p in providers:
658
            if p.get_login_policy:
659
                enabled.append(p)
660
        return enabled
661

    
662
    def get_auth_providers(self, **filters):
663
        providers = []
664
        for provider in self.auth_providers.active(**filters):
665
            if provider.settings.module_enabled:
666
                providers.append(provider.settings)
667

    
668
        modules = astakos_settings.IM_MODULES
669

    
670
        def key(p):
671
            if not p.module in modules:
672
                return 100
673
            return modules.index(p.module)
674

    
675
        providers = sorted(providers, key=key)
676
        return providers
677

    
678
    # URL methods
679
    @property
680
    def auth_providers_display(self):
681
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
682
                         self.get_enabled_auth_providers()])
683

    
684
    def add_auth_provider(self, module='local', identifier=None, **params):
685
        provider = auth.get_provider(module, self, identifier, **params)
686
        provider.add_to_user()
687

    
688
    def get_resend_activation_url(self):
689
        return reverse('send_activation', kwargs={'user_id': self.pk})
690

    
691
    def get_activation_url(self, nxt=False):
692
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
693
                                 quote(self.verification_code))
694
        if nxt:
695
            url += "&next=%s" % quote(nxt)
696
        return url
697

    
698
    def get_password_reset_url(self, token_generator=default_token_generator):
699
        return reverse('astakos.im.views.target.local.password_reset_confirm',
700
                          kwargs={'uidb36':int_to_base36(self.id),
701
                                  'token':token_generator.make_token(self)})
702

    
703
    def get_inactive_message(self, provider_module, identifier=None):
704
        provider = self.get_auth_provider(provider_module, identifier)
705

    
706
        msg_extra = ''
707
        message = ''
708

    
709
        msg_inactive = provider.get_account_inactive_msg
710
        msg_pending = provider.get_pending_activation_msg
711
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
712
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
713
        msg_pending_mod = provider.get_pending_moderation_msg
714
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
715
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
716

    
717
        if not self.email_verified:
718
            message = msg_pending
719
            url = self.get_resend_activation_url()
720
            msg_extra = msg_pending_help + \
721
                        u' ' + \
722
                        '<a href="%s">%s?</a>' % (url, msg_resend)
723
        else:
724
            if not self.moderated:
725
                message = msg_pending_mod
726
            else:
727
                if self.is_rejected:
728
                    message = msg_rejected
729
                else:
730
                    message = msg_inactive
731

    
732
        return mark_safe(message + u' ' + msg_extra)
733

    
734
    def owns_application(self, application):
735
        return application.owner == self
736

    
737
    def owns_project(self, project):
738
        return project.application.owner == self
739

    
740
    def is_associated(self, project):
741
        try:
742
            m = ProjectMembership.objects.get(person=self, project=project)
743
            return m.state in ProjectMembership.ASSOCIATED_STATES
744
        except ProjectMembership.DoesNotExist:
745
            return False
746

    
747
    def get_membership(self, project):
748
        try:
749
            return ProjectMembership.objects.get(
750
                project=project,
751
                person=self)
752
        except ProjectMembership.DoesNotExist:
753
            return None
754

    
755
    def membership_display(self, project):
756
        m = self.get_membership(project)
757
        if m is None:
758
            return _('Not a member')
759
        else:
760
            return m.user_friendly_state_display()
761

    
762
    def non_owner_can_view(self, maybe_project):
763
        if self.is_project_admin():
764
            return True
765
        if maybe_project is None:
766
            return False
767
        project = maybe_project
768
        if self.is_associated(project):
769
            return True
770
        if project.is_deactivated():
771
            return False
772
        return True
773

    
774

    
775
class AstakosUserAuthProviderManager(models.Manager):
776

    
777
    def active(self, **filters):
778
        return self.filter(active=True, **filters)
779

    
780
    def remove_unverified_providers(self, provider, **filters):
781
        try:
782
            existing = self.filter(module=provider, user__email_verified=False,
783
                                   **filters)
784
            for p in existing:
785
                p.user.delete()
786
        except:
787
            pass
788

    
789
    def unverified(self, provider, **filters):
790
        try:
791
            return self.get(module=provider, user__email_verified=False,
792
                            **filters).settings
793
        except AstakosUserAuthProvider.DoesNotExist:
794
            return None
795

    
796
    def verified(self, provider, **filters):
797
        try:
798
            return self.get(module=provider, user__email_verified=True,
799
                            **filters).settings
800
        except AstakosUserAuthProvider.DoesNotExist:
801
            return None
802

    
803

    
804
class AuthProviderPolicyProfileManager(models.Manager):
805

    
806
    def active(self):
807
        return self.filter(active=True)
808

    
809
    def for_user(self, user, provider):
810
        policies = {}
811
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
812
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
813
        exclusive_q = exclusive_q1 | exclusive_q2
814

    
815
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
816
            policies.update(profile.policies)
817

    
818
        user_groups = user.groups.all().values('pk')
819
        for profile in self.active().filter(groups__in=user_groups).filter(
820
                exclusive_q):
821
            policies.update(profile.policies)
822
        return policies
823

    
824
    def add_policy(self, name, provider, group_or_user, exclusive=False,
825
                   **policies):
826
        is_group = isinstance(group_or_user, Group)
827
        profile, created = self.get_or_create(name=name, provider=provider,
828
                                              is_exclusive=exclusive)
829
        profile.is_exclusive = exclusive
830
        profile.save()
831
        if is_group:
832
            profile.groups.add(group_or_user)
833
        else:
834
            profile.users.add(group_or_user)
835
        profile.set_policies(policies)
836
        profile.save()
837
        return profile
838

    
839

    
840
class AuthProviderPolicyProfile(models.Model):
841
    name = models.CharField(_('Name'), max_length=255, blank=False,
842
                            null=False, db_index=True)
843
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
844
                                null=False)
845

    
846
    # apply policies to all providers excluding the one set in provider field
847
    is_exclusive = models.BooleanField(default=False)
848

    
849
    policy_add = models.NullBooleanField(null=True, default=None)
850
    policy_remove = models.NullBooleanField(null=True, default=None)
851
    policy_create = models.NullBooleanField(null=True, default=None)
852
    policy_login = models.NullBooleanField(null=True, default=None)
853
    policy_limit = models.IntegerField(null=True, default=None)
854
    policy_required = models.NullBooleanField(null=True, default=None)
855
    policy_automoderate = models.NullBooleanField(null=True, default=None)
856
    policy_switch = models.NullBooleanField(null=True, default=None)
857

    
858
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
859
                     'automoderate')
860

    
861
    priority = models.IntegerField(null=False, default=1)
862
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
863
    users = models.ManyToManyField(AstakosUser,
864
                                   related_name='authpolicy_profiles')
865
    active = models.BooleanField(default=True)
866

    
867
    objects = AuthProviderPolicyProfileManager()
868

    
869
    class Meta:
870
        ordering = ['priority']
871

    
872
    @property
873
    def policies(self):
874
        policies = {}
875
        for pkey in self.POLICY_FIELDS:
876
            value = getattr(self, 'policy_%s' % pkey, None)
877
            if value is None:
878
                continue
879
            policies[pkey] = value
880
        return policies
881

    
882
    def set_policies(self, policies_dict):
883
        for key, value in policies_dict.iteritems():
884
            if key in self.POLICY_FIELDS:
885
                setattr(self, 'policy_%s' % key, value)
886
        return self.policies
887

    
888

    
889
class AstakosUserAuthProvider(models.Model):
890
    """
891
    Available user authentication methods.
892
    """
893
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
894
                                   null=True, default=None)
895
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
896
    module = models.CharField(_('Provider'), max_length=255, blank=False,
897
                                default='local')
898
    identifier = models.CharField(_('Third-party identifier'),
899
                                              max_length=255, null=True,
900
                                              blank=True)
901
    active = models.BooleanField(default=True)
902
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
903
                                   default='astakos')
904
    info_data = models.TextField(default="", null=True, blank=True)
905
    created = models.DateTimeField('Creation date', auto_now_add=True)
906

    
907
    objects = AstakosUserAuthProviderManager()
908

    
909
    class Meta:
910
        unique_together = (('identifier', 'module', 'user'), )
911
        ordering = ('module', 'created')
912

    
913
    def __init__(self, *args, **kwargs):
914
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
915
        try:
916
            self.info = json.loads(self.info_data)
917
            if not self.info:
918
                self.info = {}
919
        except Exception, e:
920
            self.info = {}
921

    
922
        for key,value in self.info.iteritems():
923
            setattr(self, 'info_%s' % key, value)
924

    
925
    @property
926
    def settings(self):
927
        extra_data = {}
928

    
929
        info_data = {}
930
        if self.info_data:
931
            info_data = json.loads(self.info_data)
932

    
933
        extra_data['info'] = info_data
934

    
935
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
936
            extra_data[key] = getattr(self, key)
937

    
938
        extra_data['instance'] = self
939
        return auth.get_provider(self.module, self.user,
940
                                           self.identifier, **extra_data)
941

    
942
    def __repr__(self):
943
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
944

    
945
    def __unicode__(self):
946
        if self.identifier:
947
            return "%s:%s" % (self.module, self.identifier)
948
        if self.auth_backend:
949
            return "%s:%s" % (self.module, self.auth_backend)
950
        return self.module
951

    
952
    def save(self, *args, **kwargs):
953
        self.info_data = json.dumps(self.info)
954
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
955

    
956

    
957
class ExtendedManager(models.Manager):
958
    def _update_or_create(self, **kwargs):
959
        assert kwargs, \
960
            'update_or_create() must be passed at least one keyword argument'
961
        obj, created = self.get_or_create(**kwargs)
962
        defaults = kwargs.pop('defaults', {})
963
        if created:
964
            return obj, True, False
965
        else:
966
            try:
967
                params = dict(
968
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
969
                params.update(defaults)
970
                for attr, val in params.items():
971
                    if hasattr(obj, attr):
972
                        setattr(obj, attr, val)
973
                sid = transaction.savepoint()
974
                obj.save(force_update=True)
975
                transaction.savepoint_commit(sid)
976
                return obj, False, True
977
            except IntegrityError, e:
978
                transaction.savepoint_rollback(sid)
979
                try:
980
                    return self.get(**kwargs), False, False
981
                except self.model.DoesNotExist:
982
                    raise e
983

    
984
    update_or_create = _update_or_create
985

    
986

    
987
class AstakosUserQuota(models.Model):
988
    objects = ExtendedManager()
989
    capacity = intDecimalField()
990
    resource = models.ForeignKey(Resource)
991
    user = models.ForeignKey(AstakosUser)
992

    
993
    class Meta:
994
        unique_together = ("resource", "user")
995

    
996

    
997
class ApprovalTerms(models.Model):
998
    """
999
    Model for approval terms
1000
    """
1001

    
1002
    date = models.DateTimeField(
1003
        _('Issue date'), db_index=True, auto_now_add=True)
1004
    location = models.CharField(_('Terms location'), max_length=255)
1005

    
1006

    
1007
class Invitation(models.Model):
1008
    """
1009
    Model for registring invitations
1010
    """
1011
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1012
                                null=True)
1013
    realname = models.CharField(_('Real name'), max_length=255)
1014
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1015
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1016
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1017
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1018
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1019

    
1020
    def __init__(self, *args, **kwargs):
1021
        super(Invitation, self).__init__(*args, **kwargs)
1022
        if not self.id:
1023
            self.code = _generate_invitation_code()
1024

    
1025
    def consume(self):
1026
        self.is_consumed = True
1027
        self.consumed = datetime.now()
1028
        self.save()
1029

    
1030
    def __unicode__(self):
1031
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1032

    
1033

    
1034
class EmailChangeManager(models.Manager):
1035

    
1036
    @transaction.commit_on_success
1037
    def change_email(self, activation_key):
1038
        """
1039
        Validate an activation key and change the corresponding
1040
        ``User`` if valid.
1041

1042
        If the key is valid and has not expired, return the ``User``
1043
        after activating.
1044

1045
        If the key is not valid or has expired, return ``None``.
1046

1047
        If the key is valid but the ``User`` is already active,
1048
        return ``None``.
1049

1050
        After successful email change the activation record is deleted.
1051

1052
        Throws ValueError if there is already
1053
        """
1054
        try:
1055
            email_change = self.model.objects.get(
1056
                activation_key=activation_key)
1057
            if email_change.activation_key_expired():
1058
                email_change.delete()
1059
                raise EmailChange.DoesNotExist
1060
            # is there an active user with this address?
1061
            try:
1062
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1063
            except AstakosUser.DoesNotExist:
1064
                pass
1065
            else:
1066
                raise ValueError(_('The new email address is reserved.'))
1067
            # update user
1068
            user = AstakosUser.objects.get(pk=email_change.user_id)
1069
            old_email = user.email
1070
            user.email = email_change.new_email_address
1071
            user.save()
1072
            email_change.delete()
1073
            msg = "User %s changed email from %s to %s" % (user.log_display,
1074
                                                           old_email,
1075
                                                           user.email)
1076
            logger.log(astakos_settings.LOGGING_LEVEL, msg)
1077
            return user
1078
        except EmailChange.DoesNotExist:
1079
            raise ValueError(_('Invalid activation key.'))
1080

    
1081

    
1082
class EmailChange(models.Model):
1083
    new_email_address = models.EmailField(
1084
        _(u'new e-mail address'),
1085
        help_text=_('Provide a new email address. Until you verify the new '
1086
                    'address by following the activation link that will be '
1087
                    'sent to it, your old email address will remain active.'))
1088
    user = models.ForeignKey(
1089
        AstakosUser, unique=True, related_name='emailchanges')
1090
    requested_at = models.DateTimeField(auto_now_add=True)
1091
    activation_key = models.CharField(
1092
        max_length=40, unique=True, db_index=True)
1093

    
1094
    objects = EmailChangeManager()
1095

    
1096
    def get_url(self):
1097
        return reverse('email_change_confirm',
1098
                      kwargs={'activation_key': self.activation_key})
1099

    
1100
    def activation_key_expired(self):
1101
        expiration_date = timedelta(days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1102
        return self.requested_at + expiration_date < datetime.now()
1103

    
1104

    
1105
class AdditionalMail(models.Model):
1106
    """
1107
    Model for registring invitations
1108
    """
1109
    owner = models.ForeignKey(AstakosUser)
1110
    email = models.EmailField()
1111

    
1112

    
1113
def _generate_invitation_code():
1114
    while True:
1115
        code = randint(1, 2L ** 63 - 1)
1116
        try:
1117
            Invitation.objects.get(code=code)
1118
            # An invitation with this code already exists, try again
1119
        except Invitation.DoesNotExist:
1120
            return code
1121

    
1122

    
1123
def get_latest_terms():
1124
    try:
1125
        term = ApprovalTerms.objects.order_by('-id')[0]
1126
        return term
1127
    except IndexError:
1128
        pass
1129
    return None
1130

    
1131

    
1132
class PendingThirdPartyUser(models.Model):
1133
    """
1134
    Model for registring successful third party user authentications
1135
    """
1136
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1137
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1138
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1139
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1140
                                  null=True)
1141
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1142
                                 null=True)
1143
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1144
                                   null=True)
1145
    username = models.CharField(_('username'), max_length=30, unique=True,
1146
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1147
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1148
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1149
    info = models.TextField(default="", null=True, blank=True)
1150

    
1151
    class Meta:
1152
        unique_together = ("provider", "third_party_identifier")
1153

    
1154
    def get_user_instance(self):
1155
        """
1156
        Create a new AstakosUser instance based on details provided when user
1157
        initially signed up.
1158
        """
1159
        d = copy.copy(self.__dict__)
1160
        d.pop('_state', None)
1161
        d.pop('id', None)
1162
        d.pop('token', None)
1163
        d.pop('created', None)
1164
        d.pop('info', None)
1165
        d.pop('affiliation', None)
1166
        d.pop('provider', None)
1167
        d.pop('third_party_identifier', None)
1168
        user = AstakosUser(**d)
1169

    
1170
        return user
1171

    
1172
    @property
1173
    def realname(self):
1174
        return '%s %s' %(self.first_name, self.last_name)
1175

    
1176
    @realname.setter
1177
    def realname(self, value):
1178
        parts = value.split(' ')
1179
        if len(parts) == 2:
1180
            self.first_name = parts[0]
1181
            self.last_name = parts[1]
1182
        else:
1183
            self.last_name = parts[0]
1184

    
1185
    def save(self, **kwargs):
1186
        if not self.id:
1187
            # set username
1188
            while not self.username:
1189
                username =  uuid.uuid4().hex[:30]
1190
                try:
1191
                    AstakosUser.objects.get(username = username)
1192
                except AstakosUser.DoesNotExist, e:
1193
                    self.username = username
1194
        super(PendingThirdPartyUser, self).save(**kwargs)
1195

    
1196
    def generate_token(self):
1197
        self.password = self.third_party_identifier
1198
        self.last_login = datetime.now()
1199
        self.token = default_token_generator.make_token(self)
1200

    
1201
    def existing_user(self):
1202
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1203
                                         auth_providers__identifier=self.third_party_identifier)
1204

    
1205
    def get_provider(self, user):
1206
        params = {
1207
            'info_data': self.info,
1208
            'affiliation': self.affiliation
1209
        }
1210
        return auth.get_provider(self.provider, user,
1211
                                 self.third_party_identifier, **params)
1212

    
1213
class SessionCatalog(models.Model):
1214
    session_key = models.CharField(_('session key'), max_length=40)
1215
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1216

    
1217

    
1218
class UserSetting(models.Model):
1219
    user = models.ForeignKey(AstakosUser)
1220
    setting = models.CharField(max_length=255)
1221
    value = models.IntegerField()
1222

    
1223
    objects = ForUpdateManager()
1224

    
1225
    class Meta:
1226
        unique_together = ("user", "setting")
1227

    
1228

    
1229
### PROJECTS ###
1230
################
1231

    
1232
class ChainManager(ForUpdateManager):
1233

    
1234
    def search_by_name(self, *search_strings):
1235
        projects = Project.objects.search_by_name(*search_strings)
1236
        chains = [p.id for p in projects]
1237
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1238
        apps = (app for app in apps if app.is_latest())
1239
        app_chains = [app.chain for app in apps if app.chain not in chains]
1240
        return chains + app_chains
1241

    
1242
    def all_full_state(self):
1243
        chains = self.all()
1244
        cids = [c.chain for c in chains]
1245
        projects = Project.objects.select_related('application').in_bulk(cids)
1246

    
1247
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1248
        chain_latest = dict(objs.values_list('chain', 'latest'))
1249

    
1250
        objs = ProjectApplication.objects.select_related('applicant')
1251
        apps = objs.in_bulk(chain_latest.values())
1252

    
1253
        d = {}
1254
        for chain in chains:
1255
            pk = chain.pk
1256
            project = projects.get(pk, None)
1257
            app = apps[chain_latest[pk]]
1258
            d[chain.pk] = chain.get_state(project, app)
1259

    
1260
        return d
1261

    
1262
    def of_project(self, project):
1263
        if project is None:
1264
            return None
1265
        try:
1266
            return self.get(chain=project.id)
1267
        except Chain.DoesNotExist:
1268
            raise AssertionError('project with no chain')
1269

    
1270

    
1271
class Chain(models.Model):
1272
    chain  =   models.AutoField(primary_key=True)
1273

    
1274
    def __str__(self):
1275
        return "%s" % (self.chain,)
1276

    
1277
    objects = ChainManager()
1278

    
1279
    PENDING            = 0
1280
    DENIED             = 3
1281
    DISMISSED          = 4
1282
    CANCELLED          = 5
1283

    
1284
    APPROVED           = 10
1285
    APPROVED_PENDING   = 11
1286
    SUSPENDED          = 12
1287
    SUSPENDED_PENDING  = 13
1288
    TERMINATED         = 14
1289
    TERMINATED_PENDING = 15
1290

    
1291
    PENDING_STATES = [PENDING,
1292
                      APPROVED_PENDING,
1293
                      SUSPENDED_PENDING,
1294
                      TERMINATED_PENDING,
1295
                      ]
1296

    
1297
    MODIFICATION_STATES = [APPROVED_PENDING,
1298
                           SUSPENDED_PENDING,
1299
                           TERMINATED_PENDING,
1300
                           ]
1301

    
1302
    RELEVANT_STATES = [PENDING,
1303
                       DENIED,
1304
                       APPROVED,
1305
                       APPROVED_PENDING,
1306
                       SUSPENDED,
1307
                       SUSPENDED_PENDING,
1308
                       TERMINATED_PENDING,
1309
                       ]
1310

    
1311
    SKIP_STATES = [DISMISSED,
1312
                   CANCELLED,
1313
                   TERMINATED]
1314

    
1315
    STATE_DISPLAY = {
1316
        PENDING            : _("Pending"),
1317
        DENIED             : _("Denied"),
1318
        DISMISSED          : _("Dismissed"),
1319
        CANCELLED          : _("Cancelled"),
1320
        APPROVED           : _("Active"),
1321
        APPROVED_PENDING   : _("Active - Pending"),
1322
        SUSPENDED          : _("Suspended"),
1323
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1324
        TERMINATED         : _("Terminated"),
1325
        TERMINATED_PENDING : _("Terminated - Pending"),
1326
        }
1327

    
1328

    
1329
    @classmethod
1330
    def _chain_state(cls, project_state, app_state):
1331
        s = CHAIN_STATE.get((project_state, app_state), None)
1332
        if s is None:
1333
            raise AssertionError('inconsistent chain state')
1334
        return s
1335

    
1336
    @classmethod
1337
    def chain_state(cls, project, app):
1338
        p_state = project.state if project else None
1339
        return cls._chain_state(p_state, app.state)
1340

    
1341
    @classmethod
1342
    def state_display(cls, s):
1343
        if s is None:
1344
            return _("Unknown")
1345
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1346

    
1347
    def last_application(self):
1348
        return self.chained_apps.order_by('-id')[0]
1349

    
1350
    def get_project(self):
1351
        try:
1352
            return self.chained_project
1353
        except Project.DoesNotExist:
1354
            return None
1355

    
1356
    def get_elements(self):
1357
        project = self.get_project()
1358
        app = self.last_application()
1359
        return project, app
1360

    
1361
    def get_state(self, project, app):
1362
        s = self.chain_state(project, app)
1363
        return s, project, app
1364

    
1365
    def full_state(self):
1366
        project, app = self.get_elements()
1367
        return self.get_state(project, app)
1368

    
1369

    
1370
def new_chain():
1371
    c = Chain.objects.create()
1372
    return c
1373

    
1374

    
1375
class ProjectApplicationManager(ForUpdateManager):
1376

    
1377
    def user_visible_projects(self, *filters, **kw_filters):
1378
        model = self.model
1379
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1380

    
1381
    def user_visible_by_chain(self, flt):
1382
        model = self.model
1383
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1384
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1385
        by_chain = dict(pending.annotate(models.Max('id')))
1386
        by_chain.update(approved.annotate(models.Max('id')))
1387
        return self.filter(flt, id__in=by_chain.values())
1388

    
1389
    def user_accessible_projects(self, user):
1390
        """
1391
        Return projects accessed by specified user.
1392
        """
1393
        if user.is_project_admin():
1394
            participates_filters = Q()
1395
        else:
1396
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1397
                                   Q(project__projectmembership__person=user)
1398

    
1399
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1400

    
1401
    def search_by_name(self, *search_strings):
1402
        q = Q()
1403
        for s in search_strings:
1404
            q = q | Q(name__icontains=s)
1405
        return self.filter(q)
1406

    
1407
    def latest_of_chain(self, chain_id):
1408
        try:
1409
            return self.filter(chain=chain_id).order_by('-id')[0]
1410
        except IndexError:
1411
            return None
1412

    
1413

    
1414
class ProjectApplication(models.Model):
1415
    applicant               =   models.ForeignKey(
1416
                                    AstakosUser,
1417
                                    related_name='projects_applied',
1418
                                    db_index=True)
1419

    
1420
    PENDING     =    0
1421
    APPROVED    =    1
1422
    REPLACED    =    2
1423
    DENIED      =    3
1424
    DISMISSED   =    4
1425
    CANCELLED   =    5
1426

    
1427
    state                   =   models.IntegerField(default=PENDING,
1428
                                                    db_index=True)
1429

    
1430
    owner                   =   models.ForeignKey(
1431
                                    AstakosUser,
1432
                                    related_name='projects_owned',
1433
                                    db_index=True)
1434

    
1435
    chain                   =   models.ForeignKey(Chain,
1436
                                                  related_name='chained_apps',
1437
                                                  db_column='chain')
1438
    precursor_application   =   models.ForeignKey('ProjectApplication',
1439
                                                  null=True,
1440
                                                  blank=True)
1441

    
1442
    name                    =   models.CharField(max_length=80)
1443
    homepage                =   models.URLField(max_length=255, null=True,
1444
                                                verify_exists=False)
1445
    description             =   models.TextField(null=True, blank=True)
1446
    start_date              =   models.DateTimeField(null=True, blank=True)
1447
    end_date                =   models.DateTimeField()
1448
    member_join_policy      =   models.IntegerField()
1449
    member_leave_policy     =   models.IntegerField()
1450
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1451
    resource_grants         =   models.ManyToManyField(
1452
                                    Resource,
1453
                                    null=True,
1454
                                    blank=True,
1455
                                    through='ProjectResourceGrant')
1456
    comments                =   models.TextField(null=True, blank=True)
1457
    issue_date              =   models.DateTimeField(auto_now_add=True)
1458
    response_date           =   models.DateTimeField(null=True, blank=True)
1459
    response                =   models.TextField(null=True, blank=True)
1460

    
1461
    objects                 =   ProjectApplicationManager()
1462

    
1463
    # Compiled queries
1464
    Q_PENDING  = Q(state=PENDING)
1465
    Q_APPROVED = Q(state=APPROVED)
1466
    Q_DENIED   = Q(state=DENIED)
1467

    
1468
    class Meta:
1469
        unique_together = ("chain", "id")
1470

    
1471
    def __unicode__(self):
1472
        return "%s applied by %s" % (self.name, self.applicant)
1473

    
1474
    # TODO: Move to a more suitable place
1475
    APPLICATION_STATE_DISPLAY = {
1476
        PENDING  : _('Pending review'),
1477
        APPROVED : _('Approved'),
1478
        REPLACED : _('Replaced'),
1479
        DENIED   : _('Denied'),
1480
        DISMISSED: _('Dismissed'),
1481
        CANCELLED: _('Cancelled')
1482
    }
1483

    
1484
    @property
1485
    def log_display(self):
1486
        return "application %s (%s) for project %s" % (
1487
            self.id, self.name, self.chain)
1488

    
1489
    def state_display(self):
1490
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1491

    
1492
    def project_state_display(self):
1493
        try:
1494
            project = self.project
1495
            return project.state_display()
1496
        except Project.DoesNotExist:
1497
            return self.state_display()
1498

    
1499
    def add_resource_policy(self, resource, uplimit):
1500
        """Raises ObjectDoesNotExist, IntegrityError"""
1501
        q = self.projectresourcegrant_set
1502
        resource = Resource.objects.get(name=resource)
1503
        q.create(resource=resource, member_capacity=uplimit)
1504

    
1505
    def members_count(self):
1506
        return self.project.approved_memberships.count()
1507

    
1508
    @property
1509
    def grants(self):
1510
        return self.projectresourcegrant_set.values('member_capacity',
1511
                                                    'resource__name')
1512

    
1513
    @property
1514
    def resource_policies(self):
1515
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1516

    
1517
    def set_resource_policies(self, policies):
1518
        for resource, uplimit in policies:
1519
            self.add_resource_policy(resource, uplimit)
1520

    
1521
    def pending_modifications_incl_me(self):
1522
        q = self.chained_applications()
1523
        q = q.filter(Q(state=self.PENDING))
1524
        return q
1525

    
1526
    def last_pending_incl_me(self):
1527
        try:
1528
            return self.pending_modifications_incl_me().order_by('-id')[0]
1529
        except IndexError:
1530
            return None
1531

    
1532
    def pending_modifications(self):
1533
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1534

    
1535
    def last_pending(self):
1536
        try:
1537
            return self.pending_modifications().order_by('-id')[0]
1538
        except IndexError:
1539
            return None
1540

    
1541
    def is_modification(self):
1542
        # if self.state != self.PENDING:
1543
        #     return False
1544
        parents = self.chained_applications().filter(id__lt=self.id)
1545
        parents = parents.filter(state__in=[self.APPROVED])
1546
        return parents.count() > 0
1547

    
1548
    def chained_applications(self):
1549
        return ProjectApplication.objects.filter(chain=self.chain)
1550

    
1551
    def is_latest(self):
1552
        return self.chained_applications().order_by('-id')[0] == self
1553

    
1554
    def has_pending_modifications(self):
1555
        return bool(self.last_pending())
1556

    
1557
    def denied_modifications(self):
1558
        q = self.chained_applications()
1559
        q = q.filter(Q(state=self.DENIED))
1560
        q = q.filter(~Q(id=self.id))
1561
        return q
1562

    
1563
    def last_denied(self):
1564
        try:
1565
            return self.denied_modifications().order_by('-id')[0]
1566
        except IndexError:
1567
            return None
1568

    
1569
    def has_denied_modifications(self):
1570
        return bool(self.last_denied())
1571

    
1572
    def is_applied(self):
1573
        try:
1574
            self.project
1575
            return True
1576
        except Project.DoesNotExist:
1577
            return False
1578

    
1579
    def get_project(self):
1580
        try:
1581
            return Project.objects.get(id=self.chain)
1582
        except Project.DoesNotExist:
1583
            return None
1584

    
1585
    def project_exists(self):
1586
        return self.get_project() is not None
1587

    
1588
    def can_cancel(self):
1589
        return self.state == self.PENDING
1590

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

    
1597
        self.state = self.CANCELLED
1598
        self.save()
1599

    
1600
    def can_dismiss(self):
1601
        return self.state == self.DENIED
1602

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

    
1609
        self.state = self.DISMISSED
1610
        self.save()
1611

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

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

    
1621
        self.state = self.DENIED
1622
        self.response_date = datetime.now()
1623
        self.response = reason
1624
        self.save()
1625

    
1626
    def can_approve(self):
1627
        return self.state == self.PENDING
1628

    
1629
    def approve(self, reason):
1630
        if not self.can_approve():
1631
            m = _("cannot approve: project '%s' in state '%s'") % (
1632
                    self.name, self.state)
1633
            raise AssertionError(m) # invalid argument
1634

    
1635
        now = datetime.now()
1636
        self.state = self.APPROVED
1637
        self.response_date = now
1638
        self.response = reason
1639
        self.save()
1640

    
1641
        project = self.get_project()
1642
        if project is None:
1643
            project = Project(id=self.chain)
1644

    
1645
        project.name = self.name
1646
        project.application = self
1647
        project.last_approval_date = now
1648
        project.save()
1649
        return project
1650

    
1651
    @property
1652
    def member_join_policy_display(self):
1653
        policy = self.member_join_policy
1654
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1655

    
1656
    @property
1657
    def member_leave_policy_display(self):
1658
        policy = self.member_leave_policy
1659
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1660

    
1661
class ProjectResourceGrant(models.Model):
1662

    
1663
    resource                =   models.ForeignKey(Resource)
1664
    project_application     =   models.ForeignKey(ProjectApplication,
1665
                                                  null=True)
1666
    project_capacity        =   intDecimalField(null=True)
1667
    member_capacity         =   intDecimalField(default=0)
1668

    
1669
    objects = ExtendedManager()
1670

    
1671
    class Meta:
1672
        unique_together = ("resource", "project_application")
1673

    
1674
    def display_member_capacity(self):
1675
        if self.member_capacity:
1676
            if self.resource.unit:
1677
                return ProjectResourceGrant.display_filesize(
1678
                    self.member_capacity)
1679
            else:
1680
                if math.isinf(self.member_capacity):
1681
                    return 'Unlimited'
1682
                else:
1683
                    return self.member_capacity
1684
        else:
1685
            return 'Unlimited'
1686

    
1687
    def __str__(self):
1688
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1689
                                        self.display_member_capacity())
1690

    
1691
    @classmethod
1692
    def display_filesize(cls, value):
1693
        try:
1694
            value = float(value)
1695
        except:
1696
            return
1697
        else:
1698
            if math.isinf(value):
1699
                return 'Unlimited'
1700
            if value > 1:
1701
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1702
                                [0, 0, 0, 0, 0, 0])
1703
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1704
                quotient = float(value) / 1024**exponent
1705
                unit, value_decimals = unit_list[exponent]
1706
                format_string = '{0:.%sf} {1}' % (value_decimals)
1707
                return format_string.format(quotient, unit)
1708
            if value == 0:
1709
                return '0 bytes'
1710
            if value == 1:
1711
                return '1 byte'
1712
            else:
1713
               return '0'
1714

    
1715

    
1716
class ProjectManager(ForUpdateManager):
1717

    
1718
    def terminated_projects(self):
1719
        q = self.model.Q_TERMINATED
1720
        return self.filter(q)
1721

    
1722
    def not_terminated_projects(self):
1723
        q = ~self.model.Q_TERMINATED
1724
        return self.filter(q)
1725

    
1726
    def deactivated_projects(self):
1727
        q = self.model.Q_DEACTIVATED
1728
        return self.filter(q)
1729

    
1730
    def expired_projects(self):
1731
        q = (~Q(state=Project.TERMINATED) &
1732
              Q(application__end_date__lt=datetime.now()))
1733
        return self.filter(q)
1734

    
1735
    def search_by_name(self, *search_strings):
1736
        q = Q()
1737
        for s in search_strings:
1738
            q = q | Q(name__icontains=s)
1739
        return self.filter(q)
1740

    
1741

    
1742
class Project(models.Model):
1743

    
1744
    id                          =   models.OneToOneField(Chain,
1745
                                                      related_name='chained_project',
1746
                                                      db_column='id',
1747
                                                      primary_key=True)
1748

    
1749
    application                 =   models.OneToOneField(
1750
                                            ProjectApplication,
1751
                                            related_name='project')
1752
    last_approval_date          =   models.DateTimeField(null=True)
1753

    
1754
    members                     =   models.ManyToManyField(
1755
                                            AstakosUser,
1756
                                            through='ProjectMembership')
1757

    
1758
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1759
    deactivation_date           =   models.DateTimeField(null=True)
1760

    
1761
    creation_date               =   models.DateTimeField(auto_now_add=True)
1762
    name                        =   models.CharField(
1763
                                            max_length=80,
1764
                                            null=True,
1765
                                            db_index=True,
1766
                                            unique=True)
1767

    
1768
    APPROVED    = 1
1769
    SUSPENDED   = 10
1770
    TERMINATED  = 100
1771

    
1772
    state                       =   models.IntegerField(default=APPROVED,
1773
                                                        db_index=True)
1774

    
1775
    objects     =   ProjectManager()
1776

    
1777
    # Compiled queries
1778
    Q_TERMINATED  = Q(state=TERMINATED)
1779
    Q_SUSPENDED   = Q(state=SUSPENDED)
1780
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1781

    
1782
    def __str__(self):
1783
        return uenc(_("<project %s '%s'>") %
1784
                    (self.id, udec(self.application.name)))
1785

    
1786
    __repr__ = __str__
1787

    
1788
    def __unicode__(self):
1789
        return _("<project %s '%s'>") % (self.id, self.application.name)
1790

    
1791
    STATE_DISPLAY = {
1792
        APPROVED   : 'Active',
1793
        SUSPENDED  : 'Suspended',
1794
        TERMINATED : 'Terminated'
1795
        }
1796

    
1797
    def state_display(self):
1798
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1799

    
1800
    def expiration_info(self):
1801
        return (str(self.id), self.name, self.state_display(),
1802
                str(self.application.end_date))
1803

    
1804
    def is_deactivated(self, reason=None):
1805
        if reason is not None:
1806
            return self.state == reason
1807

    
1808
        return self.state != self.APPROVED
1809

    
1810
    ### Deactivation calls
1811

    
1812
    def terminate(self):
1813
        self.deactivation_reason = 'TERMINATED'
1814
        self.deactivation_date = datetime.now()
1815
        self.state = self.TERMINATED
1816
        self.name = None
1817
        self.save()
1818

    
1819
    def suspend(self):
1820
        self.deactivation_reason = 'SUSPENDED'
1821
        self.deactivation_date = datetime.now()
1822
        self.state = self.SUSPENDED
1823
        self.save()
1824

    
1825
    def resume(self):
1826
        self.deactivation_reason = None
1827
        self.deactivation_date = None
1828
        self.state = self.APPROVED
1829
        self.save()
1830

    
1831
    ### Logical checks
1832

    
1833
    def is_inconsistent(self):
1834
        now = datetime.now()
1835
        dates = [self.creation_date,
1836
                 self.last_approval_date,
1837
                 self.deactivation_date]
1838
        return any([date > now for date in dates])
1839

    
1840
    def is_approved(self):
1841
        return self.state == self.APPROVED
1842

    
1843
    @property
1844
    def is_alive(self):
1845
        return not self.is_terminated
1846

    
1847
    @property
1848
    def is_terminated(self):
1849
        return self.is_deactivated(self.TERMINATED)
1850

    
1851
    @property
1852
    def is_suspended(self):
1853
        return self.is_deactivated(self.SUSPENDED)
1854

    
1855
    def violates_resource_grants(self):
1856
        return False
1857

    
1858
    def violates_members_limit(self, adding=0):
1859
        application = self.application
1860
        limit = application.limit_on_members_number
1861
        if limit is None:
1862
            return False
1863
        return (len(self.approved_members) + adding > limit)
1864

    
1865

    
1866
    ### Other
1867

    
1868
    def count_pending_memberships(self):
1869
        memb_set = self.projectmembership_set
1870
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1871
        return memb_count
1872

    
1873
    def members_count(self):
1874
        return self.approved_memberships.count()
1875

    
1876
    @property
1877
    def approved_memberships(self):
1878
        query = ProjectMembership.Q_ACCEPTED_STATES
1879
        return self.projectmembership_set.filter(query)
1880

    
1881
    @property
1882
    def approved_members(self):
1883
        return [m.person for m in self.approved_memberships]
1884

    
1885

    
1886
CHAIN_STATE = {
1887
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1888
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1889
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1890
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1891
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1892

    
1893
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1894
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1895
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1896
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1897
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1898

    
1899
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1900
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1901
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1902
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1903
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1904

    
1905
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1906
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1907
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1908
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1909
    }
1910

    
1911

    
1912
class ProjectMembershipManager(ForUpdateManager):
1913

    
1914
    def any_accepted(self):
1915
        q = self.model.Q_ACTUALLY_ACCEPTED
1916
        return self.filter(q)
1917

    
1918
    def actually_accepted(self):
1919
        q = self.model.Q_ACTUALLY_ACCEPTED
1920
        return self.filter(q)
1921

    
1922
    def requested(self):
1923
        return self.filter(state=ProjectMembership.REQUESTED)
1924

    
1925
    def suspended(self):
1926
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1927

    
1928
class ProjectMembership(models.Model):
1929

    
1930
    person              =   models.ForeignKey(AstakosUser)
1931
    request_date        =   models.DateTimeField(auto_now_add=True)
1932
    project             =   models.ForeignKey(Project)
1933

    
1934
    REQUESTED           =   0
1935
    ACCEPTED            =   1
1936
    LEAVE_REQUESTED     =   5
1937
    # User deactivation
1938
    USER_SUSPENDED      =   10
1939

    
1940
    REMOVED             =   200
1941

    
1942
    ASSOCIATED_STATES   =   set([REQUESTED,
1943
                                 ACCEPTED,
1944
                                 LEAVE_REQUESTED,
1945
                                 USER_SUSPENDED,
1946
                                 ])
1947

    
1948
    ACCEPTED_STATES     =   set([ACCEPTED,
1949
                                 LEAVE_REQUESTED,
1950
                                 USER_SUSPENDED,
1951
                                 ])
1952

    
1953
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1954

    
1955
    state               =   models.IntegerField(default=REQUESTED,
1956
                                                db_index=True)
1957
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
1958
    leave_request_date  =   models.DateTimeField(null=True)
1959

    
1960
    objects     =   ProjectMembershipManager()
1961

    
1962
    # Compiled queries
1963
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1964
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1965

    
1966
    MEMBERSHIP_STATE_DISPLAY = {
1967
        REQUESTED           : _('Requested'),
1968
        ACCEPTED            : _('Accepted'),
1969
        LEAVE_REQUESTED     : _('Leave Requested'),
1970
        USER_SUSPENDED      : _('Suspended'),
1971
        REMOVED             : _('Pending removal'),
1972
        }
1973

    
1974
    USER_FRIENDLY_STATE_DISPLAY = {
1975
        REQUESTED           : _('Join requested'),
1976
        ACCEPTED            : _('Accepted member'),
1977
        LEAVE_REQUESTED     : _('Requested to leave'),
1978
        USER_SUSPENDED      : _('Suspended member'),
1979
        REMOVED             : _('Pending removal'),
1980
        }
1981

    
1982
    def state_display(self):
1983
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1984

    
1985
    def user_friendly_state_display(self):
1986
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1987

    
1988
    class Meta:
1989
        unique_together = ("person", "project")
1990
        #index_together = [["project", "state"]]
1991

    
1992
    def __str__(self):
1993
        return uenc(_("<'%s' membership in '%s'>") % (
1994
                self.person.username, self.project))
1995

    
1996
    __repr__ = __str__
1997

    
1998
    def __init__(self, *args, **kwargs):
1999
        self.state = self.REQUESTED
2000
        super(ProjectMembership, self).__init__(*args, **kwargs)
2001

    
2002
    def _set_history_item(self, reason, date=None):
2003
        if isinstance(reason, basestring):
2004
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2005

    
2006
        history_item = ProjectMembershipHistory(
2007
                            serial=self.id,
2008
                            person=self.person_id,
2009
                            project=self.project_id,
2010
                            date=date or datetime.now(),
2011
                            reason=reason)
2012
        history_item.save()
2013
        serial = history_item.id
2014

    
2015
    def can_accept(self):
2016
        return self.state == self.REQUESTED
2017

    
2018
    def accept(self):
2019
        if not self.can_accept():
2020
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
2021
            raise AssertionError(m)
2022

    
2023
        now = datetime.now()
2024
        self.acceptance_date = now
2025
        self._set_history_item(reason='ACCEPT', date=now)
2026
        self.state = self.ACCEPTED
2027
        self.save()
2028

    
2029
    def can_leave(self):
2030
        return self.state in self.ACCEPTED_STATES
2031

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

    
2038
        self.leave_request_date = datetime.now()
2039
        self.state = self.LEAVE_REQUESTED
2040
        self.save()
2041

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

    
2045
    def leave_request_deny(self):
2046
        if not self.can_deny_leave():
2047
            m = _("%s: attempt to deny 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_cancel_leave(self):
2056
        return self.state == self.LEAVE_REQUESTED
2057

    
2058
    def leave_request_cancel(self):
2059
        if not self.can_cancel_leave():
2060
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2061
                self, self.state)
2062
            raise AssertionError(m)
2063

    
2064
        self.leave_request_date = None
2065
        self.state = self.ACCEPTED
2066
        self.save()
2067

    
2068
    def can_remove(self):
2069
        return self.state in self.ACCEPTED_STATES
2070

    
2071
    def remove(self):
2072
        if not self.can_remove():
2073
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2074
            raise AssertionError(m)
2075

    
2076
        self._set_history_item(reason='REMOVE')
2077
        self.delete()
2078

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

    
2082
    def reject(self):
2083
        if not self.can_reject():
2084
            m = _("%s: attempt to reject 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='REJECT')
2090
        self.delete()
2091

    
2092
    def can_cancel(self):
2093
        return self.state == self.REQUESTED
2094

    
2095
    def cancel(self):
2096
        if not self.can_cancel():
2097
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2098
            raise AssertionError(m)
2099

    
2100
        # rejected requests don't need sync,
2101
        # because they were never effected
2102
        self._set_history_item(reason='CANCEL')
2103
        self.delete()
2104

    
2105

    
2106
class Serial(models.Model):
2107
    serial  =   models.AutoField(primary_key=True)
2108

    
2109

    
2110
class ProjectMembershipHistory(models.Model):
2111
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2112
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2113

    
2114
    person  =   models.BigIntegerField()
2115
    project =   models.BigIntegerField()
2116
    date    =   models.DateTimeField(auto_now_add=True)
2117
    reason  =   models.IntegerField()
2118
    serial  =   models.BigIntegerField()
2119

    
2120
### SIGNALS ###
2121
################
2122

    
2123
def create_astakos_user(u):
2124
    try:
2125
        AstakosUser.objects.get(user_ptr=u.pk)
2126
    except AstakosUser.DoesNotExist:
2127
        extended_user = AstakosUser(user_ptr_id=u.pk)
2128
        extended_user.__dict__.update(u.__dict__)
2129
        extended_user.save()
2130
        if not extended_user.has_auth_provider('local'):
2131
            extended_user.add_auth_provider('local')
2132
    except BaseException, e:
2133
        logger.exception(e)
2134

    
2135
def fix_superusers():
2136
    # Associate superusers with AstakosUser
2137
    admins = User.objects.filter(is_superuser=True)
2138
    for u in admins:
2139
        create_astakos_user(u)
2140

    
2141
def user_post_save(sender, instance, created, **kwargs):
2142
    if not created:
2143
        return
2144
    create_astakos_user(instance)
2145
post_save.connect(user_post_save, sender=User)
2146

    
2147
def astakosuser_post_save(sender, instance, created, **kwargs):
2148
    pass
2149

    
2150
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2151

    
2152
def resource_post_save(sender, instance, created, **kwargs):
2153
    pass
2154

    
2155
post_save.connect(resource_post_save, sender=Resource)
2156

    
2157
def renew_token(sender, instance, **kwargs):
2158
    if not instance.auth_token:
2159
        instance.renew_token()
2160
pre_save.connect(renew_token, sender=AstakosUser)
2161
pre_save.connect(renew_token, sender=Service)