Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (72.2 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 signed_terms(self):
548
        term = get_latest_terms()
549
        if not term:
550
            return True
551
        if not self.has_signed_terms:
552
            return False
553
        if not self.date_signed_terms:
554
            return False
555
        if self.date_signed_terms < term.date:
556
            self.has_signed_terms = False
557
            self.date_signed_terms = None
558
            self.save()
559
            return False
560
        return True
561

    
562
    def set_invitations_level(self):
563
        """
564
        Update user invitation level
565
        """
566
        level = self.invitation.inviter.level + 1
567
        self.level = level
568
        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
569

    
570
    def can_change_password(self):
571
        return self.has_auth_provider('local', auth_backend='astakos')
572

    
573
    def can_change_email(self):
574
        if not self.has_auth_provider('local'):
575
            return True
576

    
577
        local = self.get_auth_provider('local')._instance
578
        return local.auth_backend == 'astakos'
579

    
580
    # Auth providers related methods
581
    def get_auth_provider(self, module=None, identifier=None, **filters):
582
        if not module:
583
            return self.auth_providers.active()[0].settings
584

    
585
        params = {'module': module}
586
        if identifier:
587
            params['identifier'] = identifier
588
        params.update(filters)
589
        return self.auth_providers.active().get(**params).settings
590

    
591
    def has_auth_provider(self, provider, **kwargs):
592
        return bool(self.auth_providers.active().filter(module=provider,
593
                                                        **kwargs).count())
594

    
595
    def get_required_providers(self, **kwargs):
596
        return auth.REQUIRED_PROVIDERS.keys()
597

    
598
    def missing_required_providers(self):
599
        required = self.get_required_providers()
600
        missing = []
601
        for provider in required:
602
            if not self.has_auth_provider(provider):
603
                missing.append(auth.get_provider(provider, self))
604
        return missing
605

    
606
    def get_available_auth_providers(self, **filters):
607
        """
608
        Returns a list of providers available for add by the user.
609
        """
610
        modules = astakos_settings.IM_MODULES
611
        providers = []
612
        for p in modules:
613
            providers.append(auth.get_provider(p, self))
614
        available = []
615

    
616
        for p in providers:
617
            if p.get_add_policy:
618
                available.append(p)
619
        return available
620

    
621
    def get_disabled_auth_providers(self, **filters):
622
        providers = self.get_auth_providers(**filters)
623
        disabled = []
624
        for p in providers:
625
            if not p.get_login_policy:
626
                disabled.append(p)
627
        return disabled
628

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

    
637
    def get_auth_providers(self, **filters):
638
        providers = []
639
        for provider in self.auth_providers.active(**filters):
640
            if provider.settings.module_enabled:
641
                providers.append(provider.settings)
642

    
643
        modules = astakos_settings.IM_MODULES
644

    
645
        def key(p):
646
            if not p.module in modules:
647
                return 100
648
            return modules.index(p.module)
649

    
650
        providers = sorted(providers, key=key)
651
        return providers
652

    
653
    # URL methods
654
    @property
655
    def auth_providers_display(self):
656
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
657
                         self.get_enabled_auth_providers()])
658

    
659
    def add_auth_provider(self, module='local', identifier=None, **params):
660
        provider = auth.get_provider(module, self, identifier, **params)
661
        provider.add_to_user()
662

    
663
    def get_resend_activation_url(self):
664
        return reverse('send_activation', kwargs={'user_id': self.pk})
665

    
666
    def get_activation_url(self, nxt=False):
667
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
668
                                 quote(self.verification_code))
669
        if nxt:
670
            url += "&next=%s" % quote(nxt)
671
        return url
672

    
673
    def get_password_reset_url(self, token_generator=default_token_generator):
674
        return reverse('astakos.im.views.target.local.password_reset_confirm',
675
                          kwargs={'uidb36':int_to_base36(self.id),
676
                                  'token':token_generator.make_token(self)})
677

    
678
    def get_inactive_message(self, provider_module, identifier=None):
679
        provider = self.get_auth_provider(provider_module, identifier)
680

    
681
        msg_extra = ''
682
        message = ''
683

    
684
        msg_inactive = provider.get_account_inactive_msg
685
        msg_pending = provider.get_pending_activation_msg
686
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
687
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
688
        msg_pending_mod = provider.get_pending_moderation_msg
689
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
690
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
691

    
692
        if not self.email_verified:
693
            message = msg_pending
694
            url = self.get_resend_activation_url()
695
            msg_extra = msg_pending_help + \
696
                        u' ' + \
697
                        '<a href="%s">%s?</a>' % (url, msg_resend)
698
        else:
699
            if not self.moderated:
700
                message = msg_pending_mod
701
            else:
702
                if self.is_rejected:
703
                    message = msg_rejected
704
                else:
705
                    message = msg_inactive
706

    
707
        return mark_safe(message + u' ' + msg_extra)
708

    
709
    def owns_application(self, application):
710
        return application.owner == self
711

    
712
    def owns_project(self, project):
713
        return project.application.owner == self
714

    
715
    def is_associated(self, project):
716
        try:
717
            m = ProjectMembership.objects.get(person=self, project=project)
718
            return m.state in ProjectMembership.ASSOCIATED_STATES
719
        except ProjectMembership.DoesNotExist:
720
            return False
721

    
722
    def get_membership(self, project):
723
        try:
724
            return ProjectMembership.objects.get(
725
                project=project,
726
                person=self)
727
        except ProjectMembership.DoesNotExist:
728
            return None
729

    
730
    def membership_display(self, project):
731
        m = self.get_membership(project)
732
        if m is None:
733
            return _('Not a member')
734
        else:
735
            return m.user_friendly_state_display()
736

    
737
    def non_owner_can_view(self, maybe_project):
738
        if self.is_project_admin():
739
            return True
740
        if maybe_project is None:
741
            return False
742
        project = maybe_project
743
        if self.is_associated(project):
744
            return True
745
        if project.is_deactivated():
746
            return False
747
        return True
748

    
749

    
750
class AstakosUserAuthProviderManager(models.Manager):
751

    
752
    def active(self, **filters):
753
        return self.filter(active=True, **filters)
754

    
755
    def remove_unverified_providers(self, provider, **filters):
756
        try:
757
            existing = self.filter(module=provider, user__email_verified=False,
758
                                   **filters)
759
            for p in existing:
760
                p.user.delete()
761
        except:
762
            pass
763

    
764
    def unverified(self, provider, **filters):
765
        try:
766
            return self.get(module=provider, user__email_verified=False,
767
                            **filters).settings
768
        except AstakosUserAuthProvider.DoesNotExist:
769
            return None
770

    
771
    def verified(self, provider, **filters):
772
        try:
773
            return self.get(module=provider, user__email_verified=True,
774
                            **filters).settings
775
        except AstakosUserAuthProvider.DoesNotExist:
776
            return None
777

    
778

    
779
class AuthProviderPolicyProfileManager(models.Manager):
780

    
781
    def active(self):
782
        return self.filter(active=True)
783

    
784
    def for_user(self, user, provider):
785
        policies = {}
786
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
787
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
788
        exclusive_q = exclusive_q1 | exclusive_q2
789

    
790
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
791
            policies.update(profile.policies)
792

    
793
        user_groups = user.groups.all().values('pk')
794
        for profile in self.active().filter(groups__in=user_groups).filter(
795
                exclusive_q):
796
            policies.update(profile.policies)
797
        return policies
798

    
799
    def add_policy(self, name, provider, group_or_user, exclusive=False,
800
                   **policies):
801
        is_group = isinstance(group_or_user, Group)
802
        profile, created = self.get_or_create(name=name, provider=provider,
803
                                              is_exclusive=exclusive)
804
        profile.is_exclusive = exclusive
805
        profile.save()
806
        if is_group:
807
            profile.groups.add(group_or_user)
808
        else:
809
            profile.users.add(group_or_user)
810
        profile.set_policies(policies)
811
        profile.save()
812
        return profile
813

    
814

    
815
class AuthProviderPolicyProfile(models.Model):
816
    name = models.CharField(_('Name'), max_length=255, blank=False,
817
                            null=False, db_index=True)
818
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
819
                                null=False)
820

    
821
    # apply policies to all providers excluding the one set in provider field
822
    is_exclusive = models.BooleanField(default=False)
823

    
824
    policy_add = models.NullBooleanField(null=True, default=None)
825
    policy_remove = models.NullBooleanField(null=True, default=None)
826
    policy_create = models.NullBooleanField(null=True, default=None)
827
    policy_login = models.NullBooleanField(null=True, default=None)
828
    policy_limit = models.IntegerField(null=True, default=None)
829
    policy_required = models.NullBooleanField(null=True, default=None)
830
    policy_automoderate = models.NullBooleanField(null=True, default=None)
831
    policy_switch = models.NullBooleanField(null=True, default=None)
832

    
833
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
834
                     'automoderate')
835

    
836
    priority = models.IntegerField(null=False, default=1)
837
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
838
    users = models.ManyToManyField(AstakosUser,
839
                                   related_name='authpolicy_profiles')
840
    active = models.BooleanField(default=True)
841

    
842
    objects = AuthProviderPolicyProfileManager()
843

    
844
    class Meta:
845
        ordering = ['priority']
846

    
847
    @property
848
    def policies(self):
849
        policies = {}
850
        for pkey in self.POLICY_FIELDS:
851
            value = getattr(self, 'policy_%s' % pkey, None)
852
            if value is None:
853
                continue
854
            policies[pkey] = value
855
        return policies
856

    
857
    def set_policies(self, policies_dict):
858
        for key, value in policies_dict.iteritems():
859
            if key in self.POLICY_FIELDS:
860
                setattr(self, 'policy_%s' % key, value)
861
        return self.policies
862

    
863

    
864
class AstakosUserAuthProvider(models.Model):
865
    """
866
    Available user authentication methods.
867
    """
868
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
869
                                   null=True, default=None)
870
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
871
    module = models.CharField(_('Provider'), max_length=255, blank=False,
872
                                default='local')
873
    identifier = models.CharField(_('Third-party identifier'),
874
                                              max_length=255, null=True,
875
                                              blank=True)
876
    active = models.BooleanField(default=True)
877
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
878
                                   default='astakos')
879
    info_data = models.TextField(default="", null=True, blank=True)
880
    created = models.DateTimeField('Creation date', auto_now_add=True)
881

    
882
    objects = AstakosUserAuthProviderManager()
883

    
884
    class Meta:
885
        unique_together = (('identifier', 'module', 'user'), )
886
        ordering = ('module', 'created')
887

    
888
    def __init__(self, *args, **kwargs):
889
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
890
        try:
891
            self.info = json.loads(self.info_data)
892
            if not self.info:
893
                self.info = {}
894
        except Exception, e:
895
            self.info = {}
896

    
897
        for key,value in self.info.iteritems():
898
            setattr(self, 'info_%s' % key, value)
899

    
900
    @property
901
    def settings(self):
902
        extra_data = {}
903

    
904
        info_data = {}
905
        if self.info_data:
906
            info_data = json.loads(self.info_data)
907

    
908
        extra_data['info'] = info_data
909

    
910
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
911
            extra_data[key] = getattr(self, key)
912

    
913
        extra_data['instance'] = self
914
        return auth.get_provider(self.module, self.user,
915
                                           self.identifier, **extra_data)
916

    
917
    def __repr__(self):
918
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
919

    
920
    def __unicode__(self):
921
        if self.identifier:
922
            return "%s:%s" % (self.module, self.identifier)
923
        if self.auth_backend:
924
            return "%s:%s" % (self.module, self.auth_backend)
925
        return self.module
926

    
927
    def save(self, *args, **kwargs):
928
        self.info_data = json.dumps(self.info)
929
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
930

    
931

    
932
class ExtendedManager(models.Manager):
933
    def _update_or_create(self, **kwargs):
934
        assert kwargs, \
935
            'update_or_create() must be passed at least one keyword argument'
936
        obj, created = self.get_or_create(**kwargs)
937
        defaults = kwargs.pop('defaults', {})
938
        if created:
939
            return obj, True, False
940
        else:
941
            try:
942
                params = dict(
943
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
944
                params.update(defaults)
945
                for attr, val in params.items():
946
                    if hasattr(obj, attr):
947
                        setattr(obj, attr, val)
948
                sid = transaction.savepoint()
949
                obj.save(force_update=True)
950
                transaction.savepoint_commit(sid)
951
                return obj, False, True
952
            except IntegrityError, e:
953
                transaction.savepoint_rollback(sid)
954
                try:
955
                    return self.get(**kwargs), False, False
956
                except self.model.DoesNotExist:
957
                    raise e
958

    
959
    update_or_create = _update_or_create
960

    
961

    
962
class AstakosUserQuota(models.Model):
963
    objects = ExtendedManager()
964
    capacity = intDecimalField()
965
    resource = models.ForeignKey(Resource)
966
    user = models.ForeignKey(AstakosUser)
967

    
968
    class Meta:
969
        unique_together = ("resource", "user")
970

    
971

    
972
class ApprovalTerms(models.Model):
973
    """
974
    Model for approval terms
975
    """
976

    
977
    date = models.DateTimeField(
978
        _('Issue date'), db_index=True, auto_now_add=True)
979
    location = models.CharField(_('Terms location'), max_length=255)
980

    
981

    
982
class Invitation(models.Model):
983
    """
984
    Model for registring invitations
985
    """
986
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
987
                                null=True)
988
    realname = models.CharField(_('Real name'), max_length=255)
989
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
990
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
991
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
992
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
993
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
994

    
995
    def __init__(self, *args, **kwargs):
996
        super(Invitation, self).__init__(*args, **kwargs)
997
        if not self.id:
998
            self.code = _generate_invitation_code()
999

    
1000
    def consume(self):
1001
        self.is_consumed = True
1002
        self.consumed = datetime.now()
1003
        self.save()
1004

    
1005
    def __unicode__(self):
1006
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1007

    
1008

    
1009
class EmailChangeManager(models.Manager):
1010

    
1011
    @transaction.commit_on_success
1012
    def change_email(self, activation_key):
1013
        """
1014
        Validate an activation key and change the corresponding
1015
        ``User`` if valid.
1016

1017
        If the key is valid and has not expired, return the ``User``
1018
        after activating.
1019

1020
        If the key is not valid or has expired, return ``None``.
1021

1022
        If the key is valid but the ``User`` is already active,
1023
        return ``None``.
1024

1025
        After successful email change the activation record is deleted.
1026

1027
        Throws ValueError if there is already
1028
        """
1029
        try:
1030
            email_change = self.model.objects.get(
1031
                activation_key=activation_key)
1032
            if email_change.activation_key_expired():
1033
                email_change.delete()
1034
                raise EmailChange.DoesNotExist
1035
            # is there an active user with this address?
1036
            try:
1037
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1038
            except AstakosUser.DoesNotExist:
1039
                pass
1040
            else:
1041
                raise ValueError(_('The new email address is reserved.'))
1042
            # update user
1043
            user = AstakosUser.objects.get(pk=email_change.user_id)
1044
            old_email = user.email
1045
            user.email = email_change.new_email_address
1046
            user.save()
1047
            email_change.delete()
1048
            msg = "User %s changed email from %s to %s" % (user.log_display,
1049
                                                           old_email,
1050
                                                           user.email)
1051
            logger.log(LOGGING_LEVEL, msg)
1052
            return user
1053
        except EmailChange.DoesNotExist:
1054
            raise ValueError(_('Invalid activation key.'))
1055

    
1056

    
1057
class EmailChange(models.Model):
1058
    new_email_address = models.EmailField(
1059
        _(u'new e-mail address'),
1060
        help_text=_('Provide a new email address. Until you verify the new '
1061
                    'address by following the activation link that will be '
1062
                    'sent to it, your old email address will remain active.'))
1063
    user = models.ForeignKey(
1064
        AstakosUser, unique=True, related_name='emailchanges')
1065
    requested_at = models.DateTimeField(auto_now_add=True)
1066
    activation_key = models.CharField(
1067
        max_length=40, unique=True, db_index=True)
1068

    
1069
    objects = EmailChangeManager()
1070

    
1071
    def get_url(self):
1072
        return reverse('email_change_confirm',
1073
                      kwargs={'activation_key': self.activation_key})
1074

    
1075
    def activation_key_expired(self):
1076
        expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
1077
        return self.requested_at + expiration_date < datetime.now()
1078

    
1079

    
1080
class AdditionalMail(models.Model):
1081
    """
1082
    Model for registring invitations
1083
    """
1084
    owner = models.ForeignKey(AstakosUser)
1085
    email = models.EmailField()
1086

    
1087

    
1088
def _generate_invitation_code():
1089
    while True:
1090
        code = randint(1, 2L ** 63 - 1)
1091
        try:
1092
            Invitation.objects.get(code=code)
1093
            # An invitation with this code already exists, try again
1094
        except Invitation.DoesNotExist:
1095
            return code
1096

    
1097

    
1098
def get_latest_terms():
1099
    try:
1100
        term = ApprovalTerms.objects.order_by('-id')[0]
1101
        return term
1102
    except IndexError:
1103
        pass
1104
    return None
1105

    
1106

    
1107
class PendingThirdPartyUser(models.Model):
1108
    """
1109
    Model for registring successful third party user authentications
1110
    """
1111
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1112
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1113
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1114
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1115
                                  null=True)
1116
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1117
                                 null=True)
1118
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1119
                                   null=True)
1120
    username = models.CharField(_('username'), max_length=30, unique=True,
1121
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1122
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1123
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1124
    info = models.TextField(default="", null=True, blank=True)
1125

    
1126
    class Meta:
1127
        unique_together = ("provider", "third_party_identifier")
1128

    
1129
    def get_user_instance(self):
1130
        """
1131
        Create a new AstakosUser instance based on details provided when user
1132
        initially signed up.
1133
        """
1134
        d = copy.copy(self.__dict__)
1135
        d.pop('_state', None)
1136
        d.pop('id', None)
1137
        d.pop('token', None)
1138
        d.pop('created', None)
1139
        d.pop('info', None)
1140
        d.pop('affiliation', None)
1141
        d.pop('provider', None)
1142
        d.pop('third_party_identifier', None)
1143
        user = AstakosUser(**d)
1144

    
1145
        return user
1146

    
1147
    @property
1148
    def realname(self):
1149
        return '%s %s' %(self.first_name, self.last_name)
1150

    
1151
    @realname.setter
1152
    def realname(self, value):
1153
        parts = value.split(' ')
1154
        if len(parts) == 2:
1155
            self.first_name = parts[0]
1156
            self.last_name = parts[1]
1157
        else:
1158
            self.last_name = parts[0]
1159

    
1160
    def save(self, **kwargs):
1161
        if not self.id:
1162
            # set username
1163
            while not self.username:
1164
                username =  uuid.uuid4().hex[:30]
1165
                try:
1166
                    AstakosUser.objects.get(username = username)
1167
                except AstakosUser.DoesNotExist, e:
1168
                    self.username = username
1169
        super(PendingThirdPartyUser, self).save(**kwargs)
1170

    
1171
    def generate_token(self):
1172
        self.password = self.third_party_identifier
1173
        self.last_login = datetime.now()
1174
        self.token = default_token_generator.make_token(self)
1175

    
1176
    def existing_user(self):
1177
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1178
                                         auth_providers__identifier=self.third_party_identifier)
1179

    
1180
    def get_provider(self, user):
1181
        params = {
1182
            'info_data': self.info,
1183
            'affiliation': self.affiliation
1184
        }
1185
        return auth.get_provider(self.provider, user,
1186
                                 self.third_party_identifier, **params)
1187

    
1188
class SessionCatalog(models.Model):
1189
    session_key = models.CharField(_('session key'), max_length=40)
1190
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1191

    
1192

    
1193
class UserSetting(models.Model):
1194
    user = models.ForeignKey(AstakosUser)
1195
    setting = models.CharField(max_length=255)
1196
    value = models.IntegerField()
1197

    
1198
    objects = ForUpdateManager()
1199

    
1200
    class Meta:
1201
        unique_together = ("user", "setting")
1202

    
1203

    
1204
### PROJECTS ###
1205
################
1206

    
1207
class ChainManager(ForUpdateManager):
1208

    
1209
    def search_by_name(self, *search_strings):
1210
        projects = Project.objects.search_by_name(*search_strings)
1211
        chains = [p.id for p in projects]
1212
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1213
        apps = (app for app in apps if app.is_latest())
1214
        app_chains = [app.chain for app in apps if app.chain not in chains]
1215
        return chains + app_chains
1216

    
1217
    def all_full_state(self):
1218
        chains = self.all()
1219
        cids = [c.chain for c in chains]
1220
        projects = Project.objects.select_related('application').in_bulk(cids)
1221

    
1222
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1223
        chain_latest = dict(objs.values_list('chain', 'latest'))
1224

    
1225
        objs = ProjectApplication.objects.select_related('applicant')
1226
        apps = objs.in_bulk(chain_latest.values())
1227

    
1228
        d = {}
1229
        for chain in chains:
1230
            pk = chain.pk
1231
            project = projects.get(pk, None)
1232
            app = apps[chain_latest[pk]]
1233
            d[chain.pk] = chain.get_state(project, app)
1234

    
1235
        return d
1236

    
1237
    def of_project(self, project):
1238
        if project is None:
1239
            return None
1240
        try:
1241
            return self.get(chain=project.id)
1242
        except Chain.DoesNotExist:
1243
            raise AssertionError('project with no chain')
1244

    
1245

    
1246
class Chain(models.Model):
1247
    chain  =   models.AutoField(primary_key=True)
1248

    
1249
    def __str__(self):
1250
        return "%s" % (self.chain,)
1251

    
1252
    objects = ChainManager()
1253

    
1254
    PENDING            = 0
1255
    DENIED             = 3
1256
    DISMISSED          = 4
1257
    CANCELLED          = 5
1258

    
1259
    APPROVED           = 10
1260
    APPROVED_PENDING   = 11
1261
    SUSPENDED          = 12
1262
    SUSPENDED_PENDING  = 13
1263
    TERMINATED         = 14
1264
    TERMINATED_PENDING = 15
1265

    
1266
    PENDING_STATES = [PENDING,
1267
                      APPROVED_PENDING,
1268
                      SUSPENDED_PENDING,
1269
                      TERMINATED_PENDING,
1270
                      ]
1271

    
1272
    MODIFICATION_STATES = [APPROVED_PENDING,
1273
                           SUSPENDED_PENDING,
1274
                           TERMINATED_PENDING,
1275
                           ]
1276

    
1277
    RELEVANT_STATES = [PENDING,
1278
                       DENIED,
1279
                       APPROVED,
1280
                       APPROVED_PENDING,
1281
                       SUSPENDED,
1282
                       SUSPENDED_PENDING,
1283
                       TERMINATED_PENDING,
1284
                       ]
1285

    
1286
    SKIP_STATES = [DISMISSED,
1287
                   CANCELLED,
1288
                   TERMINATED]
1289

    
1290
    STATE_DISPLAY = {
1291
        PENDING            : _("Pending"),
1292
        DENIED             : _("Denied"),
1293
        DISMISSED          : _("Dismissed"),
1294
        CANCELLED          : _("Cancelled"),
1295
        APPROVED           : _("Active"),
1296
        APPROVED_PENDING   : _("Active - Pending"),
1297
        SUSPENDED          : _("Suspended"),
1298
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1299
        TERMINATED         : _("Terminated"),
1300
        TERMINATED_PENDING : _("Terminated - Pending"),
1301
        }
1302

    
1303

    
1304
    @classmethod
1305
    def _chain_state(cls, project_state, app_state):
1306
        s = CHAIN_STATE.get((project_state, app_state), None)
1307
        if s is None:
1308
            raise AssertionError('inconsistent chain state')
1309
        return s
1310

    
1311
    @classmethod
1312
    def chain_state(cls, project, app):
1313
        p_state = project.state if project else None
1314
        return cls._chain_state(p_state, app.state)
1315

    
1316
    @classmethod
1317
    def state_display(cls, s):
1318
        if s is None:
1319
            return _("Unknown")
1320
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1321

    
1322
    def last_application(self):
1323
        return self.chained_apps.order_by('-id')[0]
1324

    
1325
    def get_project(self):
1326
        try:
1327
            return self.chained_project
1328
        except Project.DoesNotExist:
1329
            return None
1330

    
1331
    def get_elements(self):
1332
        project = self.get_project()
1333
        app = self.last_application()
1334
        return project, app
1335

    
1336
    def get_state(self, project, app):
1337
        s = self.chain_state(project, app)
1338
        return s, project, app
1339

    
1340
    def full_state(self):
1341
        project, app = self.get_elements()
1342
        return self.get_state(project, app)
1343

    
1344

    
1345
def new_chain():
1346
    c = Chain.objects.create()
1347
    return c
1348

    
1349

    
1350
class ProjectApplicationManager(ForUpdateManager):
1351

    
1352
    def user_visible_projects(self, *filters, **kw_filters):
1353
        model = self.model
1354
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1355

    
1356
    def user_visible_by_chain(self, flt):
1357
        model = self.model
1358
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1359
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1360
        by_chain = dict(pending.annotate(models.Max('id')))
1361
        by_chain.update(approved.annotate(models.Max('id')))
1362
        return self.filter(flt, id__in=by_chain.values())
1363

    
1364
    def user_accessible_projects(self, user):
1365
        """
1366
        Return projects accessed by specified user.
1367
        """
1368
        if user.is_project_admin():
1369
            participates_filters = Q()
1370
        else:
1371
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1372
                                   Q(project__projectmembership__person=user)
1373

    
1374
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1375

    
1376
    def search_by_name(self, *search_strings):
1377
        q = Q()
1378
        for s in search_strings:
1379
            q = q | Q(name__icontains=s)
1380
        return self.filter(q)
1381

    
1382
    def latest_of_chain(self, chain_id):
1383
        try:
1384
            return self.filter(chain=chain_id).order_by('-id')[0]
1385
        except IndexError:
1386
            return None
1387

    
1388

    
1389
class ProjectApplication(models.Model):
1390
    applicant               =   models.ForeignKey(
1391
                                    AstakosUser,
1392
                                    related_name='projects_applied',
1393
                                    db_index=True)
1394

    
1395
    PENDING     =    0
1396
    APPROVED    =    1
1397
    REPLACED    =    2
1398
    DENIED      =    3
1399
    DISMISSED   =    4
1400
    CANCELLED   =    5
1401

    
1402
    state                   =   models.IntegerField(default=PENDING,
1403
                                                    db_index=True)
1404

    
1405
    owner                   =   models.ForeignKey(
1406
                                    AstakosUser,
1407
                                    related_name='projects_owned',
1408
                                    db_index=True)
1409

    
1410
    chain                   =   models.ForeignKey(Chain,
1411
                                                  related_name='chained_apps',
1412
                                                  db_column='chain')
1413
    precursor_application   =   models.ForeignKey('ProjectApplication',
1414
                                                  null=True,
1415
                                                  blank=True)
1416

    
1417
    name                    =   models.CharField(max_length=80)
1418
    homepage                =   models.URLField(max_length=255, null=True,
1419
                                                verify_exists=False)
1420
    description             =   models.TextField(null=True, blank=True)
1421
    start_date              =   models.DateTimeField(null=True, blank=True)
1422
    end_date                =   models.DateTimeField()
1423
    member_join_policy      =   models.IntegerField()
1424
    member_leave_policy     =   models.IntegerField()
1425
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1426
    resource_grants         =   models.ManyToManyField(
1427
                                    Resource,
1428
                                    null=True,
1429
                                    blank=True,
1430
                                    through='ProjectResourceGrant')
1431
    comments                =   models.TextField(null=True, blank=True)
1432
    issue_date              =   models.DateTimeField(auto_now_add=True)
1433
    response_date           =   models.DateTimeField(null=True, blank=True)
1434
    response                =   models.TextField(null=True, blank=True)
1435

    
1436
    objects                 =   ProjectApplicationManager()
1437

    
1438
    # Compiled queries
1439
    Q_PENDING  = Q(state=PENDING)
1440
    Q_APPROVED = Q(state=APPROVED)
1441
    Q_DENIED   = Q(state=DENIED)
1442

    
1443
    class Meta:
1444
        unique_together = ("chain", "id")
1445

    
1446
    def __unicode__(self):
1447
        return "%s applied by %s" % (self.name, self.applicant)
1448

    
1449
    # TODO: Move to a more suitable place
1450
    APPLICATION_STATE_DISPLAY = {
1451
        PENDING  : _('Pending review'),
1452
        APPROVED : _('Approved'),
1453
        REPLACED : _('Replaced'),
1454
        DENIED   : _('Denied'),
1455
        DISMISSED: _('Dismissed'),
1456
        CANCELLED: _('Cancelled')
1457
    }
1458

    
1459
    @property
1460
    def log_display(self):
1461
        return "application %s (%s) for project %s" % (
1462
            self.id, self.name, self.chain)
1463

    
1464
    def state_display(self):
1465
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1466

    
1467
    def project_state_display(self):
1468
        try:
1469
            project = self.project
1470
            return project.state_display()
1471
        except Project.DoesNotExist:
1472
            return self.state_display()
1473

    
1474
    def add_resource_policy(self, resource, uplimit):
1475
        """Raises ObjectDoesNotExist, IntegrityError"""
1476
        q = self.projectresourcegrant_set
1477
        resource = Resource.objects.get(name=resource)
1478
        q.create(resource=resource, member_capacity=uplimit)
1479

    
1480
    def members_count(self):
1481
        return self.project.approved_memberships.count()
1482

    
1483
    @property
1484
    def grants(self):
1485
        return self.projectresourcegrant_set.values('member_capacity',
1486
                                                    'resource__name')
1487

    
1488
    @property
1489
    def resource_policies(self):
1490
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1491

    
1492
    def set_resource_policies(self, policies):
1493
        for resource, uplimit in policies:
1494
            self.add_resource_policy(resource, uplimit)
1495

    
1496
    def pending_modifications_incl_me(self):
1497
        q = self.chained_applications()
1498
        q = q.filter(Q(state=self.PENDING))
1499
        return q
1500

    
1501
    def last_pending_incl_me(self):
1502
        try:
1503
            return self.pending_modifications_incl_me().order_by('-id')[0]
1504
        except IndexError:
1505
            return None
1506

    
1507
    def pending_modifications(self):
1508
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1509

    
1510
    def last_pending(self):
1511
        try:
1512
            return self.pending_modifications().order_by('-id')[0]
1513
        except IndexError:
1514
            return None
1515

    
1516
    def is_modification(self):
1517
        # if self.state != self.PENDING:
1518
        #     return False
1519
        parents = self.chained_applications().filter(id__lt=self.id)
1520
        parents = parents.filter(state__in=[self.APPROVED])
1521
        return parents.count() > 0
1522

    
1523
    def chained_applications(self):
1524
        return ProjectApplication.objects.filter(chain=self.chain)
1525

    
1526
    def is_latest(self):
1527
        return self.chained_applications().order_by('-id')[0] == self
1528

    
1529
    def has_pending_modifications(self):
1530
        return bool(self.last_pending())
1531

    
1532
    def denied_modifications(self):
1533
        q = self.chained_applications()
1534
        q = q.filter(Q(state=self.DENIED))
1535
        q = q.filter(~Q(id=self.id))
1536
        return q
1537

    
1538
    def last_denied(self):
1539
        try:
1540
            return self.denied_modifications().order_by('-id')[0]
1541
        except IndexError:
1542
            return None
1543

    
1544
    def has_denied_modifications(self):
1545
        return bool(self.last_denied())
1546

    
1547
    def is_applied(self):
1548
        try:
1549
            self.project
1550
            return True
1551
        except Project.DoesNotExist:
1552
            return False
1553

    
1554
    def get_project(self):
1555
        try:
1556
            return Project.objects.get(id=self.chain)
1557
        except Project.DoesNotExist:
1558
            return None
1559

    
1560
    def project_exists(self):
1561
        return self.get_project() is not None
1562

    
1563
    def can_cancel(self):
1564
        return self.state == self.PENDING
1565

    
1566
    def cancel(self):
1567
        if not self.can_cancel():
1568
            m = _("cannot cancel: application '%s' in state '%s'") % (
1569
                    self.id, self.state)
1570
            raise AssertionError(m)
1571

    
1572
        self.state = self.CANCELLED
1573
        self.save()
1574

    
1575
    def can_dismiss(self):
1576
        return self.state == self.DENIED
1577

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

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

    
1587
    def can_deny(self):
1588
        return self.state == self.PENDING
1589

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

    
1596
        self.state = self.DENIED
1597
        self.response_date = datetime.now()
1598
        self.response = reason
1599
        self.save()
1600

    
1601
    def can_approve(self):
1602
        return self.state == self.PENDING
1603

    
1604
    def approve(self, reason):
1605
        if not self.can_approve():
1606
            m = _("cannot approve: project '%s' in state '%s'") % (
1607
                    self.name, self.state)
1608
            raise AssertionError(m) # invalid argument
1609

    
1610
        now = datetime.now()
1611
        self.state = self.APPROVED
1612
        self.response_date = now
1613
        self.response = reason
1614
        self.save()
1615

    
1616
        project = self.get_project()
1617
        if project is None:
1618
            project = Project(id=self.chain)
1619

    
1620
        project.name = self.name
1621
        project.application = self
1622
        project.last_approval_date = now
1623
        project.save()
1624
        return project
1625

    
1626
    @property
1627
    def member_join_policy_display(self):
1628
        policy = self.member_join_policy
1629
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1630

    
1631
    @property
1632
    def member_leave_policy_display(self):
1633
        policy = self.member_leave_policy
1634
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1635

    
1636
class ProjectResourceGrant(models.Model):
1637

    
1638
    resource                =   models.ForeignKey(Resource)
1639
    project_application     =   models.ForeignKey(ProjectApplication,
1640
                                                  null=True)
1641
    project_capacity        =   intDecimalField(null=True)
1642
    member_capacity         =   intDecimalField(default=0)
1643

    
1644
    objects = ExtendedManager()
1645

    
1646
    class Meta:
1647
        unique_together = ("resource", "project_application")
1648

    
1649
    def display_member_capacity(self):
1650
        if self.member_capacity:
1651
            if self.resource.unit:
1652
                return ProjectResourceGrant.display_filesize(
1653
                    self.member_capacity)
1654
            else:
1655
                if math.isinf(self.member_capacity):
1656
                    return 'Unlimited'
1657
                else:
1658
                    return self.member_capacity
1659
        else:
1660
            return 'Unlimited'
1661

    
1662
    def __str__(self):
1663
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1664
                                        self.display_member_capacity())
1665

    
1666
    @classmethod
1667
    def display_filesize(cls, value):
1668
        try:
1669
            value = float(value)
1670
        except:
1671
            return
1672
        else:
1673
            if math.isinf(value):
1674
                return 'Unlimited'
1675
            if value > 1:
1676
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1677
                                [0, 0, 0, 0, 0, 0])
1678
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1679
                quotient = float(value) / 1024**exponent
1680
                unit, value_decimals = unit_list[exponent]
1681
                format_string = '{0:.%sf} {1}' % (value_decimals)
1682
                return format_string.format(quotient, unit)
1683
            if value == 0:
1684
                return '0 bytes'
1685
            if value == 1:
1686
                return '1 byte'
1687
            else:
1688
               return '0'
1689

    
1690

    
1691
class ProjectManager(ForUpdateManager):
1692

    
1693
    def terminated_projects(self):
1694
        q = self.model.Q_TERMINATED
1695
        return self.filter(q)
1696

    
1697
    def not_terminated_projects(self):
1698
        q = ~self.model.Q_TERMINATED
1699
        return self.filter(q)
1700

    
1701
    def deactivated_projects(self):
1702
        q = self.model.Q_DEACTIVATED
1703
        return self.filter(q)
1704

    
1705
    def expired_projects(self):
1706
        q = (~Q(state=Project.TERMINATED) &
1707
              Q(application__end_date__lt=datetime.now()))
1708
        return self.filter(q)
1709

    
1710
    def search_by_name(self, *search_strings):
1711
        q = Q()
1712
        for s in search_strings:
1713
            q = q | Q(name__icontains=s)
1714
        return self.filter(q)
1715

    
1716

    
1717
class Project(models.Model):
1718

    
1719
    id                          =   models.OneToOneField(Chain,
1720
                                                      related_name='chained_project',
1721
                                                      db_column='id',
1722
                                                      primary_key=True)
1723

    
1724
    application                 =   models.OneToOneField(
1725
                                            ProjectApplication,
1726
                                            related_name='project')
1727
    last_approval_date          =   models.DateTimeField(null=True)
1728

    
1729
    members                     =   models.ManyToManyField(
1730
                                            AstakosUser,
1731
                                            through='ProjectMembership')
1732

    
1733
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1734
    deactivation_date           =   models.DateTimeField(null=True)
1735

    
1736
    creation_date               =   models.DateTimeField(auto_now_add=True)
1737
    name                        =   models.CharField(
1738
                                            max_length=80,
1739
                                            null=True,
1740
                                            db_index=True,
1741
                                            unique=True)
1742

    
1743
    APPROVED    = 1
1744
    SUSPENDED   = 10
1745
    TERMINATED  = 100
1746

    
1747
    state                       =   models.IntegerField(default=APPROVED,
1748
                                                        db_index=True)
1749

    
1750
    objects     =   ProjectManager()
1751

    
1752
    # Compiled queries
1753
    Q_TERMINATED  = Q(state=TERMINATED)
1754
    Q_SUSPENDED   = Q(state=SUSPENDED)
1755
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1756

    
1757
    def __str__(self):
1758
        return uenc(_("<project %s '%s'>") %
1759
                    (self.id, udec(self.application.name)))
1760

    
1761
    __repr__ = __str__
1762

    
1763
    def __unicode__(self):
1764
        return _("<project %s '%s'>") % (self.id, self.application.name)
1765

    
1766
    STATE_DISPLAY = {
1767
        APPROVED   : 'Active',
1768
        SUSPENDED  : 'Suspended',
1769
        TERMINATED : 'Terminated'
1770
        }
1771

    
1772
    def state_display(self):
1773
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1774

    
1775
    def expiration_info(self):
1776
        return (str(self.id), self.name, self.state_display(),
1777
                str(self.application.end_date))
1778

    
1779
    def is_deactivated(self, reason=None):
1780
        if reason is not None:
1781
            return self.state == reason
1782

    
1783
        return self.state != self.APPROVED
1784

    
1785
    ### Deactivation calls
1786

    
1787
    def terminate(self):
1788
        self.deactivation_reason = 'TERMINATED'
1789
        self.deactivation_date = datetime.now()
1790
        self.state = self.TERMINATED
1791
        self.name = None
1792
        self.save()
1793

    
1794
    def suspend(self):
1795
        self.deactivation_reason = 'SUSPENDED'
1796
        self.deactivation_date = datetime.now()
1797
        self.state = self.SUSPENDED
1798
        self.save()
1799

    
1800
    def resume(self):
1801
        self.deactivation_reason = None
1802
        self.deactivation_date = None
1803
        self.state = self.APPROVED
1804
        self.save()
1805

    
1806
    ### Logical checks
1807

    
1808
    def is_inconsistent(self):
1809
        now = datetime.now()
1810
        dates = [self.creation_date,
1811
                 self.last_approval_date,
1812
                 self.deactivation_date]
1813
        return any([date > now for date in dates])
1814

    
1815
    def is_approved(self):
1816
        return self.state == self.APPROVED
1817

    
1818
    @property
1819
    def is_alive(self):
1820
        return not self.is_terminated
1821

    
1822
    @property
1823
    def is_terminated(self):
1824
        return self.is_deactivated(self.TERMINATED)
1825

    
1826
    @property
1827
    def is_suspended(self):
1828
        return self.is_deactivated(self.SUSPENDED)
1829

    
1830
    def violates_resource_grants(self):
1831
        return False
1832

    
1833
    def violates_members_limit(self, adding=0):
1834
        application = self.application
1835
        limit = application.limit_on_members_number
1836
        if limit is None:
1837
            return False
1838
        return (len(self.approved_members) + adding > limit)
1839

    
1840

    
1841
    ### Other
1842

    
1843
    def count_pending_memberships(self):
1844
        memb_set = self.projectmembership_set
1845
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1846
        return memb_count
1847

    
1848
    def members_count(self):
1849
        return self.approved_memberships.count()
1850

    
1851
    @property
1852
    def approved_memberships(self):
1853
        query = ProjectMembership.Q_ACCEPTED_STATES
1854
        return self.projectmembership_set.filter(query)
1855

    
1856
    @property
1857
    def approved_members(self):
1858
        return [m.person for m in self.approved_memberships]
1859

    
1860

    
1861
CHAIN_STATE = {
1862
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1863
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1864
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1865
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1866
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1867

    
1868
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1869
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1870
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1871
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1872
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1873

    
1874
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1875
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1876
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1877
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1878
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1879

    
1880
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1881
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1882
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1883
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1884
    }
1885

    
1886

    
1887
class ProjectMembershipManager(ForUpdateManager):
1888

    
1889
    def any_accepted(self):
1890
        q = self.model.Q_ACTUALLY_ACCEPTED
1891
        return self.filter(q)
1892

    
1893
    def actually_accepted(self):
1894
        q = self.model.Q_ACTUALLY_ACCEPTED
1895
        return self.filter(q)
1896

    
1897
    def requested(self):
1898
        return self.filter(state=ProjectMembership.REQUESTED)
1899

    
1900
    def suspended(self):
1901
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1902

    
1903
class ProjectMembership(models.Model):
1904

    
1905
    person              =   models.ForeignKey(AstakosUser)
1906
    request_date        =   models.DateTimeField(auto_now_add=True)
1907
    project             =   models.ForeignKey(Project)
1908

    
1909
    REQUESTED           =   0
1910
    ACCEPTED            =   1
1911
    LEAVE_REQUESTED     =   5
1912
    # User deactivation
1913
    USER_SUSPENDED      =   10
1914

    
1915
    REMOVED             =   200
1916

    
1917
    ASSOCIATED_STATES   =   set([REQUESTED,
1918
                                 ACCEPTED,
1919
                                 LEAVE_REQUESTED,
1920
                                 USER_SUSPENDED,
1921
                                 ])
1922

    
1923
    ACCEPTED_STATES     =   set([ACCEPTED,
1924
                                 LEAVE_REQUESTED,
1925
                                 USER_SUSPENDED,
1926
                                 ])
1927

    
1928
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1929

    
1930
    state               =   models.IntegerField(default=REQUESTED,
1931
                                                db_index=True)
1932
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
1933
    leave_request_date  =   models.DateTimeField(null=True)
1934

    
1935
    objects     =   ProjectMembershipManager()
1936

    
1937
    # Compiled queries
1938
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
1939
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1940

    
1941
    MEMBERSHIP_STATE_DISPLAY = {
1942
        REQUESTED           : _('Requested'),
1943
        ACCEPTED            : _('Accepted'),
1944
        LEAVE_REQUESTED     : _('Leave Requested'),
1945
        USER_SUSPENDED      : _('Suspended'),
1946
        REMOVED             : _('Pending removal'),
1947
        }
1948

    
1949
    USER_FRIENDLY_STATE_DISPLAY = {
1950
        REQUESTED           : _('Join requested'),
1951
        ACCEPTED            : _('Accepted member'),
1952
        LEAVE_REQUESTED     : _('Requested to leave'),
1953
        USER_SUSPENDED      : _('Suspended member'),
1954
        REMOVED             : _('Pending removal'),
1955
        }
1956

    
1957
    def state_display(self):
1958
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1959

    
1960
    def user_friendly_state_display(self):
1961
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1962

    
1963
    class Meta:
1964
        unique_together = ("person", "project")
1965
        #index_together = [["project", "state"]]
1966

    
1967
    def __str__(self):
1968
        return uenc(_("<'%s' membership in '%s'>") % (
1969
                self.person.username, self.project))
1970

    
1971
    __repr__ = __str__
1972

    
1973
    def __init__(self, *args, **kwargs):
1974
        self.state = self.REQUESTED
1975
        super(ProjectMembership, self).__init__(*args, **kwargs)
1976

    
1977
    def _set_history_item(self, reason, date=None):
1978
        if isinstance(reason, basestring):
1979
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
1980

    
1981
        history_item = ProjectMembershipHistory(
1982
                            serial=self.id,
1983
                            person=self.person_id,
1984
                            project=self.project_id,
1985
                            date=date or datetime.now(),
1986
                            reason=reason)
1987
        history_item.save()
1988
        serial = history_item.id
1989

    
1990
    def can_accept(self):
1991
        return self.state == self.REQUESTED
1992

    
1993
    def accept(self):
1994
        if not self.can_accept():
1995
            m = _("%s: attempt to accept in state '%s'") % (self, self.state)
1996
            raise AssertionError(m)
1997

    
1998
        now = datetime.now()
1999
        self.acceptance_date = now
2000
        self._set_history_item(reason='ACCEPT', date=now)
2001
        self.state = self.ACCEPTED
2002
        self.save()
2003

    
2004
    def can_leave(self):
2005
        return self.state in self.ACCEPTED_STATES
2006

    
2007
    def leave_request(self):
2008
        if not self.can_leave():
2009
            m = _("%s: attempt to request to leave in state '%s'") % (
2010
                self, self.state)
2011
            raise AssertionError(m)
2012

    
2013
        self.leave_request_date = datetime.now()
2014
        self.state = self.LEAVE_REQUESTED
2015
        self.save()
2016

    
2017
    def can_deny_leave(self):
2018
        return self.state == self.LEAVE_REQUESTED
2019

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

    
2026
        self.leave_request_date = None
2027
        self.state = self.ACCEPTED
2028
        self.save()
2029

    
2030
    def can_cancel_leave(self):
2031
        return self.state == self.LEAVE_REQUESTED
2032

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

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

    
2043
    def can_remove(self):
2044
        return self.state in self.ACCEPTED_STATES
2045

    
2046
    def remove(self):
2047
        if not self.can_remove():
2048
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2049
            raise AssertionError(m)
2050

    
2051
        self._set_history_item(reason='REMOVE')
2052
        self.delete()
2053

    
2054
    def can_reject(self):
2055
        return self.state == self.REQUESTED
2056

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

    
2062
        # rejected requests don't need sync,
2063
        # because they were never effected
2064
        self._set_history_item(reason='REJECT')
2065
        self.delete()
2066

    
2067
    def can_cancel(self):
2068
        return self.state == self.REQUESTED
2069

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

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

    
2080

    
2081
class Serial(models.Model):
2082
    serial  =   models.AutoField(primary_key=True)
2083

    
2084

    
2085
class ProjectMembershipHistory(models.Model):
2086
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2087
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2088

    
2089
    person  =   models.BigIntegerField()
2090
    project =   models.BigIntegerField()
2091
    date    =   models.DateTimeField(auto_now_add=True)
2092
    reason  =   models.IntegerField()
2093
    serial  =   models.BigIntegerField()
2094

    
2095
### SIGNALS ###
2096
################
2097

    
2098
def create_astakos_user(u):
2099
    try:
2100
        AstakosUser.objects.get(user_ptr=u.pk)
2101
    except AstakosUser.DoesNotExist:
2102
        extended_user = AstakosUser(user_ptr_id=u.pk)
2103
        extended_user.__dict__.update(u.__dict__)
2104
        extended_user.save()
2105
        if not extended_user.has_auth_provider('local'):
2106
            extended_user.add_auth_provider('local')
2107
    except BaseException, e:
2108
        logger.exception(e)
2109

    
2110
def fix_superusers():
2111
    # Associate superusers with AstakosUser
2112
    admins = User.objects.filter(is_superuser=True)
2113
    for u in admins:
2114
        create_astakos_user(u)
2115

    
2116
def user_post_save(sender, instance, created, **kwargs):
2117
    if not created:
2118
        return
2119
    create_astakos_user(instance)
2120
post_save.connect(user_post_save, sender=User)
2121

    
2122
def astakosuser_post_save(sender, instance, created, **kwargs):
2123
    pass
2124

    
2125
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2126

    
2127
def resource_post_save(sender, instance, created, **kwargs):
2128
    pass
2129

    
2130
post_save.connect(resource_post_save, sender=Resource)
2131

    
2132
def renew_token(sender, instance, **kwargs):
2133
    if not instance.auth_token:
2134
        instance.renew_token()
2135
pre_save.connect(renew_token, sender=AstakosUser)
2136
pre_save.connect(renew_token, sender=Service)