Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 72313b77

History | View | Annotate | Download (67.3 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 uuid
35
import logging
36
import json
37
import copy
38

    
39
from datetime import datetime, timedelta
40
import base64
41
from urllib import quote
42
from random import randint
43
import os
44

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

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

    
59
from synnefo.lib.utils import dict_merge
60

    
61
from astakos.im import settings as astakos_settings
62
from astakos.im import auth_providers as auth
63

    
64
import astakos.im.messages as astakos_messages
65
from synnefo.lib.ordereddict import OrderedDict
66

    
67
from snf_django.lib.db.fields import intDecimalField
68
from synnefo.util.text import uenc, udec
69
from synnefo.util import units
70
from astakos.im import presentation
71

    
72
logger = logging.getLogger(__name__)
73

    
74
DEFAULT_CONTENT_TYPE = None
75
_content_type = None
76

    
77

    
78
def get_content_type():
79
    global _content_type
80
    if _content_type is not None:
81
        return _content_type
82

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

    
91
inf = float('inf')
92

    
93

    
94
def generate_token():
95
    s = os.urandom(32)
96
    return base64.urlsafe_b64encode(s).rstrip('=')
97

    
98

    
99
def _partition_by(f, l):
100
    d = {}
101
    for x in l:
102
        group = f(x)
103
        group_l = d.get(group, [])
104
        group_l.append(x)
105
        d[group] = group_l
106
    return d
107

    
108

    
109
def first_of_group(f, l):
110
    Nothing = type("Nothing", (), {})
111
    last_group = Nothing
112
    d = {}
113
    for x in l:
114
        group = f(x)
115
        if group != last_group:
116
            last_group = group
117
            d[group] = x
118
    return d
119

    
120

    
121
class Component(models.Model):
122
    name = models.CharField(_('Name'), max_length=255, unique=True,
123
                            db_index=True)
124
    url = models.CharField(_('Component url'), max_length=1024, null=True,
125
                           help_text=_("URL the component is accessible from"))
126
    base_url = models.CharField(max_length=1024, null=True)
127
    auth_token = models.CharField(_('Authentication Token'), max_length=64,
128
                                  null=True, blank=True, unique=True)
129
    auth_token_created = models.DateTimeField(_('Token creation date'),
130
                                              null=True)
131
    auth_token_expires = models.DateTimeField(_('Token expiration date'),
132
                                              null=True)
133

    
134
    def renew_token(self, expiration_date=None):
135
        for i in range(10):
136
            new_token = generate_token()
137
            count = Component.objects.filter(auth_token=new_token).count()
138
            if count == 0:
139
                break
140
            continue
141
        else:
142
            raise ValueError('Could not generate a token')
143

    
144
        self.auth_token = new_token
145
        self.auth_token_created = datetime.now()
146
        if expiration_date:
147
            self.auth_token_expires = expiration_date
148
        else:
149
            self.auth_token_expires = None
150
        msg = 'Token renewed for component %s'
151
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.name)
152

    
153
    def __str__(self):
154
        return self.name
155

    
156
    @classmethod
157
    def catalog(cls, orderfor=None):
158
        catalog = {}
159
        components = list(cls.objects.all())
160
        default_metadata = presentation.COMPONENTS
161
        metadata = {}
162

    
163
        for component in components:
164
            d = {'url': component.url,
165
                 'name': component.name}
166
            if component.name in default_metadata:
167
                metadata[component.name] = default_metadata.get(component.name)
168
                metadata[component.name].update(d)
169
            else:
170
                metadata[component.name] = d
171

    
172
        def component_by_order(s):
173
            return s[1].get('order')
174

    
175
        def component_by_dashboard_order(s):
176
            return s[1].get('dashboard').get('order')
177

    
178
        metadata = dict_merge(metadata,
179
                              astakos_settings.COMPONENTS_META)
180

    
181
        for component, info in metadata.iteritems():
182
            default_meta = presentation.component_defaults(component)
183
            base_meta = metadata.get(component, {})
184
            settings_meta = astakos_settings.COMPONENTS_META.get(component, {})
185
            component_meta = dict_merge(default_meta, base_meta)
186
            meta = dict_merge(component_meta, settings_meta)
187
            catalog[component] = meta
188

    
189
        order_key = component_by_order
190
        if orderfor == 'dashboard':
191
            order_key = component_by_dashboard_order
192

    
193
        ordered_catalog = OrderedDict(sorted(catalog.iteritems(),
194
                                             key=order_key))
195
        return ordered_catalog
196

    
197

    
198
_presentation_data = {}
199

    
200

    
201
def get_presentation(resource):
202
    global _presentation_data
203
    resource_presentation = _presentation_data.get(resource, {})
204
    if not resource_presentation:
205
        resources_presentation = presentation.RESOURCES.get('resources', {})
206
        resource_presentation = resources_presentation.get(resource, {})
207
        _presentation_data[resource] = resource_presentation
208
    return resource_presentation
209

    
210

    
211
class Service(models.Model):
212
    component = models.ForeignKey(Component)
213
    name = models.CharField(max_length=255, unique=True)
214
    type = models.CharField(max_length=255)
215

    
216

    
217
class Endpoint(models.Model):
218
    service = models.ForeignKey(Service, related_name='endpoints')
219

    
220

    
221
class EndpointData(models.Model):
222
    endpoint = models.ForeignKey(Endpoint, related_name='data')
223
    key = models.CharField(max_length=255)
224
    value = models.CharField(max_length=1024)
225

    
226
    class Meta:
227
        unique_together = (('endpoint', 'key'),)
228

    
229

    
230
class Resource(models.Model):
231
    name = models.CharField(_('Name'), max_length=255, unique=True)
232
    desc = models.TextField(_('Description'), null=True)
233
    service_type = models.CharField(_('Type'), max_length=255)
234
    service_origin = models.CharField(max_length=255, db_index=True)
235
    unit = models.CharField(_('Unit'), null=True, max_length=255)
236
    uplimit = intDecimalField(default=0)
237
    allow_in_projects = models.BooleanField(default=True)
238

    
239
    def __str__(self):
240
        return self.name
241

    
242
    def full_name(self):
243
        return str(self)
244

    
245
    def get_info(self):
246
        return {'service': self.service_origin,
247
                'description': self.desc,
248
                'unit': self.unit,
249
                'allow_in_projects': self.allow_in_projects,
250
                }
251

    
252
    @property
253
    def group(self):
254
        default = self.name
255
        return get_presentation(str(self)).get('group', default)
256

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

    
262
    @property
263
    def help_text_input_each(self):
264
        default = "%s resource" % self.name
265
        return get_presentation(str(self)).get('help_text_input_each', default)
266

    
267
    @property
268
    def is_abbreviation(self):
269
        return get_presentation(str(self)).get('is_abbreviation', False)
270

    
271
    @property
272
    def report_desc(self):
273
        default = "%s resource" % self.name
274
        return get_presentation(str(self)).get('report_desc', default)
275

    
276
    @property
277
    def placeholder(self):
278
        return get_presentation(str(self)).get('placeholder', self.unit)
279

    
280
    @property
281
    def verbose_name(self):
282
        return get_presentation(str(self)).get('verbose_name', self.name)
283

    
284
    @property
285
    def display_name(self):
286
        name = self.verbose_name
287
        if self.is_abbreviation:
288
            name = name.upper()
289
        return name
290

    
291
    @property
292
    def pluralized_display_name(self):
293
        if not self.unit:
294
            return '%ss' % self.display_name
295
        return self.display_name
296

    
297

    
298
def get_resource_names():
299
    _RESOURCE_NAMES = []
300
    resources = Resource.objects.select_related('service').all()
301
    _RESOURCE_NAMES = [resource.full_name() for resource in resources]
302
    return _RESOURCE_NAMES
303

    
304

    
305
class AstakosUserManager(UserManager):
306

    
307
    def get_auth_provider_user(self, provider, **kwargs):
308
        """
309
        Retrieve AstakosUser instance associated with the specified third party
310
        id.
311
        """
312
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
313
                          kwargs.iteritems()))
314
        return self.get(auth_providers__module=provider, **kwargs)
315

    
316
    def get_by_email(self, email):
317
        return self.get(email=email)
318

    
319
    def get_by_identifier(self, email_or_username, **kwargs):
320
        try:
321
            return self.get(email__iexact=email_or_username, **kwargs)
322
        except AstakosUser.DoesNotExist:
323
            return self.get(username__iexact=email_or_username, **kwargs)
324

    
325
    def user_exists(self, email_or_username, **kwargs):
326
        qemail = Q(email__iexact=email_or_username)
327
        qusername = Q(username__iexact=email_or_username)
328
        qextra = Q(**kwargs)
329
        return self.filter((qemail | qusername) & qextra).exists()
330

    
331
    def verified_user_exists(self, email_or_username):
332
        return self.user_exists(email_or_username, email_verified=True)
333

    
334
    def verified(self):
335
        return self.filter(email_verified=True)
336

    
337
    def accepted(self):
338
        return self.filter(moderated=True, is_rejected=False)
339

    
340
    def uuid_catalog(self, l=None):
341
        """
342
        Returns a uuid to username mapping for the uuids appearing in l.
343
        If l is None returns the mapping for all existing users.
344
        """
345
        q = self.filter(uuid__in=l) if l is not None else self
346
        return dict(q.values_list('uuid', 'username'))
347

    
348
    def displayname_catalog(self, l=None):
349
        """
350
        Returns a username to uuid mapping for the usernames appearing in l.
351
        If l is None returns the mapping for all existing users.
352
        """
353
        if l is not None:
354
            lmap = dict((x.lower(), x) for x in l)
355
            q = self.filter(username__in=lmap.keys())
356
            values = ((lmap[n], u)
357
                      for n, u in q.values_list('username', 'uuid'))
358
        else:
359
            q = self
360
            values = self.values_list('username', 'uuid')
361
        return dict(values)
362

    
363

    
364
class AstakosUser(User):
365
    """
366
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
367
    """
368
    affiliation = models.CharField(_('Affiliation'), max_length=255,
369
                                   blank=True, null=True)
370

    
371
    #for invitations
372
    user_level = astakos_settings.DEFAULT_USER_LEVEL
373
    level = models.IntegerField(_('Inviter level'), default=user_level)
374
    invitations = models.IntegerField(
375
        _('Invitations left'),
376
        default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
377

    
378
    auth_token = models.CharField(
379
        _('Authentication Token'),
380
        max_length=64,
381
        unique=True,
382
        null=True,
383
        blank=True,
384
        help_text=_('Renew your authentication '
385
                    'token. Make sure to set the new '
386
                    'token in any client you may be '
387
                    'using, to preserve its '
388
                    'functionality.'))
389
    auth_token_created = models.DateTimeField(_('Token creation date'),
390
                                              null=True)
391
    auth_token_expires = models.DateTimeField(
392
        _('Token expiration date'), null=True)
393

    
394
    updated = models.DateTimeField(_('Update date'))
395

    
396
    # Arbitrary text to identify the reason user got deactivated.
397
    # To be used as a reference from administrators.
398
    deactivated_reason = models.TextField(
399
        _('Reason the user was disabled for'),
400
        default=None, null=True)
401
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
402
                                          blank=True)
403

    
404
    has_credits = models.BooleanField(_('Has credits?'), default=False)
405

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

    
409
    # user email is verified
410
    email_verified = models.BooleanField(_('Email verified?'), default=False)
411

    
412
    # unique string used in user email verification url
413
    verification_code = models.CharField(max_length=255, null=True,
414
                                         blank=False, unique=True)
415

    
416
    # date user email verified
417
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
418
                                       blank=True)
419

    
420
    # email verification notice was sent to the user at this time
421
    activation_sent = models.DateTimeField(_('Activation sent date'),
422
                                           null=True, blank=True)
423

    
424
    # user got rejected during moderation process
425
    is_rejected = models.BooleanField(_('Account rejected'),
426
                                      default=False)
427
    # reason user got rejected
428
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
429
                                       blank=True)
430
    # moderation status
431
    moderated = models.BooleanField(_('User moderated'), default=False)
432
    # date user moderated (either accepted or rejected)
433
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
434
                                        blank=True, null=True)
435
    # a snapshot of user instance the time got moderated
436
    moderated_data = models.TextField(null=True, default=None, blank=True)
437
    # a string which identifies how the user got moderated
438
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
439
                                       default=None, null=True, blank=True)
440
    # the email used to accept the user
441
    accepted_email = models.EmailField(null=True, default=None, blank=True)
442

    
443
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
444
                                           default=False)
445
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
446
                                             null=True, blank=True)
447
    # permanent unique user identifier
448
    uuid = models.CharField(max_length=255, null=True, blank=False,
449
                            unique=True)
450

    
451
    policy = models.ManyToManyField(
452
        Resource, null=True, through='AstakosUserQuota')
453

    
454
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
455
                                          default=False, db_index=True)
456

    
457
    objects = AstakosUserManager()
458

    
459
    def __init__(self, *args, **kwargs):
460
        super(AstakosUser, self).__init__(*args, **kwargs)
461
        if not self.id:
462
            self.is_active = False
463

    
464
    @property
465
    def realname(self):
466
        return '%s %s' % (self.first_name, self.last_name)
467

    
468
    @property
469
    def log_display(self):
470
        """
471
        Should be used in all logger.* calls that refer to a user so that
472
        user display is consistent across log entries.
473
        """
474
        return '%s::%s' % (self.uuid, self.email)
475

    
476
    @realname.setter
477
    def realname(self, value):
478
        parts = value.split(' ')
479
        if len(parts) == 2:
480
            self.first_name = parts[0]
481
            self.last_name = parts[1]
482
        else:
483
            self.last_name = parts[0]
484

    
485
    def add_permission(self, pname):
486
        if self.has_perm(pname):
487
            return
488
        p, created = Permission.objects.get_or_create(
489
            codename=pname,
490
            name=pname.capitalize(),
491
            content_type=get_content_type())
492
        self.user_permissions.add(p)
493

    
494
    def remove_permission(self, pname):
495
        if self.has_perm(pname):
496
            return
497
        p = Permission.objects.get(codename=pname,
498
                                   content_type=get_content_type())
499
        self.user_permissions.remove(p)
500

    
501
    def add_group(self, gname):
502
        group, _ = Group.objects.get_or_create(name=gname)
503
        self.groups.add(group)
504

    
505
    def is_accepted(self):
506
        return self.moderated and not self.is_rejected
507

    
508
    def is_project_admin(self, application_id=None):
509
        return self.uuid in astakos_settings.PROJECT_ADMINS
510

    
511
    @property
512
    def invitation(self):
513
        try:
514
            return Invitation.objects.get(username=self.email)
515
        except Invitation.DoesNotExist:
516
            return None
517

    
518
    @property
519
    def policies(self):
520
        return self.astakosuserquota_set.select_related().all()
521

    
522
    def get_resource_policy(self, resource):
523
        resource = Resource.objects.get(name=resource)
524
        default_capacity = resource.uplimit
525
        try:
526
            policy = AstakosUserQuota.objects.get(user=self, resource=resource)
527
            return policy, default_capacity
528
        except AstakosUserQuota.DoesNotExist:
529
            return None, default_capacity
530

    
531
    def update_uuid(self):
532
        while not self.uuid:
533
            uuid_val = str(uuid.uuid4())
534
            try:
535
                AstakosUser.objects.get(uuid=uuid_val)
536
            except AstakosUser.DoesNotExist:
537
                self.uuid = uuid_val
538
        return self.uuid
539

    
540
    def save(self, update_timestamps=True, **kwargs):
541
        if update_timestamps:
542
            if not self.id:
543
                self.date_joined = datetime.now()
544
            self.updated = datetime.now()
545

    
546
        self.update_uuid()
547

    
548
        if not self.verification_code:
549
            self.renew_verification_code()
550

    
551
        # username currently matches email
552
        if self.username != self.email.lower():
553
            self.username = self.email.lower()
554

    
555
        super(AstakosUser, self).save(**kwargs)
556

    
557
    def renew_verification_code(self):
558
        self.verification_code = str(uuid.uuid4())
559
        logger.info("Verification code renewed for %s" % self.log_display)
560

    
561
    def renew_token(self, flush_sessions=False, current_key=None):
562
        for i in range(10):
563
            new_token = generate_token()
564
            count = AstakosUser.objects.filter(auth_token=new_token).count()
565
            if count == 0:
566
                break
567
            continue
568
        else:
569
            raise ValueError('Could not generate a token')
570

    
571
        self.auth_token = new_token
572
        self.auth_token_created = datetime.now()
573
        self.auth_token_expires = self.auth_token_created + \
574
            timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
575
        if flush_sessions:
576
            self.flush_sessions(current_key)
577
        msg = 'Token renewed for %s'
578
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
579

    
580
    def token_expired(self):
581
        return self.auth_token_expires < datetime.now()
582

    
583
    def flush_sessions(self, current_key=None):
584
        q = self.sessions
585
        if current_key:
586
            q = q.exclude(session_key=current_key)
587

    
588
        keys = q.values_list('session_key', flat=True)
589
        if keys:
590
            msg = 'Flushing sessions: %s'
591
            logger.log(astakos_settings.LOGGING_LEVEL, msg, ','.join(keys))
592
        engine = import_module(settings.SESSION_ENGINE)
593
        for k in keys:
594
            s = engine.SessionStore(k)
595
            s.flush()
596

    
597
    def __unicode__(self):
598
        return '%s (%s)' % (self.realname, self.email)
599

    
600
    def conflicting_email(self):
601
        q = AstakosUser.objects.exclude(username=self.username)
602
        q = q.filter(email__iexact=self.email)
603
        if q.count() != 0:
604
            return True
605
        return False
606

    
607
    def email_change_is_pending(self):
608
        return self.emailchanges.count() > 0
609

    
610
    @property
611
    def status_display(self):
612
        msg = ""
613
        if self.is_active:
614
            msg = "Accepted/Active"
615
        if self.is_rejected:
616
            msg = "Rejected"
617
            if self.rejected_reason:
618
                msg += " (%s)" % self.rejected_reason
619
        if not self.email_verified:
620
            msg = "Pending email verification"
621
        if not self.moderated:
622
            msg = "Pending moderation"
623
        if not self.is_active and self.email_verified:
624
            msg = "Accepted/Inactive"
625
            if self.deactivated_reason:
626
                msg += " (%s)" % (self.deactivated_reason)
627

    
628
        if self.moderated and not self.is_rejected:
629
            if self.accepted_policy == 'manual':
630
                msg += " (manually accepted)"
631
            else:
632
                msg += " (accepted policy: %s)" % \
633
                    self.accepted_policy
634
        return msg
635

    
636
    @property
637
    def signed_terms(self):
638
        term = get_latest_terms()
639
        if not term:
640
            return True
641
        if not self.has_signed_terms:
642
            return False
643
        if not self.date_signed_terms:
644
            return False
645
        if self.date_signed_terms < term.date:
646
            self.has_signed_terms = False
647
            self.date_signed_terms = None
648
            self.save()
649
            return False
650
        return True
651

    
652
    def set_invitations_level(self):
653
        """
654
        Update user invitation level
655
        """
656
        level = self.invitation.inviter.level + 1
657
        self.level = level
658
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
659

    
660
    def can_change_password(self):
661
        return self.has_auth_provider('local', auth_backend='astakos')
662

    
663
    def can_change_email(self):
664
        if not self.has_auth_provider('local'):
665
            return True
666

    
667
        local = self.get_auth_provider('local')._instance
668
        return local.auth_backend == 'astakos'
669

    
670
    # Auth providers related methods
671
    def get_auth_provider(self, module=None, identifier=None, **filters):
672
        if not module:
673
            return self.auth_providers.active()[0].settings
674

    
675
        params = {'module': module}
676
        if identifier:
677
            params['identifier'] = identifier
678
        params.update(filters)
679
        return self.auth_providers.active().get(**params).settings
680

    
681
    def has_auth_provider(self, provider, **kwargs):
682
        return bool(self.auth_providers.active().filter(module=provider,
683
                                                        **kwargs).count())
684

    
685
    def get_required_providers(self, **kwargs):
686
        return auth.REQUIRED_PROVIDERS.keys()
687

    
688
    def missing_required_providers(self):
689
        required = self.get_required_providers()
690
        missing = []
691
        for provider in required:
692
            if not self.has_auth_provider(provider):
693
                missing.append(auth.get_provider(provider, self))
694
        return missing
695

    
696
    def get_available_auth_providers(self, **filters):
697
        """
698
        Returns a list of providers available for add by the user.
699
        """
700
        modules = astakos_settings.IM_MODULES
701
        providers = []
702
        for p in modules:
703
            providers.append(auth.get_provider(p, self))
704
        available = []
705

    
706
        for p in providers:
707
            if p.get_add_policy:
708
                available.append(p)
709
        return available
710

    
711
    def get_disabled_auth_providers(self, **filters):
712
        providers = self.get_auth_providers(**filters)
713
        disabled = []
714
        for p in providers:
715
            if not p.get_login_policy:
716
                disabled.append(p)
717
        return disabled
718

    
719
    def get_enabled_auth_providers(self, **filters):
720
        providers = self.get_auth_providers(**filters)
721
        enabled = []
722
        for p in providers:
723
            if p.get_login_policy:
724
                enabled.append(p)
725
        return enabled
726

    
727
    def get_auth_providers(self, **filters):
728
        providers = []
729
        for provider in self.auth_providers.active(**filters):
730
            if provider.settings.module_enabled:
731
                providers.append(provider.settings)
732

    
733
        modules = astakos_settings.IM_MODULES
734

    
735
        def key(p):
736
            if not p.module in modules:
737
                return 100
738
            return modules.index(p.module)
739

    
740
        providers = sorted(providers, key=key)
741
        return providers
742

    
743
    # URL methods
744
    @property
745
    def auth_providers_display(self):
746
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
747
                         self.get_enabled_auth_providers()])
748

    
749
    def add_auth_provider(self, module='local', identifier=None, **params):
750
        provider = auth.get_provider(module, self, identifier, **params)
751
        provider.add_to_user()
752

    
753
    def get_resend_activation_url(self):
754
        return reverse('send_activation', kwargs={'user_id': self.pk})
755

    
756
    def get_activation_url(self, nxt=False):
757
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
758
                              quote(self.verification_code))
759
        if nxt:
760
            url += "&next=%s" % quote(nxt)
761
        return url
762

    
763
    def get_password_reset_url(self, token_generator=default_token_generator):
764
        return reverse('astakos.im.views.target.local.password_reset_confirm',
765
                       kwargs={'uidb36': int_to_base36(self.id),
766
                               'token': token_generator.make_token(self)})
767

    
768
    def get_inactive_message(self, provider_module, identifier=None):
769
        provider = self.get_auth_provider(provider_module, identifier)
770

    
771
        msg_extra = ''
772
        message = ''
773

    
774
        msg_inactive = provider.get_account_inactive_msg
775
        msg_pending = provider.get_pending_activation_msg
776
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
777
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
778
        msg_pending_mod = provider.get_pending_moderation_msg
779
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
780
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
781

    
782
        if not self.email_verified:
783
            message = msg_pending
784
            url = self.get_resend_activation_url()
785
            msg_extra = msg_pending_help + \
786
                u' ' + \
787
                '<a href="%s">%s?</a>' % (url, msg_resend)
788
        else:
789
            if not self.moderated:
790
                message = msg_pending_mod
791
            else:
792
                if self.is_rejected:
793
                    message = msg_rejected
794
                else:
795
                    message = msg_inactive
796

    
797
        return mark_safe(message + u' ' + msg_extra)
798

    
799
    def owns_application(self, application):
800
        return application.owner == self
801

    
802
    def owns_project(self, project):
803
        return project.application.owner == self
804

    
805
    def is_associated(self, project):
806
        try:
807
            m = ProjectMembership.objects.get(person=self, project=project)
808
            return m.state in ProjectMembership.ASSOCIATED_STATES
809
        except ProjectMembership.DoesNotExist:
810
            return False
811

    
812
    def get_membership(self, project):
813
        try:
814
            return ProjectMembership.objects.get(
815
                project=project,
816
                person=self)
817
        except ProjectMembership.DoesNotExist:
818
            return None
819

    
820
    def membership_display(self, project):
821
        m = self.get_membership(project)
822
        if m is None:
823
            return _('Not a member')
824
        else:
825
            return m.user_friendly_state_display()
826

    
827
    def non_owner_can_view(self, maybe_project):
828
        if self.is_project_admin():
829
            return True
830
        if maybe_project is None:
831
            return False
832
        project = maybe_project
833
        if self.is_associated(project):
834
            return True
835
        if project.is_deactivated():
836
            return False
837
        return True
838

    
839

    
840
class AstakosUserAuthProviderManager(models.Manager):
841

    
842
    def active(self, **filters):
843
        return self.filter(active=True, **filters)
844

    
845
    def remove_unverified_providers(self, provider, **filters):
846
        try:
847
            existing = self.filter(module=provider, user__email_verified=False,
848
                                   **filters)
849
            for p in existing:
850
                p.user.delete()
851
        except:
852
            pass
853

    
854
    def unverified(self, provider, **filters):
855
        try:
856
            return self.get(module=provider, user__email_verified=False,
857
                            **filters).settings
858
        except AstakosUserAuthProvider.DoesNotExist:
859
            return None
860

    
861
    def verified(self, provider, **filters):
862
        try:
863
            return self.get(module=provider, user__email_verified=True,
864
                            **filters).settings
865
        except AstakosUserAuthProvider.DoesNotExist:
866
            return None
867

    
868

    
869
class AuthProviderPolicyProfileManager(models.Manager):
870

    
871
    def active(self):
872
        return self.filter(active=True)
873

    
874
    def for_user(self, user, provider):
875
        policies = {}
876
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
877
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
878
        exclusive_q = exclusive_q1 | exclusive_q2
879

    
880
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
881
            policies.update(profile.policies)
882

    
883
        user_groups = user.groups.all().values('pk')
884
        for profile in self.active().filter(groups__in=user_groups).filter(
885
                exclusive_q):
886
            policies.update(profile.policies)
887
        return policies
888

    
889
    def add_policy(self, name, provider, group_or_user, exclusive=False,
890
                   **policies):
891
        is_group = isinstance(group_or_user, Group)
892
        profile, created = self.get_or_create(name=name, provider=provider,
893
                                              is_exclusive=exclusive)
894
        profile.is_exclusive = exclusive
895
        profile.save()
896
        if is_group:
897
            profile.groups.add(group_or_user)
898
        else:
899
            profile.users.add(group_or_user)
900
        profile.set_policies(policies)
901
        profile.save()
902
        return profile
903

    
904

    
905
class AuthProviderPolicyProfile(models.Model):
906
    name = models.CharField(_('Name'), max_length=255, blank=False,
907
                            null=False, db_index=True)
908
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
909
                                null=False)
910

    
911
    # apply policies to all providers excluding the one set in provider field
912
    is_exclusive = models.BooleanField(default=False)
913

    
914
    policy_add = models.NullBooleanField(null=True, default=None)
915
    policy_remove = models.NullBooleanField(null=True, default=None)
916
    policy_create = models.NullBooleanField(null=True, default=None)
917
    policy_login = models.NullBooleanField(null=True, default=None)
918
    policy_limit = models.IntegerField(null=True, default=None)
919
    policy_required = models.NullBooleanField(null=True, default=None)
920
    policy_automoderate = models.NullBooleanField(null=True, default=None)
921
    policy_switch = models.NullBooleanField(null=True, default=None)
922

    
923
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
924
                     'automoderate')
925

    
926
    priority = models.IntegerField(null=False, default=1)
927
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
928
    users = models.ManyToManyField(AstakosUser,
929
                                   related_name='authpolicy_profiles')
930
    active = models.BooleanField(default=True)
931

    
932
    objects = AuthProviderPolicyProfileManager()
933

    
934
    class Meta:
935
        ordering = ['priority']
936

    
937
    @property
938
    def policies(self):
939
        policies = {}
940
        for pkey in self.POLICY_FIELDS:
941
            value = getattr(self, 'policy_%s' % pkey, None)
942
            if value is None:
943
                continue
944
            policies[pkey] = value
945
        return policies
946

    
947
    def set_policies(self, policies_dict):
948
        for key, value in policies_dict.iteritems():
949
            if key in self.POLICY_FIELDS:
950
                setattr(self, 'policy_%s' % key, value)
951
        return self.policies
952

    
953

    
954
class AstakosUserAuthProvider(models.Model):
955
    """
956
    Available user authentication methods.
957
    """
958
    affiliation = models.CharField(_('Affiliation'), max_length=255,
959
                                   blank=True, null=True, default=None)
960
    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
961
    module = models.CharField(_('Provider'), max_length=255, blank=False,
962
                              default='local')
963
    identifier = models.CharField(_('Third-party identifier'),
964
                                  max_length=255, null=True,
965
                                  blank=True)
966
    active = models.BooleanField(default=True)
967
    auth_backend = models.CharField(_('Backend'), max_length=255, blank=False,
968
                                    default='astakos')
969
    info_data = models.TextField(default="", null=True, blank=True)
970
    created = models.DateTimeField('Creation date', auto_now_add=True)
971

    
972
    objects = AstakosUserAuthProviderManager()
973

    
974
    class Meta:
975
        unique_together = (('identifier', 'module', 'user'), )
976
        ordering = ('module', 'created')
977

    
978
    def __init__(self, *args, **kwargs):
979
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
980
        try:
981
            self.info = json.loads(self.info_data)
982
            if not self.info:
983
                self.info = {}
984
        except Exception:
985
            self.info = {}
986

    
987
        for key, value in self.info.iteritems():
988
            setattr(self, 'info_%s' % key, value)
989

    
990
    @property
991
    def settings(self):
992
        extra_data = {}
993

    
994
        info_data = {}
995
        if self.info_data:
996
            info_data = json.loads(self.info_data)
997

    
998
        extra_data['info'] = info_data
999

    
1000
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
1001
            extra_data[key] = getattr(self, key)
1002

    
1003
        extra_data['instance'] = self
1004
        return auth.get_provider(self.module, self.user,
1005
                                 self.identifier, **extra_data)
1006

    
1007
    def __repr__(self):
1008
        return '<AstakosUserAuthProvider %s:%s>' % (
1009
            self.module, self.identifier)
1010

    
1011
    def __unicode__(self):
1012
        if self.identifier:
1013
            return "%s:%s" % (self.module, self.identifier)
1014
        if self.auth_backend:
1015
            return "%s:%s" % (self.module, self.auth_backend)
1016
        return self.module
1017

    
1018
    def save(self, *args, **kwargs):
1019
        self.info_data = json.dumps(self.info)
1020
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1021

    
1022

    
1023
class AstakosUserQuota(models.Model):
1024
    capacity = intDecimalField()
1025
    resource = models.ForeignKey(Resource)
1026
    user = models.ForeignKey(AstakosUser)
1027

    
1028
    class Meta:
1029
        unique_together = ("resource", "user")
1030

    
1031

    
1032
class ApprovalTerms(models.Model):
1033
    """
1034
    Model for approval terms
1035
    """
1036

    
1037
    date = models.DateTimeField(
1038
        _('Issue date'), db_index=True, auto_now_add=True)
1039
    location = models.CharField(_('Terms location'), max_length=255)
1040

    
1041

    
1042
class Invitation(models.Model):
1043
    """
1044
    Model for registring invitations
1045
    """
1046
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1047
                                null=True)
1048
    realname = models.CharField(_('Real name'), max_length=255)
1049
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1050
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1051
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1052
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1053
    consumed = models.DateTimeField(_('Consumption date'),
1054
                                    null=True, blank=True)
1055

    
1056
    def __init__(self, *args, **kwargs):
1057
        super(Invitation, self).__init__(*args, **kwargs)
1058
        if not self.id:
1059
            self.code = _generate_invitation_code()
1060

    
1061
    def consume(self):
1062
        self.is_consumed = True
1063
        self.consumed = datetime.now()
1064
        self.save()
1065

    
1066
    def __unicode__(self):
1067
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1068

    
1069

    
1070
class EmailChangeManager(models.Manager):
1071

    
1072
    @transaction.commit_on_success
1073
    def change_email(self, activation_key):
1074
        """
1075
        Validate an activation key and change the corresponding
1076
        ``User`` if valid.
1077

1078
        If the key is valid and has not expired, return the ``User``
1079
        after activating.
1080

1081
        If the key is not valid or has expired, return ``None``.
1082

1083
        If the key is valid but the ``User`` is already active,
1084
        return ``None``.
1085

1086
        After successful email change the activation record is deleted.
1087

1088
        Throws ValueError if there is already
1089
        """
1090
        try:
1091
            email_change = self.model.objects.get(
1092
                activation_key=activation_key)
1093
            if email_change.activation_key_expired():
1094
                email_change.delete()
1095
                raise EmailChange.DoesNotExist
1096
            # is there an active user with this address?
1097
            try:
1098
                AstakosUser.objects.get(
1099
                    email__iexact=email_change.new_email_address)
1100
            except AstakosUser.DoesNotExist:
1101
                pass
1102
            else:
1103
                raise ValueError(_('The new email address is reserved.'))
1104
            # update user
1105
            user = AstakosUser.objects.get(pk=email_change.user_id)
1106
            old_email = user.email
1107
            user.email = email_change.new_email_address
1108
            user.save()
1109
            email_change.delete()
1110
            msg = "User %s changed email from %s to %s"
1111
            logger.log(astakos_settings.LOGGING_LEVEL, msg, user.log_display,
1112
                       old_email, user.email)
1113
            return user
1114
        except EmailChange.DoesNotExist:
1115
            raise ValueError(_('Invalid activation key.'))
1116

    
1117

    
1118
class EmailChange(models.Model):
1119
    new_email_address = models.EmailField(
1120
        _(u'new e-mail address'),
1121
        help_text=_('Provide a new email address. Until you verify the new '
1122
                    'address by following the activation link that will be '
1123
                    'sent to it, your old email address will remain active.'))
1124
    user = models.ForeignKey(
1125
        AstakosUser, unique=True, related_name='emailchanges')
1126
    requested_at = models.DateTimeField(auto_now_add=True)
1127
    activation_key = models.CharField(
1128
        max_length=40, unique=True, db_index=True)
1129

    
1130
    objects = EmailChangeManager()
1131

    
1132
    def get_url(self):
1133
        return reverse('email_change_confirm',
1134
                       kwargs={'activation_key': self.activation_key})
1135

    
1136
    def activation_key_expired(self):
1137
        expiration_date = timedelta(
1138
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1139
        return self.requested_at + expiration_date < datetime.now()
1140

    
1141

    
1142
class AdditionalMail(models.Model):
1143
    """
1144
    Model for registring invitations
1145
    """
1146
    owner = models.ForeignKey(AstakosUser)
1147
    email = models.EmailField()
1148

    
1149

    
1150
def _generate_invitation_code():
1151
    while True:
1152
        code = randint(1, 2L ** 63 - 1)
1153
        try:
1154
            Invitation.objects.get(code=code)
1155
            # An invitation with this code already exists, try again
1156
        except Invitation.DoesNotExist:
1157
            return code
1158

    
1159

    
1160
def get_latest_terms():
1161
    try:
1162
        term = ApprovalTerms.objects.order_by('-id')[0]
1163
        return term
1164
    except IndexError:
1165
        pass
1166
    return None
1167

    
1168

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

    
1191
    class Meta:
1192
        unique_together = ("provider", "third_party_identifier")
1193

    
1194
    def get_user_instance(self):
1195
        """
1196
        Create a new AstakosUser instance based on details provided when user
1197
        initially signed up.
1198
        """
1199
        d = copy.copy(self.__dict__)
1200
        d.pop('_state', None)
1201
        d.pop('id', None)
1202
        d.pop('token', None)
1203
        d.pop('created', None)
1204
        d.pop('info', None)
1205
        d.pop('affiliation', None)
1206
        d.pop('provider', None)
1207
        d.pop('third_party_identifier', None)
1208
        user = AstakosUser(**d)
1209

    
1210
        return user
1211

    
1212
    @property
1213
    def realname(self):
1214
        return '%s %s' % (self.first_name, self.last_name)
1215

    
1216
    @realname.setter
1217
    def realname(self, value):
1218
        parts = value.split(' ')
1219
        if len(parts) == 2:
1220
            self.first_name = parts[0]
1221
            self.last_name = parts[1]
1222
        else:
1223
            self.last_name = parts[0]
1224

    
1225
    def save(self, *args, **kwargs):
1226
        if not self.id:
1227
            # set username
1228
            while not self.username:
1229
                username = uuid.uuid4().hex[:30]
1230
                try:
1231
                    AstakosUser.objects.get(username=username)
1232
                except AstakosUser.DoesNotExist:
1233
                    self.username = username
1234
        super(PendingThirdPartyUser, self).save(*args, **kwargs)
1235

    
1236
    def generate_token(self):
1237
        self.password = self.third_party_identifier
1238
        self.last_login = datetime.now()
1239
        self.token = default_token_generator.make_token(self)
1240

    
1241
    def existing_user(self):
1242
        return AstakosUser.objects.filter(
1243
            auth_providers__module=self.provider,
1244
            auth_providers__identifier=self.third_party_identifier)
1245

    
1246
    def get_provider(self, user):
1247
        params = {
1248
            'info_data': self.info,
1249
            'affiliation': self.affiliation
1250
        }
1251
        return auth.get_provider(self.provider, user,
1252
                                 self.third_party_identifier, **params)
1253

    
1254

    
1255
class SessionCatalog(models.Model):
1256
    session_key = models.CharField(_('session key'), max_length=40)
1257
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1258

    
1259

    
1260
class UserSetting(models.Model):
1261
    user = models.ForeignKey(AstakosUser)
1262
    setting = models.CharField(max_length=255)
1263
    value = models.IntegerField()
1264

    
1265
    class Meta:
1266
        unique_together = ("user", "setting")
1267

    
1268

    
1269
### PROJECTS ###
1270
################
1271

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

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

    
1278

    
1279
def new_chain():
1280
    c = Chain.objects.create()
1281
    return c
1282

    
1283

    
1284
class ProjectApplicationManager(models.Manager):
1285

    
1286
    def pending_per_project(self, projects):
1287
        apps = self.filter(state=self.model.PENDING,
1288
                           chain__in=projects).order_by('chain', '-id')
1289
        checked_chain = None
1290
        projs = {}
1291
        for app in apps:
1292
            chain = app.chain_id
1293
            if chain != checked_chain:
1294
                checked_chain = chain
1295
                projs[chain] = app
1296
        return projs
1297

    
1298

    
1299
class ProjectApplication(models.Model):
1300
    applicant = models.ForeignKey(
1301
        AstakosUser,
1302
        related_name='projects_applied',
1303
        db_index=True)
1304

    
1305
    PENDING = 0
1306
    APPROVED = 1
1307
    REPLACED = 2
1308
    DENIED = 3
1309
    DISMISSED = 4
1310
    CANCELLED = 5
1311

    
1312
    state = models.IntegerField(default=PENDING,
1313
                                db_index=True)
1314
    owner = models.ForeignKey(
1315
        AstakosUser,
1316
        related_name='projects_owned',
1317
        db_index=True)
1318
    chain = models.ForeignKey('Project',
1319
                              related_name='chained_apps',
1320
                              db_column='chain')
1321
    name = models.CharField(max_length=80)
1322
    homepage = models.URLField(max_length=255, null=True,
1323
                               verify_exists=False)
1324
    description = models.TextField(null=True, blank=True)
1325
    start_date = models.DateTimeField(null=True, blank=True)
1326
    end_date = models.DateTimeField()
1327
    member_join_policy = models.IntegerField()
1328
    member_leave_policy = models.IntegerField()
1329
    limit_on_members_number = models.PositiveIntegerField(null=True)
1330
    resource_grants = models.ManyToManyField(
1331
        Resource,
1332
        null=True,
1333
        blank=True,
1334
        through='ProjectResourceGrant')
1335
    comments = models.TextField(null=True, blank=True)
1336
    issue_date = models.DateTimeField(auto_now_add=True)
1337
    response_date = models.DateTimeField(null=True, blank=True)
1338
    response = models.TextField(null=True, blank=True)
1339
    response_actor = models.ForeignKey(AstakosUser, null=True,
1340
                                       related_name='responded_apps')
1341
    waive_date = models.DateTimeField(null=True, blank=True)
1342
    waive_reason = models.TextField(null=True, blank=True)
1343
    waive_actor = models.ForeignKey(AstakosUser, null=True,
1344
                                    related_name='waived_apps')
1345

    
1346
    objects = ProjectApplicationManager()
1347

    
1348
    # Compiled queries
1349
    Q_PENDING = Q(state=PENDING)
1350
    Q_APPROVED = Q(state=APPROVED)
1351
    Q_DENIED = Q(state=DENIED)
1352

    
1353
    class Meta:
1354
        unique_together = ("chain", "id")
1355

    
1356
    def __unicode__(self):
1357
        return "%s applied by %s" % (self.name, self.applicant)
1358

    
1359
    # TODO: Move to a more suitable place
1360
    APPLICATION_STATE_DISPLAY = {
1361
        PENDING:   _('Pending review'),
1362
        APPROVED:  _('Approved'),
1363
        REPLACED:  _('Replaced'),
1364
        DENIED:    _('Denied'),
1365
        DISMISSED: _('Dismissed'),
1366
        CANCELLED: _('Cancelled')
1367
    }
1368

    
1369
    @property
1370
    def log_display(self):
1371
        return "application %s (%s) for project %s" % (
1372
            self.id, self.name, self.chain)
1373

    
1374
    def state_display(self):
1375
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1376

    
1377
    @property
1378
    def grants(self):
1379
        return self.projectresourcegrant_set.values('member_capacity',
1380
                                                    'resource__name')
1381

    
1382
    @property
1383
    def resource_policies(self):
1384
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1385

    
1386
    def is_modification(self):
1387
        # if self.state != self.PENDING:
1388
        #     return False
1389
        parents = self.chained_applications().filter(id__lt=self.id)
1390
        parents = parents.filter(state__in=[self.APPROVED])
1391
        return parents.count() > 0
1392

    
1393
    def chained_applications(self):
1394
        return ProjectApplication.objects.filter(chain=self.chain)
1395

    
1396
    def denied_modifications(self):
1397
        q = self.chained_applications()
1398
        q = q.filter(Q(state=self.DENIED))
1399
        q = q.filter(~Q(id=self.id))
1400
        return q
1401

    
1402
    def last_denied(self):
1403
        try:
1404
            return self.denied_modifications().order_by('-id')[0]
1405
        except IndexError:
1406
            return None
1407

    
1408
    def has_denied_modifications(self):
1409
        return bool(self.last_denied())
1410

    
1411
    def can_cancel(self):
1412
        return self.state == self.PENDING
1413

    
1414
    def cancel(self, actor=None, reason=None):
1415
        if not self.can_cancel():
1416
            m = _("cannot cancel: application '%s' in state '%s'") % (
1417
                self.id, self.state)
1418
            raise AssertionError(m)
1419

    
1420
        self.state = self.CANCELLED
1421
        self.waive_date = datetime.now()
1422
        self.waive_reason = reason
1423
        self.waive_actor = actor
1424
        self.save()
1425

    
1426
    def can_dismiss(self):
1427
        return self.state == self.DENIED
1428

    
1429
    def dismiss(self, actor=None, reason=None):
1430
        if not self.can_dismiss():
1431
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1432
                self.id, self.state)
1433
            raise AssertionError(m)
1434

    
1435
        self.state = self.DISMISSED
1436
        self.waive_date = datetime.now()
1437
        self.waive_reason = reason
1438
        self.waive_actor = actor
1439
        self.save()
1440

    
1441
    def can_deny(self):
1442
        return self.state == self.PENDING
1443

    
1444
    def deny(self, actor=None, reason=None):
1445
        if not self.can_deny():
1446
            m = _("cannot deny: application '%s' in state '%s'") % (
1447
                self.id, self.state)
1448
            raise AssertionError(m)
1449

    
1450
        self.state = self.DENIED
1451
        self.response_date = datetime.now()
1452
        self.response = reason
1453
        self.response_actor = actor
1454
        self.save()
1455

    
1456
    def can_approve(self):
1457
        return self.state == self.PENDING
1458

    
1459
    def approve(self, actor=None, reason=None):
1460
        if not self.can_approve():
1461
            m = _("cannot approve: project '%s' in state '%s'") % (
1462
                self.name, self.state)
1463
            raise AssertionError(m)  # invalid argument
1464

    
1465
        now = datetime.now()
1466
        self.state = self.APPROVED
1467
        self.response_date = now
1468
        self.response = reason
1469
        self.response_actor = actor
1470
        self.save()
1471

    
1472
    @property
1473
    def member_join_policy_display(self):
1474
        policy = self.member_join_policy
1475
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1476

    
1477
    @property
1478
    def member_leave_policy_display(self):
1479
        policy = self.member_leave_policy
1480
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1481

    
1482

    
1483
class ProjectResourceGrantManager(models.Manager):
1484
    def grants_per_app(self, applications):
1485
        app_ids = [app.id for app in applications]
1486
        grants = self.filter(
1487
            project_application__in=app_ids).select_related("resource")
1488
        return _partition_by(lambda g: g.project_application_id, grants)
1489

    
1490

    
1491
class ProjectResourceGrant(models.Model):
1492

    
1493
    resource = models.ForeignKey(Resource)
1494
    project_application = models.ForeignKey(ProjectApplication,
1495
                                            null=True)
1496
    project_capacity = intDecimalField(null=True)
1497
    member_capacity = intDecimalField(default=0)
1498

    
1499
    objects = ProjectResourceGrantManager()
1500

    
1501
    class Meta:
1502
        unique_together = ("resource", "project_application")
1503

    
1504
    def display_member_capacity(self):
1505
        return units.show(self.member_capacity, self.resource.unit)
1506

    
1507
    def __str__(self):
1508
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1509
                                        self.display_member_capacity())
1510

    
1511

    
1512
def _distinct(f, l):
1513
    d = {}
1514
    last = None
1515
    for x in l:
1516
        group = f(x)
1517
        if group == last:
1518
            continue
1519
        last = group
1520
        d[group] = x
1521
    return d
1522

    
1523

    
1524
def invert_dict(d):
1525
    return dict((v, k) for k, v in d.iteritems())
1526

    
1527

    
1528
class ProjectManager(models.Manager):
1529

    
1530
    def all_with_pending(self, flt=None):
1531
        flt = Q() if flt is None else flt
1532
        projects = list(self.select_related(
1533
            'application', 'application__owner').filter(flt))
1534

    
1535
        objs = ProjectApplication.objects.select_related('owner')
1536
        apps = objs.filter(state=ProjectApplication.PENDING,
1537
                           chain__in=projects).order_by('chain', '-id')
1538
        app_d = _distinct(lambda app: app.chain_id, apps)
1539
        return [(project, app_d.get(project.pk)) for project in projects]
1540

    
1541
    def expired_projects(self):
1542
        model = self.model
1543
        q = ((model.o_state_q(model.O_ACTIVE) |
1544
              model.o_state_q(model.O_SUSPENDED)) &
1545
             Q(application__end_date__lt=datetime.now()))
1546
        return self.filter(q)
1547

    
1548
    def user_accessible_projects(self, user):
1549
        """
1550
        Return projects accessible by specified user.
1551
        """
1552
        model = self.model
1553
        if user.is_project_admin():
1554
            flt = Q()
1555
        else:
1556
            membs = user.projectmembership_set.associated()
1557
            memb_projects = membs.values_list("project", flat=True)
1558
            flt = (Q(application__owner=user) |
1559
                   Q(application__applicant=user) |
1560
                   Q(id__in=memb_projects))
1561

    
1562
        relevant = model.o_states_q(model.RELEVANT_STATES)
1563
        return self.filter(flt, relevant).order_by(
1564
            'application__issue_date').select_related(
1565
            'application', 'application__owner', 'application__applicant')
1566

    
1567
    def search_by_name(self, *search_strings):
1568
        q = Q()
1569
        for s in search_strings:
1570
            q = q | Q(name__icontains=s)
1571
        return self.filter(q)
1572

    
1573

    
1574
class Project(models.Model):
1575

    
1576
    id = models.BigIntegerField(db_column='id', primary_key=True)
1577

    
1578
    application = models.OneToOneField(
1579
        ProjectApplication,
1580
        related_name='project')
1581

    
1582
    members = models.ManyToManyField(
1583
        AstakosUser,
1584
        through='ProjectMembership')
1585

    
1586
    creation_date = models.DateTimeField(auto_now_add=True)
1587
    name = models.CharField(
1588
        max_length=80,
1589
        null=True,
1590
        db_index=True,
1591
        unique=True)
1592

    
1593
    NORMAL = 1
1594
    SUSPENDED = 10
1595
    TERMINATED = 100
1596

    
1597
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1598

    
1599
    state = models.IntegerField(default=NORMAL,
1600
                                db_index=True)
1601

    
1602
    objects = ProjectManager()
1603

    
1604
    def __str__(self):
1605
        return uenc(_("<project %s '%s'>") %
1606
                    (self.id, udec(self.application.name)))
1607

    
1608
    __repr__ = __str__
1609

    
1610
    def __unicode__(self):
1611
        return _("<project %s '%s'>") % (self.id, self.application.name)
1612

    
1613
    O_PENDING = 0
1614
    O_ACTIVE = 1
1615
    O_DENIED = 3
1616
    O_DISMISSED = 4
1617
    O_CANCELLED = 5
1618
    O_SUSPENDED = 10
1619
    O_TERMINATED = 100
1620

    
1621
    O_STATE_DISPLAY = {
1622
        O_PENDING:    _("Pending"),
1623
        O_ACTIVE:     _("Active"),
1624
        O_DENIED:     _("Denied"),
1625
        O_DISMISSED:  _("Dismissed"),
1626
        O_CANCELLED:  _("Cancelled"),
1627
        O_SUSPENDED:  _("Suspended"),
1628
        O_TERMINATED: _("Terminated"),
1629
    }
1630

    
1631
    OVERALL_STATE = {
1632
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1633
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1634
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1635
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1636
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1637
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1638
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1639
    }
1640

    
1641
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1642

    
1643
    @classmethod
1644
    def o_state_q(cls, o_state):
1645
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1646
        return Q(state=p_state, application__state=a_state)
1647

    
1648
    @classmethod
1649
    def o_states_q(cls, o_states):
1650
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1651

    
1652
    INITIALIZED_STATES = [O_ACTIVE,
1653
                          O_SUSPENDED,
1654
                          O_TERMINATED,
1655
                          ]
1656

    
1657
    RELEVANT_STATES = [O_PENDING,
1658
                       O_DENIED,
1659
                       O_ACTIVE,
1660
                       O_SUSPENDED,
1661
                       O_TERMINATED,
1662
                       ]
1663

    
1664
    SKIP_STATES = [O_DISMISSED,
1665
                   O_CANCELLED,
1666
                   O_TERMINATED,
1667
                   ]
1668

    
1669
    @classmethod
1670
    def _overall_state(cls, project_state, app_state):
1671
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1672

    
1673
    def overall_state(self):
1674
        return self._overall_state(self.state, self.application.state)
1675

    
1676
    def last_pending_application(self):
1677
        apps = self.chained_apps.filter(
1678
            state=ProjectApplication.PENDING).order_by('-id')
1679
        if apps:
1680
            return apps[0]
1681
        return None
1682

    
1683
    def last_pending_modification(self):
1684
        last_pending = self.last_pending_application()
1685
        if last_pending == self.application:
1686
            return None
1687
        return last_pending
1688

    
1689
    def state_display(self):
1690
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1691

    
1692
    def expiration_info(self):
1693
        return (str(self.id), self.name, self.state_display(),
1694
                str(self.application.end_date))
1695

    
1696
    def last_deactivation(self):
1697
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1698
        ls = objs.order_by("-date")
1699
        if not ls:
1700
            return None
1701
        return ls[0]
1702

    
1703
    def is_deactivated(self, reason=None):
1704
        if reason is not None:
1705
            return self.state == reason
1706

    
1707
        return self.state != self.NORMAL
1708

    
1709
    def is_active(self):
1710
        return self.overall_state() == self.O_ACTIVE
1711

    
1712
    def is_initialized(self):
1713
        return self.overall_state() in self.INITIALIZED_STATES
1714

    
1715
    ### Deactivation calls
1716

    
1717
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1718
                    comments=None):
1719
        now = datetime.now()
1720
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1721
                        actor=actor, reason=reason, comments=comments)
1722

    
1723
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1724
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1725
                         comments=comments)
1726
        self.state = to_state
1727
        self.save()
1728

    
1729
    def terminate(self, actor=None, reason=None):
1730
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1731
        self.name = None
1732
        self.save()
1733

    
1734
    def suspend(self, actor=None, reason=None):
1735
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1736

    
1737
    def resume(self, actor=None, reason=None):
1738
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1739
        if self.name is None:
1740
            self.name = self.application.name
1741
            self.save()
1742

    
1743
    ### Logical checks
1744

    
1745
    @property
1746
    def is_alive(self):
1747
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1748

    
1749
    @property
1750
    def is_terminated(self):
1751
        return self.is_deactivated(self.TERMINATED)
1752

    
1753
    @property
1754
    def is_suspended(self):
1755
        return self.is_deactivated(self.SUSPENDED)
1756

    
1757
    def violates_members_limit(self, adding=0):
1758
        application = self.application
1759
        limit = application.limit_on_members_number
1760
        if limit is None:
1761
            return False
1762
        return (len(self.approved_members) + adding > limit)
1763

    
1764
    ### Other
1765

    
1766
    def count_pending_memberships(self):
1767
        return self.projectmembership_set.requested().count()
1768

    
1769
    def members_count(self):
1770
        return self.approved_memberships.count()
1771

    
1772
    @property
1773
    def approved_memberships(self):
1774
        query = ProjectMembership.Q_ACCEPTED_STATES
1775
        return self.projectmembership_set.filter(query)
1776

    
1777
    @property
1778
    def approved_members(self):
1779
        return [m.person for m in self.approved_memberships]
1780

    
1781

    
1782
class ProjectLogManager(models.Manager):
1783
    def last_deactivations(self, projects):
1784
        logs = self.filter(
1785
            project__in=projects,
1786
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1787
        return first_of_group(lambda l: l.project_id, logs)
1788

    
1789

    
1790
class ProjectLog(models.Model):
1791
    project = models.ForeignKey(Project, related_name="log")
1792
    from_state = models.IntegerField(null=True)
1793
    to_state = models.IntegerField()
1794
    date = models.DateTimeField()
1795
    actor = models.ForeignKey(AstakosUser, null=True)
1796
    reason = models.TextField(null=True)
1797
    comments = models.TextField(null=True)
1798

    
1799
    objects = ProjectLogManager()
1800

    
1801

    
1802
class ProjectLock(models.Model):
1803
    pass
1804

    
1805

    
1806
class ProjectMembershipManager(models.Manager):
1807

    
1808
    def any_accepted(self):
1809
        q = self.model.Q_ACCEPTED_STATES
1810
        return self.filter(q)
1811

    
1812
    def actually_accepted(self):
1813
        q = self.model.Q_ACTUALLY_ACCEPTED
1814
        return self.filter(q)
1815

    
1816
    def requested(self):
1817
        return self.filter(state=ProjectMembership.REQUESTED)
1818

    
1819
    def suspended(self):
1820
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1821

    
1822
    def associated(self):
1823
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1824

    
1825
    def any_accepted_per_project(self, projects):
1826
        ms = self.any_accepted().filter(project__in=projects)
1827
        return _partition_by(lambda m: m.project_id, ms)
1828

    
1829
    def requested_per_project(self, projects):
1830
        ms = self.requested().filter(project__in=projects)
1831
        return _partition_by(lambda m: m.project_id, ms)
1832

    
1833
    def one_per_project(self):
1834
        ms = self.all().select_related(
1835
            'project', 'project__application',
1836
            'project__application__owner', 'project_application__applicant',
1837
            'person')
1838
        m_per_p = {}
1839
        for m in ms:
1840
            m_per_p[m.project_id] = m
1841
        return m_per_p
1842

    
1843

    
1844
class ProjectMembership(models.Model):
1845

    
1846
    person = models.ForeignKey(AstakosUser)
1847
    project = models.ForeignKey(Project)
1848

    
1849
    REQUESTED = 0
1850
    ACCEPTED = 1
1851
    LEAVE_REQUESTED = 5
1852
    # User deactivation
1853
    USER_SUSPENDED = 10
1854
    REJECTED = 100
1855
    CANCELLED = 101
1856
    REMOVED = 200
1857

    
1858
    ASSOCIATED_STATES = set([REQUESTED,
1859
                             ACCEPTED,
1860
                             LEAVE_REQUESTED,
1861
                             USER_SUSPENDED,
1862
                             ])
1863

    
1864
    ACCEPTED_STATES = set([ACCEPTED,
1865
                           LEAVE_REQUESTED,
1866
                           USER_SUSPENDED,
1867
                           ])
1868

    
1869
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1870

    
1871
    state = models.IntegerField(default=REQUESTED,
1872
                                db_index=True)
1873

    
1874
    objects = ProjectMembershipManager()
1875

    
1876
    # Compiled queries
1877
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1878
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1879

    
1880
    MEMBERSHIP_STATE_DISPLAY = {
1881
        REQUESTED:       _('Requested'),
1882
        ACCEPTED:        _('Accepted'),
1883
        LEAVE_REQUESTED: _('Leave Requested'),
1884
        USER_SUSPENDED:  _('Suspended'),
1885
        REJECTED:        _('Rejected'),
1886
        CANCELLED:       _('Cancelled'),
1887
        REMOVED:         _('Removed'),
1888
    }
1889

    
1890
    USER_FRIENDLY_STATE_DISPLAY = {
1891
        REQUESTED:       _('Join requested'),
1892
        ACCEPTED:        _('Accepted member'),
1893
        LEAVE_REQUESTED: _('Requested to leave'),
1894
        USER_SUSPENDED:  _('Suspended member'),
1895
        REJECTED:        _('Request rejected'),
1896
        CANCELLED:       _('Request cancelled'),
1897
        REMOVED:         _('Removed member'),
1898
    }
1899

    
1900
    def state_display(self):
1901
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1902

    
1903
    def user_friendly_state_display(self):
1904
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1905

    
1906
    class Meta:
1907
        unique_together = ("person", "project")
1908
        #index_together = [["project", "state"]]
1909

    
1910
    def __str__(self):
1911
        return uenc(_("<'%s' membership in '%s'>") %
1912
                    (self.person.username, self.project))
1913

    
1914
    __repr__ = __str__
1915

    
1916
    def latest_log(self):
1917
        logs = self.log.all()
1918
        logs_d = _partition_by(lambda l: l.to_state, logs)
1919
        for s, s_logs in logs_d.iteritems():
1920
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1921
        return logs_d
1922

    
1923
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1924
                    comments=None):
1925
        now = datetime.now()
1926
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1927
                        actor=actor, reason=reason, comments=comments)
1928

    
1929
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1930
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1931
                         comments=comments)
1932
        self.state = to_state
1933
        self.save()
1934

    
1935
    ACTION_CHECKS = {
1936
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
1937
        "accept": lambda m: m.state == m.REQUESTED,
1938
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
1939
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
1940
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
1941
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1942
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1943
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
1944
        "reject": lambda m: m.state == m.REQUESTED,
1945
        "cancel": lambda m: m.state == m.REQUESTED,
1946
    }
1947

    
1948
    ACTION_STATES = {
1949
        "join":          REQUESTED,
1950
        "accept":        ACCEPTED,
1951
        "enroll":        ACCEPTED,
1952
        "leave_request": LEAVE_REQUESTED,
1953
        "deny_leave":    ACCEPTED,
1954
        "cancel_leave":  ACCEPTED,
1955
        "remove":        REMOVED,
1956
        "reject":        REJECTED,
1957
        "cancel":        CANCELLED,
1958
    }
1959

    
1960
    def check_action(self, action):
1961
        try:
1962
            check = self.ACTION_CHECKS[action]
1963
        except KeyError:
1964
            raise ValueError("No check found for action '%s'" % action)
1965
        return check(self)
1966

    
1967
    def perform_action(self, action, actor=None, reason=None):
1968
        if not self.check_action(action):
1969
            m = _("%s: attempted action '%s' in state '%s'") % (
1970
                self, action, self.state)
1971
            raise AssertionError(m)
1972
        try:
1973
            s = self.ACTION_STATES[action]
1974
        except KeyError:
1975
            raise ValueError("No such action '%s'" % action)
1976
        return self.set_state(s, actor=actor, reason=reason)
1977

    
1978

    
1979
class ProjectMembershipLogManager(models.Manager):
1980
    def last_logs(self, memberships):
1981
        logs = self.filter(membership__in=memberships).order_by("-date")
1982
        logs = _partition_by(lambda l: l.membership_id, logs)
1983

    
1984
        for memb_id, m_logs in logs.iteritems():
1985
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
1986
        return logs
1987

    
1988

    
1989
class ProjectMembershipLog(models.Model):
1990
    membership = models.ForeignKey(ProjectMembership, related_name="log")
1991
    from_state = models.IntegerField(null=True)
1992
    to_state = models.IntegerField()
1993
    date = models.DateTimeField()
1994
    actor = models.ForeignKey(AstakosUser, null=True)
1995
    reason = models.TextField(null=True)
1996
    comments = models.TextField(null=True)
1997

    
1998
    objects = ProjectMembershipLogManager()
1999

    
2000

    
2001
### SIGNALS ###
2002
################
2003

    
2004
def create_astakos_user(u):
2005
    try:
2006
        AstakosUser.objects.get(user_ptr=u.pk)
2007
    except AstakosUser.DoesNotExist:
2008
        extended_user = AstakosUser(user_ptr_id=u.pk)
2009
        extended_user.__dict__.update(u.__dict__)
2010
        extended_user.save()
2011
        if not extended_user.has_auth_provider('local'):
2012
            extended_user.add_auth_provider('local')
2013
    except BaseException, e:
2014
        logger.exception(e)
2015

    
2016

    
2017
def fix_superusers():
2018
    # Associate superusers with AstakosUser
2019
    admins = User.objects.filter(is_superuser=True)
2020
    for u in admins:
2021
        create_astakos_user(u)
2022

    
2023

    
2024
def user_post_save(sender, instance, created, **kwargs):
2025
    if not created:
2026
        return
2027
    create_astakos_user(instance)
2028
post_save.connect(user_post_save, sender=User)
2029

    
2030

    
2031
def astakosuser_post_save(sender, instance, created, **kwargs):
2032
    pass
2033

    
2034
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2035

    
2036

    
2037
def resource_post_save(sender, instance, created, **kwargs):
2038
    pass
2039

    
2040
post_save.connect(resource_post_save, sender=Resource)
2041

    
2042

    
2043
def renew_token(sender, instance, **kwargs):
2044
    if not instance.auth_token:
2045
        instance.renew_token()
2046
pre_save.connect(renew_token, sender=AstakosUser)
2047
pre_save.connect(renew_token, sender=Component)