Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 67cf14bf

History | View | Annotate | Download (73.1 kB)

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

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

    
41
from 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.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, \
64
    AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
65
from astakos.im import settings as astakos_settings
66
from astakos.im import auth_providers as auth
67

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

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

    
76
logger = logging.getLogger(__name__)
77

    
78
DEFAULT_CONTENT_TYPE = None
79
_content_type = None
80

    
81

    
82
def get_content_type():
83
    global _content_type
84
    if _content_type is not None:
85
        return _content_type
86

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

    
95
inf = float('inf')
96

    
97

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

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

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

    
124
    def __str__(self):
125
        return self.name
126

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

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

    
144

    
145
        def service_by_order(s):
146
            return s[1].get('order')
147

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

    
151
        metadata = dict_merge(metadata,
152
                              astakos_settings.SERVICES_META)
153

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

    
162
        order_key = service_by_order
163
        if orderfor == 'dashboard':
164
            order_key = service_by_dashbaord_order
165

    
166
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
167
                                             key=order_key))
168
        return ordered_catalog
169

    
170

    
171
_presentation_data = {}
172

    
173

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

    
183

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

    
192
    objects = ForUpdateManager()
193

    
194
    def __str__(self):
195
        return self.name
196

    
197
    def full_name(self):
198
        return str(self)
199

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

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

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

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

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

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

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

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

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

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

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

    
258

    
259
class AstakosUserManager(UserManager):
260

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

    
270
    def get_by_email(self, email):
271
        return self.get(email=email)
272

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

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

    
285
    def verified_user_exists(self, email_or_username):
286
        return self.user_exists(email_or_username, email_verified=True)
287

    
288
    def verified(self):
289
        return self.filter(email_verified=True)
290

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

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

    
313

    
314

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

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

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

    
342
    updated = models.DateTimeField(_('Update date'))
343

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

    
352
    has_credits = models.BooleanField(_('Has credits?'), default=False)
353

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

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

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

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

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

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

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

    
399
    policy = models.ManyToManyField(
400
        Resource, null=True, through='AstakosUserQuota')
401

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

    
405
    objects = AstakosUserManager()
406
    forupdate = ForUpdateManager()
407

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

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

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

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

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

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

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

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

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

    
464
    @property
465
    def policies(self):
466
        return self.astakosuserquota_set.select_related().all()
467

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

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

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

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

    
497
        super(AstakosUser, self).save(**kwargs)
498

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

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

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

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

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

    
533
    def __unicode__(self):
534
        return '%s (%s)' % (self.realname, self.email)
535

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

    
543
    def email_change_is_pending(self):
544
        return self.emailchanges.count() > 0
545

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

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

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

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

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

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

    
604
        local = self.get_auth_provider('local')._instance
605
        return local.auth_backend == 'astakos'
606

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

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

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

    
622
    def get_required_providers(self, **kwargs):
623
        return auth.REQUIRED_PROVIDERS.keys()
624

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

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

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

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

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

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

    
670
        modules = astakos_settings.IM_MODULES
671

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

    
677
        providers = sorted(providers, key=key)
678
        return providers
679

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

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

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

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

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

    
705
    def get_inactive_message(self, provider_module, identifier=None):
706
        provider = self.get_auth_provider(provider_module, identifier)
707

    
708
        msg_extra = ''
709
        message = ''
710

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

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

    
734
        return mark_safe(message + u' ' + msg_extra)
735

    
736
    def owns_application(self, application):
737
        return application.owner == self
738

    
739
    def owns_project(self, project):
740
        return project.application.owner == self
741

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

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

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

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

    
776

    
777
class AstakosUserAuthProviderManager(models.Manager):
778

    
779
    def active(self, **filters):
780
        return self.filter(active=True, **filters)
781

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

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

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

    
805

    
806
class AuthProviderPolicyProfileManager(models.Manager):
807

    
808
    def active(self):
809
        return self.filter(active=True)
810

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

    
817
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
818
            policies.update(profile.policies)
819

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

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

    
841

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

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

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

    
860
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
861
                     'automoderate')
862

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

    
869
    objects = AuthProviderPolicyProfileManager()
870

    
871
    class Meta:
872
        ordering = ['priority']
873

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

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

    
890

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

    
909
    objects = AstakosUserAuthProviderManager()
910

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

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

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

    
927
    @property
928
    def settings(self):
929
        extra_data = {}
930

    
931
        info_data = {}
932
        if self.info_data:
933
            info_data = json.loads(self.info_data)
934

    
935
        extra_data['info'] = info_data
936

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

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

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

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

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

    
958

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

    
986
    update_or_create = _update_or_create
987

    
988

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

    
995
    class Meta:
996
        unique_together = ("resource", "user")
997

    
998

    
999
class ApprovalTerms(models.Model):
1000
    """
1001
    Model for approval terms
1002
    """
1003

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

    
1008

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

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

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

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

    
1035

    
1036
class EmailChangeManager(models.Manager):
1037

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

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

1047
        If the key is not valid or has expired, return ``None``.
1048

1049
        If the key is valid but the ``User`` is already active,
1050
        return ``None``.
1051

1052
        After successful email change the activation record is deleted.
1053

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

    
1083

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

    
1096
    objects = EmailChangeManager()
1097

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

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

    
1106

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

    
1114

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

    
1124

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

    
1133

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

    
1153
    class Meta:
1154
        unique_together = ("provider", "third_party_identifier")
1155

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

    
1172
        return user
1173

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

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

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

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

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

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

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

    
1219

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

    
1225
    objects = ForUpdateManager()
1226

    
1227
    class Meta:
1228
        unique_together = ("user", "setting")
1229

    
1230

    
1231
### PROJECTS ###
1232
################
1233

    
1234
class ChainManager(ForUpdateManager):
1235

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

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

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

    
1252
        objs = ProjectApplication.objects.select_related('applicant')
1253
        apps = objs.in_bulk(chain_latest.values())
1254

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

    
1262
        return d
1263

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

    
1272

    
1273
class Chain(models.Model):
1274
    chain  =   models.AutoField(primary_key=True)
1275

    
1276
    def __str__(self):
1277
        return "%s" % (self.chain,)
1278

    
1279
    objects = ChainManager()
1280

    
1281
    PENDING            = 0
1282
    DENIED             = 3
1283
    DISMISSED          = 4
1284
    CANCELLED          = 5
1285

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

    
1293
    PENDING_STATES = [PENDING,
1294
                      APPROVED_PENDING,
1295
                      SUSPENDED_PENDING,
1296
                      TERMINATED_PENDING,
1297
                      ]
1298

    
1299
    MODIFICATION_STATES = [APPROVED_PENDING,
1300
                           SUSPENDED_PENDING,
1301
                           TERMINATED_PENDING,
1302
                           ]
1303

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

    
1313
    SKIP_STATES = [DISMISSED,
1314
                   CANCELLED,
1315
                   TERMINATED]
1316

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

    
1330

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

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

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

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

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

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

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

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

    
1371

    
1372
def new_chain():
1373
    c = Chain.objects.create()
1374
    return c
1375

    
1376

    
1377
class ProjectApplicationManager(ForUpdateManager):
1378

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

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

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

    
1401
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1402

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

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

    
1415

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

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

    
1429
    state                   =   models.IntegerField(default=PENDING,
1430
                                                    db_index=True)
1431

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

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

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

    
1463
    objects                 =   ProjectApplicationManager()
1464

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

    
1470
    class Meta:
1471
        unique_together = ("chain", "id")
1472

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

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

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

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

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

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

    
1507
    def members_count(self):
1508
        return self.project.approved_memberships.count()
1509

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

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

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

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

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

    
1534
    def pending_modifications(self):
1535
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1536

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

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

    
1550
    def chained_applications(self):
1551
        return ProjectApplication.objects.filter(chain=self.chain)
1552

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

    
1556
    def has_pending_modifications(self):
1557
        return bool(self.last_pending())
1558

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

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

    
1571
    def has_denied_modifications(self):
1572
        return bool(self.last_denied())
1573

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

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

    
1587
    def project_exists(self):
1588
        return self.get_project() is not None
1589

    
1590
    def can_cancel(self):
1591
        return self.state == self.PENDING
1592

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

    
1599
        self.state = self.CANCELLED
1600
        self.save()
1601

    
1602
    def can_dismiss(self):
1603
        return self.state == self.DENIED
1604

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

    
1611
        self.state = self.DISMISSED
1612
        self.save()
1613

    
1614
    def can_deny(self):
1615
        return self.state == self.PENDING
1616

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

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

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

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

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

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

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

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

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

    
1663
class ProjectResourceGrant(models.Model):
1664

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

    
1671
    objects = ExtendedManager()
1672

    
1673
    class Meta:
1674
        unique_together = ("resource", "project_application")
1675

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

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

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

    
1717

    
1718
class ProjectManager(ForUpdateManager):
1719

    
1720
    def terminated_projects(self):
1721
        q = self.model.Q_TERMINATED
1722
        return self.filter(q)
1723

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

    
1728
    def deactivated_projects(self):
1729
        q = self.model.Q_DEACTIVATED
1730
        return self.filter(q)
1731

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

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

    
1743

    
1744
class Project(models.Model):
1745

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

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

    
1756
    members                     =   models.ManyToManyField(
1757
                                            AstakosUser,
1758
                                            through='ProjectMembership')
1759

    
1760
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1761
    deactivation_date           =   models.DateTimeField(null=True)
1762

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

    
1770
    APPROVED    = 1
1771
    SUSPENDED   = 10
1772
    TERMINATED  = 100
1773

    
1774
    state                       =   models.IntegerField(default=APPROVED,
1775
                                                        db_index=True)
1776

    
1777
    objects     =   ProjectManager()
1778

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

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

    
1788
    __repr__ = __str__
1789

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

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

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

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

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

    
1810
        return self.state != self.APPROVED
1811

    
1812
    ### Deactivation calls
1813

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

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

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

    
1833
    ### Logical checks
1834

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

    
1842
    def is_approved(self):
1843
        return self.state == self.APPROVED
1844

    
1845
    @property
1846
    def is_alive(self):
1847
        return not self.is_terminated
1848

    
1849
    @property
1850
    def is_terminated(self):
1851
        return self.is_deactivated(self.TERMINATED)
1852

    
1853
    @property
1854
    def is_suspended(self):
1855
        return self.is_deactivated(self.SUSPENDED)
1856

    
1857
    def violates_resource_grants(self):
1858
        return False
1859

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

    
1867

    
1868
    ### Other
1869

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

    
1875
    def members_count(self):
1876
        return self.approved_memberships.count()
1877

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

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

    
1887

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

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

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

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

    
1913

    
1914
class ProjectMembershipManager(ForUpdateManager):
1915

    
1916
    def any_accepted(self):
1917
        q = self.model.Q_ACTUALLY_ACCEPTED
1918
        return self.filter(q)
1919

    
1920
    def actually_accepted(self):
1921
        q = self.model.Q_ACTUALLY_ACCEPTED
1922
        return self.filter(q)
1923

    
1924
    def requested(self):
1925
        return self.filter(state=ProjectMembership.REQUESTED)
1926

    
1927
    def suspended(self):
1928
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1929

    
1930
class ProjectMembership(models.Model):
1931

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

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

    
1942
    REMOVED             =   200
1943

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

    
1950
    ACCEPTED_STATES     =   set([ACCEPTED,
1951
                                 LEAVE_REQUESTED,
1952
                                 USER_SUSPENDED,
1953
                                 ])
1954

    
1955
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1956

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

    
1962
    objects     =   ProjectMembershipManager()
1963

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

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

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

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

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

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

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

    
1998
    __repr__ = __str__
1999

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

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

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

    
2017
    def can_accept(self):
2018
        return self.state == self.REQUESTED
2019

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

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

    
2031
    def can_leave(self):
2032
        return self.state in self.ACCEPTED_STATES
2033

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

    
2040
        self.leave_request_date = datetime.now()
2041
        self.state = self.LEAVE_REQUESTED
2042
        self.save()
2043

    
2044
    def can_deny_leave(self):
2045
        return self.state == self.LEAVE_REQUESTED
2046

    
2047
    def leave_request_deny(self):
2048
        if not self.can_deny_leave():
2049
            m = _("%s: attempt to deny leave request in state '%s'") % (
2050
                self, self.state)
2051
            raise AssertionError(m)
2052

    
2053
        self.leave_request_date = None
2054
        self.state = self.ACCEPTED
2055
        self.save()
2056

    
2057
    def can_cancel_leave(self):
2058
        return self.state == self.LEAVE_REQUESTED
2059

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

    
2066
        self.leave_request_date = None
2067
        self.state = self.ACCEPTED
2068
        self.save()
2069

    
2070
    def can_remove(self):
2071
        return self.state in self.ACCEPTED_STATES
2072

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

    
2078
        self._set_history_item(reason='REMOVE')
2079
        self.delete()
2080

    
2081
    def can_reject(self):
2082
        return self.state == self.REQUESTED
2083

    
2084
    def reject(self):
2085
        if not self.can_reject():
2086
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2087
            raise AssertionError(m)
2088

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

    
2094
    def can_cancel(self):
2095
        return self.state == self.REQUESTED
2096

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

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

    
2107

    
2108
class Serial(models.Model):
2109
    serial  =   models.AutoField(primary_key=True)
2110

    
2111

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

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

    
2122
### SIGNALS ###
2123
################
2124

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

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

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

    
2149
def astakosuser_post_save(sender, instance, created, **kwargs):
2150
    pass
2151

    
2152
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2153

    
2154
def resource_post_save(sender, instance, created, **kwargs):
2155
    pass
2156

    
2157
post_save.connect(resource_post_save, sender=Resource)
2158

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