Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 7ac2131c

History | View | Annotate | Download (73.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 import settings as astakos_settings
64
from astakos.im import auth_providers as auth
65

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

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

    
74
logger = logging.getLogger(__name__)
75

    
76
DEFAULT_CONTENT_TYPE = None
77
_content_type = None
78

    
79

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

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

    
93
inf = float('inf')
94

    
95

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

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

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

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

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

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

    
143

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

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

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

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

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

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

    
169

    
170
_presentation_data = {}
171

    
172

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

    
182

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

    
191
    objects = ForUpdateManager()
192

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

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

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

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

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

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

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

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

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

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

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

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

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

    
257

    
258
class AstakosUserManager(UserManager):
259

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

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

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

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

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

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

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

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

    
312

    
313

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
518
    def token_expired(self):
519
        return self.auth_token_expires < datetime.now()
520

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

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

    
535
    def __unicode__(self):
536
        return '%s (%s)' % (self.realname, self.email)
537

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

    
545
    def email_change_is_pending(self):
546
        return self.emailchanges.count() > 0
547

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

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

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

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

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

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

    
606
        local = self.get_auth_provider('local')._instance
607
        return local.auth_backend == 'astakos'
608

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

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

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

    
624
    def get_required_providers(self, **kwargs):
625
        return auth.REQUIRED_PROVIDERS.keys()
626

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

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

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

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

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

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

    
672
        modules = astakos_settings.IM_MODULES
673

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

    
679
        providers = sorted(providers, key=key)
680
        return providers
681

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

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

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

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

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

    
707
    def get_inactive_message(self, provider_module, identifier=None):
708
        provider = self.get_auth_provider(provider_module, identifier)
709

    
710
        msg_extra = ''
711
        message = ''
712

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

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

    
736
        return mark_safe(message + u' ' + msg_extra)
737

    
738
    def owns_application(self, application):
739
        return application.owner == self
740

    
741
    def owns_project(self, project):
742
        return project.application.owner == self
743

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

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

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

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

    
778

    
779
class AstakosUserAuthProviderManager(models.Manager):
780

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

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

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

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

    
807

    
808
class AuthProviderPolicyProfileManager(models.Manager):
809

    
810
    def active(self):
811
        return self.filter(active=True)
812

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

    
819
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
820
            policies.update(profile.policies)
821

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

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

    
843

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

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

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

    
862
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
863
                     'automoderate')
864

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

    
871
    objects = AuthProviderPolicyProfileManager()
872

    
873
    class Meta:
874
        ordering = ['priority']
875

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

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

    
892

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

    
911
    objects = AstakosUserAuthProviderManager()
912

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

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

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

    
929
    @property
930
    def settings(self):
931
        extra_data = {}
932

    
933
        info_data = {}
934
        if self.info_data:
935
            info_data = json.loads(self.info_data)
936

    
937
        extra_data['info'] = info_data
938

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

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

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

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

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

    
960

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

    
988
    update_or_create = _update_or_create
989

    
990

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

    
997
    class Meta:
998
        unique_together = ("resource", "user")
999

    
1000

    
1001
class ApprovalTerms(models.Model):
1002
    """
1003
    Model for approval terms
1004
    """
1005

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

    
1010

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

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

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

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

    
1037

    
1038
class EmailChangeManager(models.Manager):
1039

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

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

1049
        If the key is not valid or has expired, return ``None``.
1050

1051
        If the key is valid but the ``User`` is already active,
1052
        return ``None``.
1053

1054
        After successful email change the activation record is deleted.
1055

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

    
1085

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

    
1098
    objects = EmailChangeManager()
1099

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

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

    
1108

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

    
1116

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

    
1126

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

    
1135

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

    
1155
    class Meta:
1156
        unique_together = ("provider", "third_party_identifier")
1157

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

    
1174
        return user
1175

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

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

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

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

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

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

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

    
1221

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

    
1227
    objects = ForUpdateManager()
1228

    
1229
    class Meta:
1230
        unique_together = ("user", "setting")
1231

    
1232

    
1233
### PROJECTS ###
1234
################
1235

    
1236
class ChainManager(ForUpdateManager):
1237

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

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

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

    
1254
        objs = ProjectApplication.objects.select_related('applicant')
1255
        apps = objs.in_bulk(chain_latest.values())
1256

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

    
1264
        return d
1265

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

    
1274

    
1275
class Chain(models.Model):
1276
    chain  =   models.AutoField(primary_key=True)
1277

    
1278
    def __str__(self):
1279
        return "%s" % (self.chain,)
1280

    
1281
    objects = ChainManager()
1282

    
1283
    PENDING            = 0
1284
    DENIED             = 3
1285
    DISMISSED          = 4
1286
    CANCELLED          = 5
1287

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

    
1295
    PENDING_STATES = [PENDING,
1296
                      APPROVED_PENDING,
1297
                      SUSPENDED_PENDING,
1298
                      TERMINATED_PENDING,
1299
                      ]
1300

    
1301
    MODIFICATION_STATES = [APPROVED_PENDING,
1302
                           SUSPENDED_PENDING,
1303
                           TERMINATED_PENDING,
1304
                           ]
1305

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

    
1315
    SKIP_STATES = [DISMISSED,
1316
                   CANCELLED,
1317
                   TERMINATED]
1318

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

    
1332

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

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

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

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

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

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

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

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

    
1373

    
1374
def new_chain():
1375
    c = Chain.objects.create()
1376
    return c
1377

    
1378

    
1379
class ProjectApplicationManager(ForUpdateManager):
1380

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

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

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

    
1403
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1404

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

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

    
1417

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

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

    
1431
    state                   =   models.IntegerField(default=PENDING,
1432
                                                    db_index=True)
1433

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

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

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

    
1465
    objects                 =   ProjectApplicationManager()
1466

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

    
1472
    class Meta:
1473
        unique_together = ("chain", "id")
1474

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

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

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

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

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

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

    
1509
    def members_count(self):
1510
        return self.project.approved_memberships.count()
1511

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

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

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

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

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

    
1536
    def pending_modifications(self):
1537
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1538

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

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

    
1552
    def chained_applications(self):
1553
        return ProjectApplication.objects.filter(chain=self.chain)
1554

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

    
1558
    def has_pending_modifications(self):
1559
        return bool(self.last_pending())
1560

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

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

    
1573
    def has_denied_modifications(self):
1574
        return bool(self.last_denied())
1575

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

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

    
1589
    def project_exists(self):
1590
        return self.get_project() is not None
1591

    
1592
    def can_cancel(self):
1593
        return self.state == self.PENDING
1594

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

    
1601
        self.state = self.CANCELLED
1602
        self.save()
1603

    
1604
    def can_dismiss(self):
1605
        return self.state == self.DENIED
1606

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

    
1613
        self.state = self.DISMISSED
1614
        self.save()
1615

    
1616
    def can_deny(self):
1617
        return self.state == self.PENDING
1618

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

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

    
1630
    def can_approve(self):
1631
        return self.state == self.PENDING
1632

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

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

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

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

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

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

    
1665
class ProjectResourceGrant(models.Model):
1666

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

    
1673
    objects = ExtendedManager()
1674

    
1675
    class Meta:
1676
        unique_together = ("resource", "project_application")
1677

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

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

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

    
1719

    
1720
class ProjectManager(ForUpdateManager):
1721

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

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

    
1730
    def deactivated_projects(self):
1731
        q = self.model.Q_DEACTIVATED
1732
        return self.filter(q)
1733

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

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

    
1745

    
1746
class Project(models.Model):
1747

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

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

    
1758
    members                     =   models.ManyToManyField(
1759
                                            AstakosUser,
1760
                                            through='ProjectMembership')
1761

    
1762
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1763
    deactivation_date           =   models.DateTimeField(null=True)
1764

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

    
1772
    APPROVED    = 1
1773
    SUSPENDED   = 10
1774
    TERMINATED  = 100
1775

    
1776
    state                       =   models.IntegerField(default=APPROVED,
1777
                                                        db_index=True)
1778

    
1779
    objects     =   ProjectManager()
1780

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

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

    
1790
    __repr__ = __str__
1791

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

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

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

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

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

    
1812
        return self.state != self.APPROVED
1813

    
1814
    ### Deactivation calls
1815

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

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

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

    
1835
    ### Logical checks
1836

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

    
1844
    def is_approved(self):
1845
        return self.state == self.APPROVED
1846

    
1847
    @property
1848
    def is_alive(self):
1849
        return not self.is_terminated
1850

    
1851
    @property
1852
    def is_terminated(self):
1853
        return self.is_deactivated(self.TERMINATED)
1854

    
1855
    @property
1856
    def is_suspended(self):
1857
        return self.is_deactivated(self.SUSPENDED)
1858

    
1859
    def violates_resource_grants(self):
1860
        return False
1861

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

    
1869

    
1870
    ### Other
1871

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

    
1877
    def members_count(self):
1878
        return self.approved_memberships.count()
1879

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

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

    
1889

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

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

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

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

    
1915

    
1916
class ProjectMembershipManager(ForUpdateManager):
1917

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

    
1922
    def actually_accepted(self):
1923
        q = self.model.Q_ACTUALLY_ACCEPTED
1924
        return self.filter(q)
1925

    
1926
    def requested(self):
1927
        return self.filter(state=ProjectMembership.REQUESTED)
1928

    
1929
    def suspended(self):
1930
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1931

    
1932
class ProjectMembership(models.Model):
1933

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

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

    
1944
    REMOVED             =   200
1945

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

    
1952
    ACCEPTED_STATES     =   set([ACCEPTED,
1953
                                 LEAVE_REQUESTED,
1954
                                 USER_SUSPENDED,
1955
                                 ])
1956

    
1957
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
1958

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

    
1964
    objects     =   ProjectMembershipManager()
1965

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

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

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

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

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

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

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

    
2000
    __repr__ = __str__
2001

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

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

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

    
2019
    def can_accept(self):
2020
        return self.state == self.REQUESTED
2021

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

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

    
2033
    def can_leave(self):
2034
        return self.state in self.ACCEPTED_STATES
2035

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

    
2042
        self.leave_request_date = datetime.now()
2043
        self.state = self.LEAVE_REQUESTED
2044
        self.save()
2045

    
2046
    def can_deny_leave(self):
2047
        return self.state == self.LEAVE_REQUESTED
2048

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

    
2055
        self.leave_request_date = None
2056
        self.state = self.ACCEPTED
2057
        self.save()
2058

    
2059
    def can_cancel_leave(self):
2060
        return self.state == self.LEAVE_REQUESTED
2061

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

    
2068
        self.leave_request_date = None
2069
        self.state = self.ACCEPTED
2070
        self.save()
2071

    
2072
    def can_remove(self):
2073
        return self.state in self.ACCEPTED_STATES
2074

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

    
2080
        self._set_history_item(reason='REMOVE')
2081
        self.delete()
2082

    
2083
    def can_reject(self):
2084
        return self.state == self.REQUESTED
2085

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

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

    
2096
    def can_cancel(self):
2097
        return self.state == self.REQUESTED
2098

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

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

    
2109

    
2110
class Serial(models.Model):
2111
    serial  =   models.AutoField(primary_key=True)
2112

    
2113

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

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

    
2124
### SIGNALS ###
2125
################
2126

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

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

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

    
2151
def astakosuser_post_save(sender, instance, created, **kwargs):
2152
    pass
2153

    
2154
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2155

    
2156
def resource_post_save(sender, instance, created, **kwargs):
2157
    pass
2158

    
2159
post_save.connect(resource_post_save, sender=Resource)
2160

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