Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 9d9af77d

History | View | Annotate | Download (74.7 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
import time
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
def generate_token(*args):
97
    md5 = hashlib.md5()
98
    md5.update(settings.SECRET_KEY)
99
    for arg in args:
100
        md5.update(arg)
101
    md5.update("%.15f" % time.time())
102
    return b64encode(md5.digest())
103

    
104

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

    
117
    def renew_token(self, expiration_date=None):
118
        for i in range(10):
119
            data = (self.name.encode('ascii', 'ignore'),)
120
            if self.url is not None:
121
                data += (self.url.encode('ascii', 'ignore'),)
122
            new_token = generate_token(*data)
123
            count = Component.objects.filter(auth_token=new_token).count()
124
            if count == 0:
125
                break
126
            continue
127
        else:
128
            raise ValueError('Could not generate a token')
129

    
130
        self.auth_token = new_token
131
        self.auth_token_created = datetime.now()
132
        if expiration_date:
133
            self.auth_token_expires = expiration_date
134
        else:
135
            self.auth_token_expires = None
136
        msg = 'Token renewed for component %s' % self.name
137
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
138

    
139
    def __str__(self):
140
        return self.name
141

    
142
    @classmethod
143
    def catalog(cls, orderfor=None):
144
        catalog = {}
145
        components = list(cls.objects.all())
146
        default_metadata = presentation.COMPONENTS
147
        metadata = {}
148

    
149
        for component in components:
150
            d = {'url': component.url,
151
                 'name': component.name}
152
            if component.name in default_metadata:
153
                metadata[component.name] = default_metadata.get(component.name)
154
                metadata[component.name].update(d)
155
            else:
156
                metadata[component.name] = d
157

    
158

    
159
        def component_by_order(s):
160
            return s[1].get('order')
161

    
162
        def component_by_dashboard_order(s):
163
            return s[1].get('dashboard').get('order')
164

    
165
        metadata = dict_merge(metadata,
166
                              astakos_settings.COMPONENTS_META)
167

    
168
        for component, info in metadata.iteritems():
169
            default_meta = presentation.component_defaults(component)
170
            base_meta = metadata.get(component, {})
171
            settings_meta = astakos_settings.COMPONENTS_META.get(component, {})
172
            component_meta = dict_merge(default_meta, base_meta)
173
            meta = dict_merge(component_meta, settings_meta)
174
            catalog[component] = meta
175

    
176
        order_key = component_by_order
177
        if orderfor == 'dashboard':
178
            order_key = component_by_dashboard_order
179

    
180
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
181
                                             key=order_key))
182
        return ordered_catalog
183

    
184

    
185
_presentation_data = {}
186

    
187

    
188
def get_presentation(resource):
189
    global _presentation_data
190
    resource_presentation = _presentation_data.get(resource, {})
191
    if not resource_presentation:
192
        resources_presentation = presentation.RESOURCES.get('resources', {})
193
        resource_presentation = resources_presentation.get(resource, {})
194
        _presentation_data[resource] = resource_presentation
195
    return resource_presentation
196

    
197

    
198
class Service(models.Model):
199
    component = models.ForeignKey(Component)
200
    name = models.CharField(max_length=255, unique=True)
201
    type = models.CharField(max_length=255)
202

    
203

    
204
class Endpoint(models.Model):
205
    service = models.ForeignKey(Service, related_name='endpoints')
206

    
207

    
208
class EndpointData(models.Model):
209
    endpoint = models.ForeignKey(Endpoint, related_name='data')
210
    key = models.CharField(max_length=255)
211
    value = models.CharField(max_length=1024)
212

    
213
    class Meta:
214
        unique_together = (('endpoint', 'key'),)
215

    
216

    
217
class Resource(models.Model):
218
    name = models.CharField(_('Name'), max_length=255, unique=True)
219
    desc = models.TextField(_('Description'), null=True)
220
    service_type = models.CharField(_('Type'), max_length=255)
221
    unit = models.CharField(_('Unit'), null=True, max_length=255)
222
    uplimit = intDecimalField(default=0)
223
    allow_in_projects = models.BooleanField(default=True)
224

    
225
    objects = ForUpdateManager()
226

    
227
    def __str__(self):
228
        return self.name
229

    
230
    def full_name(self):
231
        return str(self)
232

    
233
    def get_info(self):
234
        return {'service_type': self.service_type,
235
                'description': self.desc,
236
                'unit': self.unit,
237
                'allow_in_projects': self.allow_in_projects,
238
                }
239

    
240
    @property
241
    def group(self):
242
        default = self.name
243
        return get_presentation(str(self)).get('group', default)
244

    
245
    @property
246
    def help_text(self):
247
        default = "%s resource" % self.name
248
        return get_presentation(str(self)).get('help_text', default)
249

    
250
    @property
251
    def help_text_input_each(self):
252
        default = "%s resource" % self.name
253
        return get_presentation(str(self)).get('help_text_input_each', default)
254

    
255
    @property
256
    def is_abbreviation(self):
257
        return get_presentation(str(self)).get('is_abbreviation', False)
258

    
259
    @property
260
    def report_desc(self):
261
        default = "%s resource" % self.name
262
        return get_presentation(str(self)).get('report_desc', default)
263

    
264
    @property
265
    def placeholder(self):
266
        return get_presentation(str(self)).get('placeholder', self.unit)
267

    
268
    @property
269
    def verbose_name(self):
270
        return get_presentation(str(self)).get('verbose_name', self.name)
271

    
272
    @property
273
    def display_name(self):
274
        name = self.verbose_name
275
        if self.is_abbreviation:
276
            name = name.upper()
277
        return name
278

    
279
    @property
280
    def pluralized_display_name(self):
281
        if not self.unit:
282
            return '%ss' % self.display_name
283
        return self.display_name
284

    
285
def get_resource_names():
286
    _RESOURCE_NAMES = []
287
    resources = Resource.objects.select_related('service').all()
288
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
289
    return _RESOURCE_NAMES
290

    
291

    
292
class AstakosUserManager(UserManager):
293

    
294
    def get_auth_provider_user(self, provider, **kwargs):
295
        """
296
        Retrieve AstakosUser instance associated with the specified third party
297
        id.
298
        """
299
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
300
                          kwargs.iteritems()))
301
        return self.get(auth_providers__module=provider, **kwargs)
302

    
303
    def get_by_email(self, email):
304
        return self.get(email=email)
305

    
306
    def get_by_identifier(self, email_or_username, **kwargs):
307
        try:
308
            return self.get(email__iexact=email_or_username, **kwargs)
309
        except AstakosUser.DoesNotExist:
310
            return self.get(username__iexact=email_or_username, **kwargs)
311

    
312
    def user_exists(self, email_or_username, **kwargs):
313
        qemail = Q(email__iexact=email_or_username)
314
        qusername = Q(username__iexact=email_or_username)
315
        qextra = Q(**kwargs)
316
        return self.filter((qemail | qusername) & qextra).exists()
317

    
318
    def verified_user_exists(self, email_or_username):
319
        return self.user_exists(email_or_username, email_verified=True)
320

    
321
    def verified(self):
322
        return self.filter(email_verified=True)
323

    
324
    def uuid_catalog(self, l=None):
325
        """
326
        Returns a uuid to username mapping for the uuids appearing in l.
327
        If l is None returns the mapping for all existing users.
328
        """
329
        q = self.filter(uuid__in=l) if l != None else self
330
        return dict(q.values_list('uuid', 'username'))
331

    
332
    def displayname_catalog(self, l=None):
333
        """
334
        Returns a username to uuid mapping for the usernames appearing in l.
335
        If l is None returns the mapping for all existing users.
336
        """
337
        if l is not None:
338
            lmap = dict((x.lower(), x) for x in l)
339
            q = self.filter(username__in=lmap.keys())
340
            values = ((lmap[n], u) for n, u in q.values_list('username', 'uuid'))
341
        else:
342
            q = self
343
            values = self.values_list('username', 'uuid')
344
        return dict(values)
345

    
346

    
347

    
348
class AstakosUser(User):
349
    """
350
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
351
    """
352
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
353
                                   null=True)
354

    
355
    #for invitations
356
    user_level = astakos_settings.DEFAULT_USER_LEVEL
357
    level = models.IntegerField(_('Inviter level'), default=user_level)
358
    invitations = models.IntegerField(
359
        _('Invitations left'), default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
360

    
361
    auth_token = models.CharField(_('Authentication Token'),
362
                                  max_length=32,
363
                                  unique=True,
364
                                  null=True,
365
                                  blank=True,
366
                                  help_text = _('Renew your authentication '
367
                                                'token. Make sure to set the new '
368
                                                'token in any client you may be '
369
                                                'using, to preserve its '
370
                                                'functionality.'))
371
    auth_token_created = models.DateTimeField(_('Token creation date'),
372
                                              null=True)
373
    auth_token_expires = models.DateTimeField(
374
        _('Token expiration date'), null=True)
375

    
376
    updated = models.DateTimeField(_('Update date'))
377

    
378
    # Arbitrary text to identify the reason user got deactivated.
379
    # To be used as a reference from administrators.
380
    deactivated_reason = models.TextField(
381
        _('Reason the user was disabled for'),
382
        default=None, null=True)
383
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
384
                                          blank=True)
385

    
386
    has_credits = models.BooleanField(_('Has credits?'), default=False)
387

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

    
391
    # user email is verified
392
    email_verified = models.BooleanField(_('Email verified?'), default=False)
393

    
394
    # unique string used in user email verification url
395
    verification_code = models.CharField(max_length=255, null=True,
396
                                         blank=False, unique=True)
397

    
398
    # date user email verified
399
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
400
                                       blank=True)
401

    
402
    # email verification notice was sent to the user at this time
403
    activation_sent = models.DateTimeField(_('Activation sent date'),
404
                                           null=True, blank=True)
405

    
406
    # user got rejected during moderation process
407
    is_rejected = models.BooleanField(_('Account rejected'),
408
                                      default=False)
409
    # reason user got rejected
410
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
411
                                       blank=True)
412
    # moderation status
413
    moderated = models.BooleanField(_('User moderated'), default=False)
414
    # date user moderated (either accepted or rejected)
415
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
416
                                        blank=True, null=True)
417
    # a snapshot of user instance the time got moderated
418
    moderated_data = models.TextField(null=True, default=None, blank=True)
419
    # a string which identifies how the user got moderated
420
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
421
                                       default=None, null=True, blank=True)
422
    # the email used to accept the user
423
    accepted_email = models.EmailField(null=True, default=None, blank=True)
424

    
425
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
426
                                           default=False)
427
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
428
                                             null=True, blank=True)
429
    # permanent unique user identifier
430
    uuid = models.CharField(max_length=255, null=True, blank=False,
431
                            unique=True)
432

    
433
    policy = models.ManyToManyField(
434
        Resource, null=True, through='AstakosUserQuota')
435

    
436
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
437
                                           default=False, db_index=True)
438

    
439
    objects = AstakosUserManager()
440
    forupdate = ForUpdateManager()
441

    
442
    def __init__(self, *args, **kwargs):
443
        super(AstakosUser, self).__init__(*args, **kwargs)
444
        if not self.id:
445
            self.is_active = False
446

    
447
    @property
448
    def realname(self):
449
        return '%s %s' % (self.first_name, self.last_name)
450

    
451
    @property
452
    def log_display(self):
453
        """
454
        Should be used in all logger.* calls that refer to a user so that
455
        user display is consistent across log entries.
456
        """
457
        return '%s::%s' % (self.uuid, self.email)
458

    
459
    @realname.setter
460
    def realname(self, value):
461
        parts = value.split(' ')
462
        if len(parts) == 2:
463
            self.first_name = parts[0]
464
            self.last_name = parts[1]
465
        else:
466
            self.last_name = parts[0]
467

    
468
    def add_permission(self, pname):
469
        if self.has_perm(pname):
470
            return
471
        p, created = Permission.objects.get_or_create(
472
                                    codename=pname,
473
                                    name=pname.capitalize(),
474
                                    content_type=get_content_type())
475
        self.user_permissions.add(p)
476

    
477
    def remove_permission(self, pname):
478
        if self.has_perm(pname):
479
            return
480
        p = Permission.objects.get(codename=pname,
481
                                   content_type=get_content_type())
482
        self.user_permissions.remove(p)
483

    
484
    def add_group(self, gname):
485
        group, _ = Group.objects.get_or_create(name=gname)
486
        self.groups.add(group)
487

    
488
    def is_project_admin(self, application_id=None):
489
        return self.uuid in astakos_settings.PROJECT_ADMINS
490

    
491
    @property
492
    def invitation(self):
493
        try:
494
            return Invitation.objects.get(username=self.email)
495
        except Invitation.DoesNotExist:
496
            return None
497

    
498
    @property
499
    def policies(self):
500
        return self.astakosuserquota_set.select_related().all()
501

    
502
    def get_resource_policy(self, resource):
503
        resource = Resource.objects.get(name=resource)
504
        default_capacity = resource.uplimit
505
        try:
506
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
507
            return policy, default_capacity
508
        except AstakosUserQuota.DoesNotExist:
509
            return None, default_capacity
510

    
511
    def update_uuid(self):
512
        while not self.uuid:
513
            uuid_val = str(uuid.uuid4())
514
            try:
515
                AstakosUser.objects.get(uuid=uuid_val)
516
            except AstakosUser.DoesNotExist, e:
517
                self.uuid = uuid_val
518
        return self.uuid
519

    
520
    def save(self, update_timestamps=True, **kwargs):
521
        if update_timestamps:
522
            if not self.id:
523
                self.date_joined = datetime.now()
524
            self.updated = datetime.now()
525

    
526
        self.update_uuid()
527

    
528
        if not self.verification_code:
529
            self.renew_verification_code()
530

    
531
        # username currently matches email
532
        if self.username != self.email.lower():
533
            self.username = self.email.lower()
534

    
535
        super(AstakosUser, self).save(**kwargs)
536

    
537
    def renew_verification_code(self):
538
        self.verification_code = str(uuid.uuid4())
539
        logger.info("Verification code renewed for %s" % self.log_display)
540

    
541
    def renew_token(self, flush_sessions=False, current_key=None):
542
        for i in range(10):
543
            data = (self.username, self.realname.encode('ascii', 'ignore'))
544
            new_token = generate_token(*data)
545
            count = AstakosUser.objects.filter(auth_token=new_token).count()
546
            if count == 0:
547
                break
548
            continue
549
        else:
550
            raise ValueError('Could not generate a token')
551

    
552
        self.auth_token = new_token
553
        self.auth_token_created = datetime.now()
554
        self.auth_token_expires = self.auth_token_created + \
555
                                  timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
556
        if flush_sessions:
557
            self.flush_sessions(current_key)
558
        msg = 'Token renewed for %s' % self.log_display
559
        logger.log(astakos_settings.LOGGING_LEVEL, msg)
560

    
561
    def token_expired(self):
562
        return self.auth_token_expires < datetime.now()
563

    
564
    def flush_sessions(self, current_key=None):
565
        q = self.sessions
566
        if current_key:
567
            q = q.exclude(session_key=current_key)
568

    
569
        keys = q.values_list('session_key', flat=True)
570
        if keys:
571
            msg = 'Flushing sessions: %s' % ','.join(keys)
572
            logger.log(astakos_settings.LOGGING_LEVEL, msg, [])
573
        engine = import_module(settings.SESSION_ENGINE)
574
        for k in keys:
575
            s = engine.SessionStore(k)
576
            s.flush()
577

    
578
    def __unicode__(self):
579
        return '%s (%s)' % (self.realname, self.email)
580

    
581
    def conflicting_email(self):
582
        q = AstakosUser.objects.exclude(username=self.username)
583
        q = q.filter(email__iexact=self.email)
584
        if q.count() != 0:
585
            return True
586
        return False
587

    
588
    def email_change_is_pending(self):
589
        return self.emailchanges.count() > 0
590

    
591
    @property
592
    def status_display(self):
593
        msg = ""
594
        append = None
595
        if self.is_active:
596
            msg = "Accepted/Active"
597
        if self.is_rejected:
598
            msg = "Rejected"
599
            if self.rejected_reason:
600
                msg += " (%s)" % self.rejected_reason
601
        if not self.email_verified:
602
            msg = "Pending email verification"
603
        if not self.moderated:
604
            msg = "Pending moderation"
605
        if not self.is_active and self.email_verified:
606
            msg = "Accepted/Inactive"
607
            if self.deactivated_reason:
608
                msg += " (%s)" % (self.deactivated_reason)
609

    
610
        if self.moderated and not self.is_rejected:
611
            if self.accepted_policy == 'manual':
612
                msg += " (manually accepted)"
613
            else:
614
                msg += " (accepted policy: %s)" % \
615
                        self.accepted_policy
616
        return msg
617

    
618
    @property
619
    def signed_terms(self):
620
        term = get_latest_terms()
621
        if not term:
622
            return True
623
        if not self.has_signed_terms:
624
            return False
625
        if not self.date_signed_terms:
626
            return False
627
        if self.date_signed_terms < term.date:
628
            self.has_signed_terms = False
629
            self.date_signed_terms = None
630
            self.save()
631
            return False
632
        return True
633

    
634
    def set_invitations_level(self):
635
        """
636
        Update user invitation level
637
        """
638
        level = self.invitation.inviter.level + 1
639
        self.level = level
640
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
641

    
642
    def can_change_password(self):
643
        return self.has_auth_provider('local', auth_backend='astakos')
644

    
645
    def can_change_email(self):
646
        if not self.has_auth_provider('local'):
647
            return True
648

    
649
        local = self.get_auth_provider('local')._instance
650
        return local.auth_backend == 'astakos'
651

    
652
    # Auth providers related methods
653
    def get_auth_provider(self, module=None, identifier=None, **filters):
654
        if not module:
655
            return self.auth_providers.active()[0].settings
656

    
657
        params = {'module': module}
658
        if identifier:
659
            params['identifier'] = identifier
660
        params.update(filters)
661
        return self.auth_providers.active().get(**params).settings
662

    
663
    def has_auth_provider(self, provider, **kwargs):
664
        return bool(self.auth_providers.active().filter(module=provider,
665
                                                        **kwargs).count())
666

    
667
    def get_required_providers(self, **kwargs):
668
        return auth.REQUIRED_PROVIDERS.keys()
669

    
670
    def missing_required_providers(self):
671
        required = self.get_required_providers()
672
        missing = []
673
        for provider in required:
674
            if not self.has_auth_provider(provider):
675
                missing.append(auth.get_provider(provider, self))
676
        return missing
677

    
678
    def get_available_auth_providers(self, **filters):
679
        """
680
        Returns a list of providers available for add by the user.
681
        """
682
        modules = astakos_settings.IM_MODULES
683
        providers = []
684
        for p in modules:
685
            providers.append(auth.get_provider(p, self))
686
        available = []
687

    
688
        for p in providers:
689
            if p.get_add_policy:
690
                available.append(p)
691
        return available
692

    
693
    def get_disabled_auth_providers(self, **filters):
694
        providers = self.get_auth_providers(**filters)
695
        disabled = []
696
        for p in providers:
697
            if not p.get_login_policy:
698
                disabled.append(p)
699
        return disabled
700

    
701
    def get_enabled_auth_providers(self, **filters):
702
        providers = self.get_auth_providers(**filters)
703
        enabled = []
704
        for p in providers:
705
            if p.get_login_policy:
706
                enabled.append(p)
707
        return enabled
708

    
709
    def get_auth_providers(self, **filters):
710
        providers = []
711
        for provider in self.auth_providers.active(**filters):
712
            if provider.settings.module_enabled:
713
                providers.append(provider.settings)
714

    
715
        modules = astakos_settings.IM_MODULES
716

    
717
        def key(p):
718
            if not p.module in modules:
719
                return 100
720
            return modules.index(p.module)
721

    
722
        providers = sorted(providers, key=key)
723
        return providers
724

    
725
    # URL methods
726
    @property
727
    def auth_providers_display(self):
728
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
729
                         self.get_enabled_auth_providers()])
730

    
731
    def add_auth_provider(self, module='local', identifier=None, **params):
732
        provider = auth.get_provider(module, self, identifier, **params)
733
        provider.add_to_user()
734

    
735
    def get_resend_activation_url(self):
736
        return reverse('send_activation', kwargs={'user_id': self.pk})
737

    
738
    def get_activation_url(self, nxt=False):
739
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
740
                                 quote(self.verification_code))
741
        if nxt:
742
            url += "&next=%s" % quote(nxt)
743
        return url
744

    
745
    def get_password_reset_url(self, token_generator=default_token_generator):
746
        return reverse('astakos.im.views.target.local.password_reset_confirm',
747
                          kwargs={'uidb36':int_to_base36(self.id),
748
                                  'token':token_generator.make_token(self)})
749

    
750
    def get_inactive_message(self, provider_module, identifier=None):
751
        provider = self.get_auth_provider(provider_module, identifier)
752

    
753
        msg_extra = ''
754
        message = ''
755

    
756
        msg_inactive = provider.get_account_inactive_msg
757
        msg_pending = provider.get_pending_activation_msg
758
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
759
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
760
        msg_pending_mod = provider.get_pending_moderation_msg
761
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
762
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
763

    
764
        if not self.email_verified:
765
            message = msg_pending
766
            url = self.get_resend_activation_url()
767
            msg_extra = msg_pending_help + \
768
                        u' ' + \
769
                        '<a href="%s">%s?</a>' % (url, msg_resend)
770
        else:
771
            if not self.moderated:
772
                message = msg_pending_mod
773
            else:
774
                if self.is_rejected:
775
                    message = msg_rejected
776
                else:
777
                    message = msg_inactive
778

    
779
        return mark_safe(message + u' ' + msg_extra)
780

    
781
    def owns_application(self, application):
782
        return application.owner == self
783

    
784
    def owns_project(self, project):
785
        return project.application.owner == self
786

    
787
    def is_associated(self, project):
788
        try:
789
            m = ProjectMembership.objects.get(person=self, project=project)
790
            return m.state in ProjectMembership.ASSOCIATED_STATES
791
        except ProjectMembership.DoesNotExist:
792
            return False
793

    
794
    def get_membership(self, project):
795
        try:
796
            return ProjectMembership.objects.get(
797
                project=project,
798
                person=self)
799
        except ProjectMembership.DoesNotExist:
800
            return None
801

    
802
    def membership_display(self, project):
803
        m = self.get_membership(project)
804
        if m is None:
805
            return _('Not a member')
806
        else:
807
            return m.user_friendly_state_display()
808

    
809
    def non_owner_can_view(self, maybe_project):
810
        if self.is_project_admin():
811
            return True
812
        if maybe_project is None:
813
            return False
814
        project = maybe_project
815
        if self.is_associated(project):
816
            return True
817
        if project.is_deactivated():
818
            return False
819
        return True
820

    
821

    
822
class AstakosUserAuthProviderManager(models.Manager):
823

    
824
    def active(self, **filters):
825
        return self.filter(active=True, **filters)
826

    
827
    def remove_unverified_providers(self, provider, **filters):
828
        try:
829
            existing = self.filter(module=provider, user__email_verified=False,
830
                                   **filters)
831
            for p in existing:
832
                p.user.delete()
833
        except:
834
            pass
835

    
836
    def unverified(self, provider, **filters):
837
        try:
838
            return self.get(module=provider, user__email_verified=False,
839
                            **filters).settings
840
        except AstakosUserAuthProvider.DoesNotExist:
841
            return None
842

    
843
    def verified(self, provider, **filters):
844
        try:
845
            return self.get(module=provider, user__email_verified=True,
846
                            **filters).settings
847
        except AstakosUserAuthProvider.DoesNotExist:
848
            return None
849

    
850

    
851
class AuthProviderPolicyProfileManager(models.Manager):
852

    
853
    def active(self):
854
        return self.filter(active=True)
855

    
856
    def for_user(self, user, provider):
857
        policies = {}
858
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
859
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
860
        exclusive_q = exclusive_q1 | exclusive_q2
861

    
862
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
863
            policies.update(profile.policies)
864

    
865
        user_groups = user.groups.all().values('pk')
866
        for profile in self.active().filter(groups__in=user_groups).filter(
867
                exclusive_q):
868
            policies.update(profile.policies)
869
        return policies
870

    
871
    def add_policy(self, name, provider, group_or_user, exclusive=False,
872
                   **policies):
873
        is_group = isinstance(group_or_user, Group)
874
        profile, created = self.get_or_create(name=name, provider=provider,
875
                                              is_exclusive=exclusive)
876
        profile.is_exclusive = exclusive
877
        profile.save()
878
        if is_group:
879
            profile.groups.add(group_or_user)
880
        else:
881
            profile.users.add(group_or_user)
882
        profile.set_policies(policies)
883
        profile.save()
884
        return profile
885

    
886

    
887
class AuthProviderPolicyProfile(models.Model):
888
    name = models.CharField(_('Name'), max_length=255, blank=False,
889
                            null=False, db_index=True)
890
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
891
                                null=False)
892

    
893
    # apply policies to all providers excluding the one set in provider field
894
    is_exclusive = models.BooleanField(default=False)
895

    
896
    policy_add = models.NullBooleanField(null=True, default=None)
897
    policy_remove = models.NullBooleanField(null=True, default=None)
898
    policy_create = models.NullBooleanField(null=True, default=None)
899
    policy_login = models.NullBooleanField(null=True, default=None)
900
    policy_limit = models.IntegerField(null=True, default=None)
901
    policy_required = models.NullBooleanField(null=True, default=None)
902
    policy_automoderate = models.NullBooleanField(null=True, default=None)
903
    policy_switch = models.NullBooleanField(null=True, default=None)
904

    
905
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
906
                     'automoderate')
907

    
908
    priority = models.IntegerField(null=False, default=1)
909
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
910
    users = models.ManyToManyField(AstakosUser,
911
                                   related_name='authpolicy_profiles')
912
    active = models.BooleanField(default=True)
913

    
914
    objects = AuthProviderPolicyProfileManager()
915

    
916
    class Meta:
917
        ordering = ['priority']
918

    
919
    @property
920
    def policies(self):
921
        policies = {}
922
        for pkey in self.POLICY_FIELDS:
923
            value = getattr(self, 'policy_%s' % pkey, None)
924
            if value is None:
925
                continue
926
            policies[pkey] = value
927
        return policies
928

    
929
    def set_policies(self, policies_dict):
930
        for key, value in policies_dict.iteritems():
931
            if key in self.POLICY_FIELDS:
932
                setattr(self, 'policy_%s' % key, value)
933
        return self.policies
934

    
935

    
936
class AstakosUserAuthProvider(models.Model):
937
    """
938
    Available user authentication methods.
939
    """
940
    affiliation = models.CharField(_('Affiliation'), max_length=255, blank=True,
941
                                   null=True, default=None)
942
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
943
    module = models.CharField(_('Provider'), max_length=255, blank=False,
944
                                default='local')
945
    identifier = models.CharField(_('Third-party identifier'),
946
                                              max_length=255, null=True,
947
                                              blank=True)
948
    active = models.BooleanField(default=True)
949
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
950
                                   default='astakos')
951
    info_data = models.TextField(default="", null=True, blank=True)
952
    created = models.DateTimeField('Creation date', auto_now_add=True)
953

    
954
    objects = AstakosUserAuthProviderManager()
955

    
956
    class Meta:
957
        unique_together = (('identifier', 'module', 'user'), )
958
        ordering = ('module', 'created')
959

    
960
    def __init__(self, *args, **kwargs):
961
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
962
        try:
963
            self.info = json.loads(self.info_data)
964
            if not self.info:
965
                self.info = {}
966
        except Exception, e:
967
            self.info = {}
968

    
969
        for key,value in self.info.iteritems():
970
            setattr(self, 'info_%s' % key, value)
971

    
972
    @property
973
    def settings(self):
974
        extra_data = {}
975

    
976
        info_data = {}
977
        if self.info_data:
978
            info_data = json.loads(self.info_data)
979

    
980
        extra_data['info'] = info_data
981

    
982
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
983
            extra_data[key] = getattr(self, key)
984

    
985
        extra_data['instance'] = self
986
        return auth.get_provider(self.module, self.user,
987
                                           self.identifier, **extra_data)
988

    
989
    def __repr__(self):
990
        return '<AstakosUserAuthProvider %s:%s>' % (self.module, self.identifier)
991

    
992
    def __unicode__(self):
993
        if self.identifier:
994
            return "%s:%s" % (self.module, self.identifier)
995
        if self.auth_backend:
996
            return "%s:%s" % (self.module, self.auth_backend)
997
        return self.module
998

    
999
    def save(self, *args, **kwargs):
1000
        self.info_data = json.dumps(self.info)
1001
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1002

    
1003

    
1004
class ExtendedManager(models.Manager):
1005
    def _update_or_create(self, **kwargs):
1006
        assert kwargs, \
1007
            'update_or_create() must be passed at least one keyword argument'
1008
        obj, created = self.get_or_create(**kwargs)
1009
        defaults = kwargs.pop('defaults', {})
1010
        if created:
1011
            return obj, True, False
1012
        else:
1013
            try:
1014
                params = dict(
1015
                    [(k, v) for k, v in kwargs.items() if '__' not in k])
1016
                params.update(defaults)
1017
                for attr, val in params.items():
1018
                    if hasattr(obj, attr):
1019
                        setattr(obj, attr, val)
1020
                sid = transaction.savepoint()
1021
                obj.save(force_update=True)
1022
                transaction.savepoint_commit(sid)
1023
                return obj, False, True
1024
            except IntegrityError, e:
1025
                transaction.savepoint_rollback(sid)
1026
                try:
1027
                    return self.get(**kwargs), False, False
1028
                except self.model.DoesNotExist:
1029
                    raise e
1030

    
1031
    update_or_create = _update_or_create
1032

    
1033

    
1034
class AstakosUserQuota(models.Model):
1035
    objects = ExtendedManager()
1036
    capacity = intDecimalField()
1037
    resource = models.ForeignKey(Resource)
1038
    user = models.ForeignKey(AstakosUser)
1039

    
1040
    class Meta:
1041
        unique_together = ("resource", "user")
1042

    
1043

    
1044
class ApprovalTerms(models.Model):
1045
    """
1046
    Model for approval terms
1047
    """
1048

    
1049
    date = models.DateTimeField(
1050
        _('Issue date'), db_index=True, auto_now_add=True)
1051
    location = models.CharField(_('Terms location'), max_length=255)
1052

    
1053

    
1054
class Invitation(models.Model):
1055
    """
1056
    Model for registring invitations
1057
    """
1058
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1059
                                null=True)
1060
    realname = models.CharField(_('Real name'), max_length=255)
1061
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1062
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1063
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1064
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1065
    consumed = models.DateTimeField(_('Consumption date'), null=True, blank=True)
1066

    
1067
    def __init__(self, *args, **kwargs):
1068
        super(Invitation, self).__init__(*args, **kwargs)
1069
        if not self.id:
1070
            self.code = _generate_invitation_code()
1071

    
1072
    def consume(self):
1073
        self.is_consumed = True
1074
        self.consumed = datetime.now()
1075
        self.save()
1076

    
1077
    def __unicode__(self):
1078
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1079

    
1080

    
1081
class EmailChangeManager(models.Manager):
1082

    
1083
    @transaction.commit_on_success
1084
    def change_email(self, activation_key):
1085
        """
1086
        Validate an activation key and change the corresponding
1087
        ``User`` if valid.
1088

1089
        If the key is valid and has not expired, return the ``User``
1090
        after activating.
1091

1092
        If the key is not valid or has expired, return ``None``.
1093

1094
        If the key is valid but the ``User`` is already active,
1095
        return ``None``.
1096

1097
        After successful email change the activation record is deleted.
1098

1099
        Throws ValueError if there is already
1100
        """
1101
        try:
1102
            email_change = self.model.objects.get(
1103
                activation_key=activation_key)
1104
            if email_change.activation_key_expired():
1105
                email_change.delete()
1106
                raise EmailChange.DoesNotExist
1107
            # is there an active user with this address?
1108
            try:
1109
                AstakosUser.objects.get(email__iexact=email_change.new_email_address)
1110
            except AstakosUser.DoesNotExist:
1111
                pass
1112
            else:
1113
                raise ValueError(_('The new email address is reserved.'))
1114
            # update user
1115
            user = AstakosUser.objects.get(pk=email_change.user_id)
1116
            old_email = user.email
1117
            user.email = email_change.new_email_address
1118
            user.save()
1119
            email_change.delete()
1120
            msg = "User %s changed email from %s to %s" % (user.log_display,
1121
                                                           old_email,
1122
                                                           user.email)
1123
            logger.log(astakos_settings.LOGGING_LEVEL, msg)
1124
            return user
1125
        except EmailChange.DoesNotExist:
1126
            raise ValueError(_('Invalid activation key.'))
1127

    
1128

    
1129
class EmailChange(models.Model):
1130
    new_email_address = models.EmailField(
1131
        _(u'new e-mail address'),
1132
        help_text=_('Provide a new email address. Until you verify the new '
1133
                    'address by following the activation link that will be '
1134
                    'sent to it, your old email address will remain active.'))
1135
    user = models.ForeignKey(
1136
        AstakosUser, unique=True, related_name='emailchanges')
1137
    requested_at = models.DateTimeField(auto_now_add=True)
1138
    activation_key = models.CharField(
1139
        max_length=40, unique=True, db_index=True)
1140

    
1141
    objects = EmailChangeManager()
1142

    
1143
    def get_url(self):
1144
        return reverse('email_change_confirm',
1145
                      kwargs={'activation_key': self.activation_key})
1146

    
1147
    def activation_key_expired(self):
1148
        expiration_date = timedelta(days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1149
        return self.requested_at + expiration_date < datetime.now()
1150

    
1151

    
1152
class AdditionalMail(models.Model):
1153
    """
1154
    Model for registring invitations
1155
    """
1156
    owner = models.ForeignKey(AstakosUser)
1157
    email = models.EmailField()
1158

    
1159

    
1160
def _generate_invitation_code():
1161
    while True:
1162
        code = randint(1, 2L ** 63 - 1)
1163
        try:
1164
            Invitation.objects.get(code=code)
1165
            # An invitation with this code already exists, try again
1166
        except Invitation.DoesNotExist:
1167
            return code
1168

    
1169

    
1170
def get_latest_terms():
1171
    try:
1172
        term = ApprovalTerms.objects.order_by('-id')[0]
1173
        return term
1174
    except IndexError:
1175
        pass
1176
    return None
1177

    
1178

    
1179
class PendingThirdPartyUser(models.Model):
1180
    """
1181
    Model for registring successful third party user authentications
1182
    """
1183
    third_party_identifier = models.CharField(_('Third-party identifier'), max_length=255, null=True, blank=True)
1184
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1185
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1186
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1187
                                  null=True)
1188
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1189
                                 null=True)
1190
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1191
                                   null=True)
1192
    username = models.CharField(_('username'), max_length=30, unique=True,
1193
                                help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
1194
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1195
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1196
    info = models.TextField(default="", null=True, blank=True)
1197

    
1198
    class Meta:
1199
        unique_together = ("provider", "third_party_identifier")
1200

    
1201
    def get_user_instance(self):
1202
        """
1203
        Create a new AstakosUser instance based on details provided when user
1204
        initially signed up.
1205
        """
1206
        d = copy.copy(self.__dict__)
1207
        d.pop('_state', None)
1208
        d.pop('id', None)
1209
        d.pop('token', None)
1210
        d.pop('created', None)
1211
        d.pop('info', None)
1212
        d.pop('affiliation', None)
1213
        d.pop('provider', None)
1214
        d.pop('third_party_identifier', None)
1215
        user = AstakosUser(**d)
1216

    
1217
        return user
1218

    
1219
    @property
1220
    def realname(self):
1221
        return '%s %s' %(self.first_name, self.last_name)
1222

    
1223
    @realname.setter
1224
    def realname(self, value):
1225
        parts = value.split(' ')
1226
        if len(parts) == 2:
1227
            self.first_name = parts[0]
1228
            self.last_name = parts[1]
1229
        else:
1230
            self.last_name = parts[0]
1231

    
1232
    def save(self, **kwargs):
1233
        if not self.id:
1234
            # set username
1235
            while not self.username:
1236
                username =  uuid.uuid4().hex[:30]
1237
                try:
1238
                    AstakosUser.objects.get(username = username)
1239
                except AstakosUser.DoesNotExist, e:
1240
                    self.username = username
1241
        super(PendingThirdPartyUser, self).save(**kwargs)
1242

    
1243
    def generate_token(self):
1244
        self.password = self.third_party_identifier
1245
        self.last_login = datetime.now()
1246
        self.token = default_token_generator.make_token(self)
1247

    
1248
    def existing_user(self):
1249
        return AstakosUser.objects.filter(auth_providers__module=self.provider,
1250
                                         auth_providers__identifier=self.third_party_identifier)
1251

    
1252
    def get_provider(self, user):
1253
        params = {
1254
            'info_data': self.info,
1255
            'affiliation': self.affiliation
1256
        }
1257
        return auth.get_provider(self.provider, user,
1258
                                 self.third_party_identifier, **params)
1259

    
1260
class SessionCatalog(models.Model):
1261
    session_key = models.CharField(_('session key'), max_length=40)
1262
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1263

    
1264

    
1265
class UserSetting(models.Model):
1266
    user = models.ForeignKey(AstakosUser)
1267
    setting = models.CharField(max_length=255)
1268
    value = models.IntegerField()
1269

    
1270
    objects = ForUpdateManager()
1271

    
1272
    class Meta:
1273
        unique_together = ("user", "setting")
1274

    
1275

    
1276
### PROJECTS ###
1277
################
1278

    
1279
class ChainManager(ForUpdateManager):
1280

    
1281
    def search_by_name(self, *search_strings):
1282
        projects = Project.objects.search_by_name(*search_strings)
1283
        chains = [p.id for p in projects]
1284
        apps  = ProjectApplication.objects.search_by_name(*search_strings)
1285
        apps = (app for app in apps if app.is_latest())
1286
        app_chains = [app.chain for app in apps if app.chain not in chains]
1287
        return chains + app_chains
1288

    
1289
    def all_full_state(self):
1290
        chains = self.all()
1291
        cids = [c.chain for c in chains]
1292
        projects = Project.objects.select_related('application').in_bulk(cids)
1293

    
1294
        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
1295
        chain_latest = dict(objs.values_list('chain', 'latest'))
1296

    
1297
        objs = ProjectApplication.objects.select_related('applicant')
1298
        apps = objs.in_bulk(chain_latest.values())
1299

    
1300
        d = {}
1301
        for chain in chains:
1302
            pk = chain.pk
1303
            project = projects.get(pk, None)
1304
            app = apps[chain_latest[pk]]
1305
            d[chain.pk] = chain.get_state(project, app)
1306

    
1307
        return d
1308

    
1309
    def of_project(self, project):
1310
        if project is None:
1311
            return None
1312
        try:
1313
            return self.get(chain=project.id)
1314
        except Chain.DoesNotExist:
1315
            raise AssertionError('project with no chain')
1316

    
1317

    
1318
class Chain(models.Model):
1319
    chain  =   models.AutoField(primary_key=True)
1320

    
1321
    def __str__(self):
1322
        return "%s" % (self.chain,)
1323

    
1324
    objects = ChainManager()
1325

    
1326
    PENDING            = 0
1327
    DENIED             = 3
1328
    DISMISSED          = 4
1329
    CANCELLED          = 5
1330

    
1331
    APPROVED           = 10
1332
    APPROVED_PENDING   = 11
1333
    SUSPENDED          = 12
1334
    SUSPENDED_PENDING  = 13
1335
    TERMINATED         = 14
1336
    TERMINATED_PENDING = 15
1337

    
1338
    PENDING_STATES = [PENDING,
1339
                      APPROVED_PENDING,
1340
                      SUSPENDED_PENDING,
1341
                      TERMINATED_PENDING,
1342
                      ]
1343

    
1344
    MODIFICATION_STATES = [APPROVED_PENDING,
1345
                           SUSPENDED_PENDING,
1346
                           TERMINATED_PENDING,
1347
                           ]
1348

    
1349
    RELEVANT_STATES = [PENDING,
1350
                       DENIED,
1351
                       APPROVED,
1352
                       APPROVED_PENDING,
1353
                       SUSPENDED,
1354
                       SUSPENDED_PENDING,
1355
                       TERMINATED_PENDING,
1356
                       ]
1357

    
1358
    SKIP_STATES = [DISMISSED,
1359
                   CANCELLED,
1360
                   TERMINATED]
1361

    
1362
    STATE_DISPLAY = {
1363
        PENDING            : _("Pending"),
1364
        DENIED             : _("Denied"),
1365
        DISMISSED          : _("Dismissed"),
1366
        CANCELLED          : _("Cancelled"),
1367
        APPROVED           : _("Active"),
1368
        APPROVED_PENDING   : _("Active - Pending"),
1369
        SUSPENDED          : _("Suspended"),
1370
        SUSPENDED_PENDING  : _("Suspended - Pending"),
1371
        TERMINATED         : _("Terminated"),
1372
        TERMINATED_PENDING : _("Terminated - Pending"),
1373
        }
1374

    
1375

    
1376
    @classmethod
1377
    def _chain_state(cls, project_state, app_state):
1378
        s = CHAIN_STATE.get((project_state, app_state), None)
1379
        if s is None:
1380
            raise AssertionError('inconsistent chain state')
1381
        return s
1382

    
1383
    @classmethod
1384
    def chain_state(cls, project, app):
1385
        p_state = project.state if project else None
1386
        return cls._chain_state(p_state, app.state)
1387

    
1388
    @classmethod
1389
    def state_display(cls, s):
1390
        if s is None:
1391
            return _("Unknown")
1392
        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
1393

    
1394
    def last_application(self):
1395
        return self.chained_apps.order_by('-id')[0]
1396

    
1397
    def get_project(self):
1398
        try:
1399
            return self.chained_project
1400
        except Project.DoesNotExist:
1401
            return None
1402

    
1403
    def get_elements(self):
1404
        project = self.get_project()
1405
        app = self.last_application()
1406
        return project, app
1407

    
1408
    def get_state(self, project, app):
1409
        s = self.chain_state(project, app)
1410
        return s, project, app
1411

    
1412
    def full_state(self):
1413
        project, app = self.get_elements()
1414
        return self.get_state(project, app)
1415

    
1416

    
1417
def new_chain():
1418
    c = Chain.objects.create()
1419
    return c
1420

    
1421

    
1422
class ProjectApplicationManager(ForUpdateManager):
1423

    
1424
    def user_visible_projects(self, *filters, **kw_filters):
1425
        model = self.model
1426
        return self.filter(model.Q_PENDING | model.Q_APPROVED)
1427

    
1428
    def user_visible_by_chain(self, flt):
1429
        model = self.model
1430
        pending = self.filter(model.Q_PENDING | model.Q_DENIED).values_list('chain')
1431
        approved = self.filter(model.Q_APPROVED).values_list('chain')
1432
        by_chain = dict(pending.annotate(models.Max('id')))
1433
        by_chain.update(approved.annotate(models.Max('id')))
1434
        return self.filter(flt, id__in=by_chain.values())
1435

    
1436
    def user_accessible_projects(self, user):
1437
        """
1438
        Return projects accessed by specified user.
1439
        """
1440
        if user.is_project_admin():
1441
            participates_filters = Q()
1442
        else:
1443
            participates_filters = Q(owner=user) | Q(applicant=user) | \
1444
                                   Q(project__projectmembership__person=user)
1445

    
1446
        return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
1447

    
1448
    def search_by_name(self, *search_strings):
1449
        q = Q()
1450
        for s in search_strings:
1451
            q = q | Q(name__icontains=s)
1452
        return self.filter(q)
1453

    
1454
    def latest_of_chain(self, chain_id):
1455
        try:
1456
            return self.filter(chain=chain_id).order_by('-id')[0]
1457
        except IndexError:
1458
            return None
1459

    
1460

    
1461
class ProjectApplication(models.Model):
1462
    applicant               =   models.ForeignKey(
1463
                                    AstakosUser,
1464
                                    related_name='projects_applied',
1465
                                    db_index=True)
1466

    
1467
    PENDING     =    0
1468
    APPROVED    =    1
1469
    REPLACED    =    2
1470
    DENIED      =    3
1471
    DISMISSED   =    4
1472
    CANCELLED   =    5
1473

    
1474
    state                   =   models.IntegerField(default=PENDING,
1475
                                                    db_index=True)
1476

    
1477
    owner                   =   models.ForeignKey(
1478
                                    AstakosUser,
1479
                                    related_name='projects_owned',
1480
                                    db_index=True)
1481

    
1482
    chain                   =   models.ForeignKey(Chain,
1483
                                                  related_name='chained_apps',
1484
                                                  db_column='chain')
1485
    precursor_application   =   models.ForeignKey('ProjectApplication',
1486
                                                  null=True,
1487
                                                  blank=True)
1488

    
1489
    name                    =   models.CharField(max_length=80)
1490
    homepage                =   models.URLField(max_length=255, null=True,
1491
                                                verify_exists=False)
1492
    description             =   models.TextField(null=True, blank=True)
1493
    start_date              =   models.DateTimeField(null=True, blank=True)
1494
    end_date                =   models.DateTimeField()
1495
    member_join_policy      =   models.IntegerField()
1496
    member_leave_policy     =   models.IntegerField()
1497
    limit_on_members_number =   models.PositiveIntegerField(null=True)
1498
    resource_grants         =   models.ManyToManyField(
1499
                                    Resource,
1500
                                    null=True,
1501
                                    blank=True,
1502
                                    through='ProjectResourceGrant')
1503
    comments                =   models.TextField(null=True, blank=True)
1504
    issue_date              =   models.DateTimeField(auto_now_add=True)
1505
    response_date           =   models.DateTimeField(null=True, blank=True)
1506
    response                =   models.TextField(null=True, blank=True)
1507

    
1508
    objects                 =   ProjectApplicationManager()
1509

    
1510
    # Compiled queries
1511
    Q_PENDING  = Q(state=PENDING)
1512
    Q_APPROVED = Q(state=APPROVED)
1513
    Q_DENIED   = Q(state=DENIED)
1514

    
1515
    class Meta:
1516
        unique_together = ("chain", "id")
1517

    
1518
    def __unicode__(self):
1519
        return "%s applied by %s" % (self.name, self.applicant)
1520

    
1521
    # TODO: Move to a more suitable place
1522
    APPLICATION_STATE_DISPLAY = {
1523
        PENDING  : _('Pending review'),
1524
        APPROVED : _('Approved'),
1525
        REPLACED : _('Replaced'),
1526
        DENIED   : _('Denied'),
1527
        DISMISSED: _('Dismissed'),
1528
        CANCELLED: _('Cancelled')
1529
    }
1530

    
1531
    @property
1532
    def log_display(self):
1533
        return "application %s (%s) for project %s" % (
1534
            self.id, self.name, self.chain)
1535

    
1536
    def state_display(self):
1537
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1538

    
1539
    def project_state_display(self):
1540
        try:
1541
            project = self.project
1542
            return project.state_display()
1543
        except Project.DoesNotExist:
1544
            return self.state_display()
1545

    
1546
    def add_resource_policy(self, resource, uplimit):
1547
        """Raises ObjectDoesNotExist, IntegrityError"""
1548
        q = self.projectresourcegrant_set
1549
        resource = Resource.objects.get(name=resource)
1550
        q.create(resource=resource, member_capacity=uplimit)
1551

    
1552
    def members_count(self):
1553
        return self.project.approved_memberships.count()
1554

    
1555
    @property
1556
    def grants(self):
1557
        return self.projectresourcegrant_set.values('member_capacity',
1558
                                                    'resource__name')
1559

    
1560
    @property
1561
    def resource_policies(self):
1562
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1563

    
1564
    def set_resource_policies(self, policies):
1565
        for resource, uplimit in policies:
1566
            self.add_resource_policy(resource, uplimit)
1567

    
1568
    def pending_modifications_incl_me(self):
1569
        q = self.chained_applications()
1570
        q = q.filter(Q(state=self.PENDING))
1571
        return q
1572

    
1573
    def last_pending_incl_me(self):
1574
        try:
1575
            return self.pending_modifications_incl_me().order_by('-id')[0]
1576
        except IndexError:
1577
            return None
1578

    
1579
    def pending_modifications(self):
1580
        return self.pending_modifications_incl_me().filter(~Q(id=self.id))
1581

    
1582
    def last_pending(self):
1583
        try:
1584
            return self.pending_modifications().order_by('-id')[0]
1585
        except IndexError:
1586
            return None
1587

    
1588
    def is_modification(self):
1589
        # if self.state != self.PENDING:
1590
        #     return False
1591
        parents = self.chained_applications().filter(id__lt=self.id)
1592
        parents = parents.filter(state__in=[self.APPROVED])
1593
        return parents.count() > 0
1594

    
1595
    def chained_applications(self):
1596
        return ProjectApplication.objects.filter(chain=self.chain)
1597

    
1598
    def is_latest(self):
1599
        return self.chained_applications().order_by('-id')[0] == self
1600

    
1601
    def has_pending_modifications(self):
1602
        return bool(self.last_pending())
1603

    
1604
    def denied_modifications(self):
1605
        q = self.chained_applications()
1606
        q = q.filter(Q(state=self.DENIED))
1607
        q = q.filter(~Q(id=self.id))
1608
        return q
1609

    
1610
    def last_denied(self):
1611
        try:
1612
            return self.denied_modifications().order_by('-id')[0]
1613
        except IndexError:
1614
            return None
1615

    
1616
    def has_denied_modifications(self):
1617
        return bool(self.last_denied())
1618

    
1619
    def is_applied(self):
1620
        try:
1621
            self.project
1622
            return True
1623
        except Project.DoesNotExist:
1624
            return False
1625

    
1626
    def get_project(self):
1627
        try:
1628
            return Project.objects.get(id=self.chain)
1629
        except Project.DoesNotExist:
1630
            return None
1631

    
1632
    def project_exists(self):
1633
        return self.get_project() is not None
1634

    
1635
    def can_cancel(self):
1636
        return self.state == self.PENDING
1637

    
1638
    def cancel(self):
1639
        if not self.can_cancel():
1640
            m = _("cannot cancel: application '%s' in state '%s'") % (
1641
                    self.id, self.state)
1642
            raise AssertionError(m)
1643

    
1644
        self.state = self.CANCELLED
1645
        self.save()
1646

    
1647
    def can_dismiss(self):
1648
        return self.state == self.DENIED
1649

    
1650
    def dismiss(self):
1651
        if not self.can_dismiss():
1652
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1653
                    self.id, self.state)
1654
            raise AssertionError(m)
1655

    
1656
        self.state = self.DISMISSED
1657
        self.save()
1658

    
1659
    def can_deny(self):
1660
        return self.state == self.PENDING
1661

    
1662
    def deny(self, reason):
1663
        if not self.can_deny():
1664
            m = _("cannot deny: application '%s' in state '%s'") % (
1665
                    self.id, self.state)
1666
            raise AssertionError(m)
1667

    
1668
        self.state = self.DENIED
1669
        self.response_date = datetime.now()
1670
        self.response = reason
1671
        self.save()
1672

    
1673
    def can_approve(self):
1674
        return self.state == self.PENDING
1675

    
1676
    def approve(self, reason):
1677
        if not self.can_approve():
1678
            m = _("cannot approve: project '%s' in state '%s'") % (
1679
                    self.name, self.state)
1680
            raise AssertionError(m) # invalid argument
1681

    
1682
        now = datetime.now()
1683
        self.state = self.APPROVED
1684
        self.response_date = now
1685
        self.response = reason
1686
        self.save()
1687

    
1688
        project = self.get_project()
1689
        if project is None:
1690
            project = Project(id=self.chain)
1691

    
1692
        project.name = self.name
1693
        project.application = self
1694
        project.last_approval_date = now
1695
        project.save()
1696
        return project
1697

    
1698
    @property
1699
    def member_join_policy_display(self):
1700
        policy = self.member_join_policy
1701
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1702

    
1703
    @property
1704
    def member_leave_policy_display(self):
1705
        policy = self.member_leave_policy
1706
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1707

    
1708
class ProjectResourceGrant(models.Model):
1709

    
1710
    resource                =   models.ForeignKey(Resource)
1711
    project_application     =   models.ForeignKey(ProjectApplication,
1712
                                                  null=True)
1713
    project_capacity        =   intDecimalField(null=True)
1714
    member_capacity         =   intDecimalField(default=0)
1715

    
1716
    objects = ExtendedManager()
1717

    
1718
    class Meta:
1719
        unique_together = ("resource", "project_application")
1720

    
1721
    def display_member_capacity(self):
1722
        if self.member_capacity:
1723
            if self.resource.unit:
1724
                return ProjectResourceGrant.display_filesize(
1725
                    self.member_capacity)
1726
            else:
1727
                if math.isinf(self.member_capacity):
1728
                    return 'Unlimited'
1729
                else:
1730
                    return self.member_capacity
1731
        else:
1732
            return 'Unlimited'
1733

    
1734
    def __str__(self):
1735
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1736
                                        self.display_member_capacity())
1737

    
1738
    @classmethod
1739
    def display_filesize(cls, value):
1740
        try:
1741
            value = float(value)
1742
        except:
1743
            return
1744
        else:
1745
            if math.isinf(value):
1746
                return 'Unlimited'
1747
            if value > 1:
1748
                unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
1749
                                [0, 0, 0, 0, 0, 0])
1750
                exponent = min(int(math.log(value, 1024)), len(unit_list) - 1)
1751
                quotient = float(value) / 1024**exponent
1752
                unit, value_decimals = unit_list[exponent]
1753
                format_string = '{0:.%sf} {1}' % (value_decimals)
1754
                return format_string.format(quotient, unit)
1755
            if value == 0:
1756
                return '0 bytes'
1757
            if value == 1:
1758
                return '1 byte'
1759
            else:
1760
               return '0'
1761

    
1762

    
1763
class ProjectManager(ForUpdateManager):
1764

    
1765
    def terminated_projects(self):
1766
        q = self.model.Q_TERMINATED
1767
        return self.filter(q)
1768

    
1769
    def not_terminated_projects(self):
1770
        q = ~self.model.Q_TERMINATED
1771
        return self.filter(q)
1772

    
1773
    def deactivated_projects(self):
1774
        q = self.model.Q_DEACTIVATED
1775
        return self.filter(q)
1776

    
1777
    def expired_projects(self):
1778
        q = (~Q(state=Project.TERMINATED) &
1779
              Q(application__end_date__lt=datetime.now()))
1780
        return self.filter(q)
1781

    
1782
    def search_by_name(self, *search_strings):
1783
        q = Q()
1784
        for s in search_strings:
1785
            q = q | Q(name__icontains=s)
1786
        return self.filter(q)
1787

    
1788

    
1789
class Project(models.Model):
1790

    
1791
    id                          =   models.OneToOneField(Chain,
1792
                                                      related_name='chained_project',
1793
                                                      db_column='id',
1794
                                                      primary_key=True)
1795

    
1796
    application                 =   models.OneToOneField(
1797
                                            ProjectApplication,
1798
                                            related_name='project')
1799
    last_approval_date          =   models.DateTimeField(null=True)
1800

    
1801
    members                     =   models.ManyToManyField(
1802
                                            AstakosUser,
1803
                                            through='ProjectMembership')
1804

    
1805
    deactivation_reason         =   models.CharField(max_length=255, null=True)
1806
    deactivation_date           =   models.DateTimeField(null=True)
1807

    
1808
    creation_date               =   models.DateTimeField(auto_now_add=True)
1809
    name                        =   models.CharField(
1810
                                            max_length=80,
1811
                                            null=True,
1812
                                            db_index=True,
1813
                                            unique=True)
1814

    
1815
    APPROVED    = 1
1816
    SUSPENDED   = 10
1817
    TERMINATED  = 100
1818

    
1819
    state                       =   models.IntegerField(default=APPROVED,
1820
                                                        db_index=True)
1821

    
1822
    objects     =   ProjectManager()
1823

    
1824
    # Compiled queries
1825
    Q_TERMINATED  = Q(state=TERMINATED)
1826
    Q_SUSPENDED   = Q(state=SUSPENDED)
1827
    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
1828

    
1829
    def __str__(self):
1830
        return uenc(_("<project %s '%s'>") %
1831
                    (self.id, udec(self.application.name)))
1832

    
1833
    __repr__ = __str__
1834

    
1835
    def __unicode__(self):
1836
        return _("<project %s '%s'>") % (self.id, self.application.name)
1837

    
1838
    STATE_DISPLAY = {
1839
        APPROVED   : 'Active',
1840
        SUSPENDED  : 'Suspended',
1841
        TERMINATED : 'Terminated'
1842
        }
1843

    
1844
    def state_display(self):
1845
        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
1846

    
1847
    def expiration_info(self):
1848
        return (str(self.id), self.name, self.state_display(),
1849
                str(self.application.end_date))
1850

    
1851
    def is_deactivated(self, reason=None):
1852
        if reason is not None:
1853
            return self.state == reason
1854

    
1855
        return self.state != self.APPROVED
1856

    
1857
    ### Deactivation calls
1858

    
1859
    def terminate(self):
1860
        self.deactivation_reason = 'TERMINATED'
1861
        self.deactivation_date = datetime.now()
1862
        self.state = self.TERMINATED
1863
        self.name = None
1864
        self.save()
1865

    
1866
    def suspend(self):
1867
        self.deactivation_reason = 'SUSPENDED'
1868
        self.deactivation_date = datetime.now()
1869
        self.state = self.SUSPENDED
1870
        self.save()
1871

    
1872
    def resume(self):
1873
        self.deactivation_reason = None
1874
        self.deactivation_date = None
1875
        self.state = self.APPROVED
1876
        self.save()
1877

    
1878
    ### Logical checks
1879

    
1880
    def is_inconsistent(self):
1881
        now = datetime.now()
1882
        dates = [self.creation_date,
1883
                 self.last_approval_date,
1884
                 self.deactivation_date]
1885
        return any([date > now for date in dates])
1886

    
1887
    def is_approved(self):
1888
        return self.state == self.APPROVED
1889

    
1890
    @property
1891
    def is_alive(self):
1892
        return not self.is_terminated
1893

    
1894
    @property
1895
    def is_terminated(self):
1896
        return self.is_deactivated(self.TERMINATED)
1897

    
1898
    @property
1899
    def is_suspended(self):
1900
        return self.is_deactivated(self.SUSPENDED)
1901

    
1902
    def violates_resource_grants(self):
1903
        return False
1904

    
1905
    def violates_members_limit(self, adding=0):
1906
        application = self.application
1907
        limit = application.limit_on_members_number
1908
        if limit is None:
1909
            return False
1910
        return (len(self.approved_members) + adding > limit)
1911

    
1912

    
1913
    ### Other
1914

    
1915
    def count_pending_memberships(self):
1916
        memb_set = self.projectmembership_set
1917
        memb_count = memb_set.filter(state=ProjectMembership.REQUESTED).count()
1918
        return memb_count
1919

    
1920
    def count_actually_accepted_memberships(self):
1921
        memb_set = self.projectmembership_set
1922
        memb_count = memb_set.filter(state=ProjectMembership.LEAVE_REQUESTED)
1923
        memb_count = memb_set.filter(state=ProjectMembership.ACCEPTED).count()
1924
        return memb_count
1925

    
1926
    def members_count(self):
1927
        return self.approved_memberships.count()
1928

    
1929
    @property
1930
    def approved_memberships(self):
1931
        query = ProjectMembership.Q_ACCEPTED_STATES
1932
        return self.projectmembership_set.filter(query)
1933

    
1934
    @property
1935
    def approved_members(self):
1936
        return [m.person for m in self.approved_memberships]
1937

    
1938

    
1939
CHAIN_STATE = {
1940
    (Project.APPROVED,   ProjectApplication.PENDING)  : Chain.APPROVED_PENDING,
1941
    (Project.APPROVED,   ProjectApplication.APPROVED) : Chain.APPROVED,
1942
    (Project.APPROVED,   ProjectApplication.DENIED)   : Chain.APPROVED,
1943
    (Project.APPROVED,   ProjectApplication.DISMISSED): Chain.APPROVED,
1944
    (Project.APPROVED,   ProjectApplication.CANCELLED): Chain.APPROVED,
1945

    
1946
    (Project.SUSPENDED,  ProjectApplication.PENDING)  : Chain.SUSPENDED_PENDING,
1947
    (Project.SUSPENDED,  ProjectApplication.APPROVED) : Chain.SUSPENDED,
1948
    (Project.SUSPENDED,  ProjectApplication.DENIED)   : Chain.SUSPENDED,
1949
    (Project.SUSPENDED,  ProjectApplication.DISMISSED): Chain.SUSPENDED,
1950
    (Project.SUSPENDED,  ProjectApplication.CANCELLED): Chain.SUSPENDED,
1951

    
1952
    (Project.TERMINATED, ProjectApplication.PENDING)  : Chain.TERMINATED_PENDING,
1953
    (Project.TERMINATED, ProjectApplication.APPROVED) : Chain.TERMINATED,
1954
    (Project.TERMINATED, ProjectApplication.DENIED)   : Chain.TERMINATED,
1955
    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
1956
    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
1957

    
1958
    (None,               ProjectApplication.PENDING)  : Chain.PENDING,
1959
    (None,               ProjectApplication.DENIED)   : Chain.DENIED,
1960
    (None,               ProjectApplication.DISMISSED): Chain.DISMISSED,
1961
    (None,               ProjectApplication.CANCELLED): Chain.CANCELLED,
1962
    }
1963

    
1964

    
1965
class ProjectMembershipManager(ForUpdateManager):
1966

    
1967
    def any_accepted(self):
1968
        q = self.model.Q_ACTUALLY_ACCEPTED
1969
        return self.filter(q)
1970

    
1971
    def actually_accepted(self):
1972
        q = self.model.Q_ACTUALLY_ACCEPTED
1973
        return self.filter(q)
1974

    
1975
    def requested(self):
1976
        return self.filter(state=ProjectMembership.REQUESTED)
1977

    
1978
    def suspended(self):
1979
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1980

    
1981
class ProjectMembership(models.Model):
1982

    
1983
    person              =   models.ForeignKey(AstakosUser)
1984
    request_date        =   models.DateTimeField(auto_now_add=True)
1985
    project             =   models.ForeignKey(Project)
1986

    
1987
    REQUESTED           =   0
1988
    ACCEPTED            =   1
1989
    LEAVE_REQUESTED     =   5
1990
    # User deactivation
1991
    USER_SUSPENDED      =   10
1992

    
1993
    REMOVED             =   200
1994

    
1995
    ASSOCIATED_STATES   =   set([REQUESTED,
1996
                                 ACCEPTED,
1997
                                 LEAVE_REQUESTED,
1998
                                 USER_SUSPENDED,
1999
                                 ])
2000

    
2001
    ACCEPTED_STATES     =   set([ACCEPTED,
2002
                                 LEAVE_REQUESTED,
2003
                                 USER_SUSPENDED,
2004
                                 ])
2005

    
2006
    ACTUALLY_ACCEPTED   =   set([ACCEPTED, LEAVE_REQUESTED])
2007

    
2008
    state               =   models.IntegerField(default=REQUESTED,
2009
                                                db_index=True)
2010
    acceptance_date     =   models.DateTimeField(null=True, db_index=True)
2011
    leave_request_date  =   models.DateTimeField(null=True)
2012

    
2013
    objects     =   ProjectMembershipManager()
2014

    
2015
    # Compiled queries
2016
    Q_ACCEPTED_STATES = ~Q(state=REQUESTED) & ~Q(state=REMOVED)
2017
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
2018

    
2019
    MEMBERSHIP_STATE_DISPLAY = {
2020
        REQUESTED           : _('Requested'),
2021
        ACCEPTED            : _('Accepted'),
2022
        LEAVE_REQUESTED     : _('Leave Requested'),
2023
        USER_SUSPENDED      : _('Suspended'),
2024
        REMOVED             : _('Pending removal'),
2025
        }
2026

    
2027
    USER_FRIENDLY_STATE_DISPLAY = {
2028
        REQUESTED           : _('Join requested'),
2029
        ACCEPTED            : _('Accepted member'),
2030
        LEAVE_REQUESTED     : _('Requested to leave'),
2031
        USER_SUSPENDED      : _('Suspended member'),
2032
        REMOVED             : _('Pending removal'),
2033
        }
2034

    
2035
    def state_display(self):
2036
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
2037

    
2038
    def user_friendly_state_display(self):
2039
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
2040

    
2041
    class Meta:
2042
        unique_together = ("person", "project")
2043
        #index_together = [["project", "state"]]
2044

    
2045
    def __str__(self):
2046
        return uenc(_("<'%s' membership in '%s'>") % (
2047
                self.person.username, self.project))
2048

    
2049
    __repr__ = __str__
2050

    
2051
    def __init__(self, *args, **kwargs):
2052
        self.state = self.REQUESTED
2053
        super(ProjectMembership, self).__init__(*args, **kwargs)
2054

    
2055
    def _set_history_item(self, reason, date=None):
2056
        if isinstance(reason, basestring):
2057
            reason = ProjectMembershipHistory.reasons.get(reason, -1)
2058

    
2059
        history_item = ProjectMembershipHistory(
2060
                            serial=self.id,
2061
                            person=self.person_id,
2062
                            project=self.project_id,
2063
                            date=date or datetime.now(),
2064
                            reason=reason)
2065
        history_item.save()
2066
        serial = history_item.id
2067

    
2068
    def can_accept(self):
2069
        return self.state == self.REQUESTED
2070

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

    
2076
        now = datetime.now()
2077
        self.acceptance_date = now
2078
        self._set_history_item(reason='ACCEPT', date=now)
2079
        self.state = self.ACCEPTED
2080
        self.save()
2081

    
2082
    def can_leave(self):
2083
        return self.state in self.ACCEPTED_STATES
2084

    
2085
    def leave_request(self):
2086
        if not self.can_leave():
2087
            m = _("%s: attempt to request to leave in state '%s'") % (
2088
                self, self.state)
2089
            raise AssertionError(m)
2090

    
2091
        self.leave_request_date = datetime.now()
2092
        self.state = self.LEAVE_REQUESTED
2093
        self.save()
2094

    
2095
    def can_deny_leave(self):
2096
        return self.state == self.LEAVE_REQUESTED
2097

    
2098
    def leave_request_deny(self):
2099
        if not self.can_deny_leave():
2100
            m = _("%s: attempt to deny leave request in state '%s'") % (
2101
                self, self.state)
2102
            raise AssertionError(m)
2103

    
2104
        self.leave_request_date = None
2105
        self.state = self.ACCEPTED
2106
        self.save()
2107

    
2108
    def can_cancel_leave(self):
2109
        return self.state == self.LEAVE_REQUESTED
2110

    
2111
    def leave_request_cancel(self):
2112
        if not self.can_cancel_leave():
2113
            m = _("%s: attempt to cancel leave request in state '%s'") % (
2114
                self, self.state)
2115
            raise AssertionError(m)
2116

    
2117
        self.leave_request_date = None
2118
        self.state = self.ACCEPTED
2119
        self.save()
2120

    
2121
    def can_remove(self):
2122
        return self.state in self.ACCEPTED_STATES
2123

    
2124
    def remove(self):
2125
        if not self.can_remove():
2126
            m = _("%s: attempt to remove in state '%s'") % (self, self.state)
2127
            raise AssertionError(m)
2128

    
2129
        self._set_history_item(reason='REMOVE')
2130
        self.delete()
2131

    
2132
    def can_reject(self):
2133
        return self.state == self.REQUESTED
2134

    
2135
    def reject(self):
2136
        if not self.can_reject():
2137
            m = _("%s: attempt to reject in state '%s'") % (self, self.state)
2138
            raise AssertionError(m)
2139

    
2140
        # rejected requests don't need sync,
2141
        # because they were never effected
2142
        self._set_history_item(reason='REJECT')
2143
        self.delete()
2144

    
2145
    def can_cancel(self):
2146
        return self.state == self.REQUESTED
2147

    
2148
    def cancel(self):
2149
        if not self.can_cancel():
2150
            m = _("%s: attempt to cancel in state '%s'") % (self, self.state)
2151
            raise AssertionError(m)
2152

    
2153
        # rejected requests don't need sync,
2154
        # because they were never effected
2155
        self._set_history_item(reason='CANCEL')
2156
        self.delete()
2157

    
2158

    
2159
class Serial(models.Model):
2160
    serial  =   models.AutoField(primary_key=True)
2161

    
2162

    
2163
class ProjectMembershipHistory(models.Model):
2164
    reasons_list    =   ['ACCEPT', 'REJECT', 'REMOVE']
2165
    reasons         =   dict((k, v) for v, k in enumerate(reasons_list))
2166

    
2167
    person  =   models.BigIntegerField()
2168
    project =   models.BigIntegerField()
2169
    date    =   models.DateTimeField(auto_now_add=True)
2170
    reason  =   models.IntegerField()
2171
    serial  =   models.BigIntegerField()
2172

    
2173
### SIGNALS ###
2174
################
2175

    
2176
def create_astakos_user(u):
2177
    try:
2178
        AstakosUser.objects.get(user_ptr=u.pk)
2179
    except AstakosUser.DoesNotExist:
2180
        extended_user = AstakosUser(user_ptr_id=u.pk)
2181
        extended_user.__dict__.update(u.__dict__)
2182
        extended_user.save()
2183
        if not extended_user.has_auth_provider('local'):
2184
            extended_user.add_auth_provider('local')
2185
    except BaseException, e:
2186
        logger.exception(e)
2187

    
2188
def fix_superusers():
2189
    # Associate superusers with AstakosUser
2190
    admins = User.objects.filter(is_superuser=True)
2191
    for u in admins:
2192
        create_astakos_user(u)
2193

    
2194
def user_post_save(sender, instance, created, **kwargs):
2195
    if not created:
2196
        return
2197
    create_astakos_user(instance)
2198
post_save.connect(user_post_save, sender=User)
2199

    
2200
def astakosuser_post_save(sender, instance, created, **kwargs):
2201
    pass
2202

    
2203
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2204

    
2205
def resource_post_save(sender, instance, created, **kwargs):
2206
    pass
2207

    
2208
post_save.connect(resource_post_save, sender=Resource)
2209

    
2210
def renew_token(sender, instance, **kwargs):
2211
    if not instance.auth_token:
2212
        instance.renew_token()
2213
pre_save.connect(renew_token, sender=AstakosUser)
2214
pre_save.connect(renew_token, sender=Component)