Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ ef1fb98b

History | View | Annotate | Download (66.8 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 synnefo.util.text import uenc, udec
68
from synnefo.util import units
69
from astakos.im import presentation
70

    
71
logger = logging.getLogger(__name__)
72

    
73
DEFAULT_CONTENT_TYPE = None
74
_content_type = None
75

    
76

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

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

    
90
inf = float('inf')
91

    
92

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

    
97

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

    
107

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

    
119

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

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

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

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

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

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

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

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

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

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

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

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

    
196

    
197
_presentation_data = {}
198

    
199

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

    
209

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

    
215

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

    
219

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

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

    
228

    
229
class Resource(models.Model):
230
    name = models.CharField(_('Name'), max_length=255, unique=True)
231
    desc = models.TextField(_('Description'), null=True)
232
    service_type = models.CharField(_('Type'), max_length=255)
233
    service_origin = models.CharField(max_length=255, db_index=True)
234
    unit = models.CharField(_('Unit'), null=True, max_length=255)
235
    uplimit = models.BigIntegerField(default=0)
236
    ui_visible = models.BooleanField(default=True)
237
    api_visible = 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
                'ui_visible': self.ui_visible,
250
                'api_visible': self.api_visible,
251
                }
252

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

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

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

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

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

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

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

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

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

    
298

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

    
305

    
306
class AstakosUserManager(UserManager):
307

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

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

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

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

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

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

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

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

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

    
364

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
458
    objects = AstakosUserManager()
459

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

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

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

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

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

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

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

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

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

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

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

    
523
    def get_resource_policy(self, resource):
524
        return AstakosUserQuota.objects.select_related("resource").\
525
            get(user=self, resource__name=resource)
526

    
527
    def update_uuid(self):
528
        while not self.uuid:
529
            uuid_val = str(uuid.uuid4())
530
            try:
531
                AstakosUser.objects.get(uuid=uuid_val)
532
            except AstakosUser.DoesNotExist:
533
                self.uuid = uuid_val
534
        return self.uuid
535

    
536
    def save(self, update_timestamps=True, **kwargs):
537
        if update_timestamps:
538
            if not self.id:
539
                self.date_joined = datetime.now()
540
            self.updated = datetime.now()
541

    
542
        self.update_uuid()
543

    
544
        if not self.verification_code:
545
            self.renew_verification_code()
546

    
547
        # username currently matches email
548
        if self.username != self.email.lower():
549
            self.username = self.email.lower()
550

    
551
        super(AstakosUser, self).save(**kwargs)
552

    
553
    def renew_verification_code(self):
554
        self.verification_code = str(uuid.uuid4())
555
        logger.info("Verification code renewed for %s" % self.log_display)
556

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

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

    
576
    def token_expired(self):
577
        return self.auth_token_expires < datetime.now()
578

    
579
    def flush_sessions(self, current_key=None):
580
        q = self.sessions
581
        if current_key:
582
            q = q.exclude(session_key=current_key)
583

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

    
593
    def __unicode__(self):
594
        return '%s (%s)' % (self.realname, self.email)
595

    
596
    def conflicting_email(self):
597
        q = AstakosUser.objects.exclude(username=self.username)
598
        q = q.filter(email__iexact=self.email)
599
        if q.count() != 0:
600
            return True
601
        return False
602

    
603
    def email_change_is_pending(self):
604
        return self.emailchanges.count() > 0
605

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

    
624
        if self.moderated and not self.is_rejected:
625
            if self.accepted_policy == 'manual':
626
                msg += " (manually accepted)"
627
            else:
628
                msg += " (accepted policy: %s)" % \
629
                    self.accepted_policy
630
        return msg
631

    
632
    @property
633
    def signed_terms(self):
634
        return self.has_signed_terms
635

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

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

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

    
651
        local = self.get_auth_provider('local')._instance
652
        return local.auth_backend == 'astakos'
653

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

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

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

    
669
    def get_required_providers(self, **kwargs):
670
        return auth.REQUIRED_PROVIDERS.keys()
671

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

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

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

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

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

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

    
717
        modules = astakos_settings.IM_MODULES
718

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

    
724
        providers = sorted(providers, key=key)
725
        return providers
726

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

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

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

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

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

    
752
    def get_inactive_message(self, provider_module, identifier=None):
753
        provider = self.get_auth_provider(provider_module, identifier)
754

    
755
        msg_extra = ''
756
        message = ''
757

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

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

    
781
        return mark_safe(message + u' ' + msg_extra)
782

    
783
    def owns_application(self, application):
784
        return application.owner == self
785

    
786
    def owns_project(self, project):
787
        return project.application.owner == self
788

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

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

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

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

    
823

    
824
class AstakosUserAuthProviderManager(models.Manager):
825

    
826
    def active(self, **filters):
827
        return self.filter(active=True, **filters)
828

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

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

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

    
852

    
853
class AuthProviderPolicyProfileManager(models.Manager):
854

    
855
    def active(self):
856
        return self.filter(active=True)
857

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

    
864
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
865
            policies.update(profile.policies)
866

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

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

    
888

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

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

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

    
907
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
908
                     'automoderate')
909

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

    
916
    objects = AuthProviderPolicyProfileManager()
917

    
918
    class Meta:
919
        ordering = ['priority']
920

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

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

    
937

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

    
956
    objects = AstakosUserAuthProviderManager()
957

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

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

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

    
974
    @property
975
    def settings(self):
976
        extra_data = {}
977

    
978
        info_data = {}
979
        if self.info_data:
980
            info_data = json.loads(self.info_data)
981

    
982
        extra_data['info'] = info_data
983

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

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

    
991
    def __repr__(self):
992
        return '<AstakosUserAuthProvider %s:%s>' % (
993
            self.module, self.identifier)
994

    
995
    def __unicode__(self):
996
        if self.identifier:
997
            return "%s:%s" % (self.module, self.identifier)
998
        if self.auth_backend:
999
            return "%s:%s" % (self.module, self.auth_backend)
1000
        return self.module
1001

    
1002
    def save(self, *args, **kwargs):
1003
        self.info_data = json.dumps(self.info)
1004
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1005

    
1006

    
1007
class AstakosUserQuota(models.Model):
1008
    capacity = models.BigIntegerField()
1009
    resource = models.ForeignKey(Resource)
1010
    user = models.ForeignKey(AstakosUser)
1011

    
1012
    class Meta:
1013
        unique_together = ("resource", "user")
1014

    
1015

    
1016
class ApprovalTerms(models.Model):
1017
    """
1018
    Model for approval terms
1019
    """
1020

    
1021
    date = models.DateTimeField(
1022
        _('Issue date'), db_index=True, auto_now_add=True)
1023
    location = models.CharField(_('Terms location'), max_length=255)
1024

    
1025

    
1026
class Invitation(models.Model):
1027
    """
1028
    Model for registring invitations
1029
    """
1030
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1031
                                null=True)
1032
    realname = models.CharField(_('Real name'), max_length=255)
1033
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1034
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1035
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1036
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1037
    consumed = models.DateTimeField(_('Consumption date'),
1038
                                    null=True, blank=True)
1039

    
1040
    def __init__(self, *args, **kwargs):
1041
        super(Invitation, self).__init__(*args, **kwargs)
1042
        if not self.id:
1043
            self.code = _generate_invitation_code()
1044

    
1045
    def consume(self):
1046
        self.is_consumed = True
1047
        self.consumed = datetime.now()
1048
        self.save()
1049

    
1050
    def __unicode__(self):
1051
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1052

    
1053

    
1054
class EmailChangeManager(models.Manager):
1055

    
1056
    @transaction.commit_on_success
1057
    def change_email(self, activation_key):
1058
        """
1059
        Validate an activation key and change the corresponding
1060
        ``User`` if valid.
1061

1062
        If the key is valid and has not expired, return the ``User``
1063
        after activating.
1064

1065
        If the key is not valid or has expired, return ``None``.
1066

1067
        If the key is valid but the ``User`` is already active,
1068
        return ``None``.
1069

1070
        After successful email change the activation record is deleted.
1071

1072
        Throws ValueError if there is already
1073
        """
1074
        try:
1075
            email_change = self.model.objects.get(
1076
                activation_key=activation_key)
1077
            if email_change.activation_key_expired():
1078
                email_change.delete()
1079
                raise EmailChange.DoesNotExist
1080
            # is there an active user with this address?
1081
            try:
1082
                AstakosUser.objects.get(
1083
                    email__iexact=email_change.new_email_address)
1084
            except AstakosUser.DoesNotExist:
1085
                pass
1086
            else:
1087
                raise ValueError(_('The new email address is reserved.'))
1088
            # update user
1089
            user = AstakosUser.objects.get(pk=email_change.user_id)
1090
            old_email = user.email
1091
            user.email = email_change.new_email_address
1092
            user.save()
1093
            email_change.delete()
1094
            msg = "User %s changed email from %s to %s"
1095
            logger.log(astakos_settings.LOGGING_LEVEL, msg, user.log_display,
1096
                       old_email, user.email)
1097
            return user
1098
        except EmailChange.DoesNotExist:
1099
            raise ValueError(_('Invalid activation key.'))
1100

    
1101

    
1102
class EmailChange(models.Model):
1103
    new_email_address = models.EmailField(
1104
        _(u'new e-mail address'),
1105
        help_text=_('Provide a new email address. Until you verify the new '
1106
                    'address by following the activation link that will be '
1107
                    'sent to it, your old email address will remain active.'))
1108
    user = models.ForeignKey(
1109
        AstakosUser, unique=True, related_name='emailchanges')
1110
    requested_at = models.DateTimeField(auto_now_add=True)
1111
    activation_key = models.CharField(
1112
        max_length=40, unique=True, db_index=True)
1113

    
1114
    objects = EmailChangeManager()
1115

    
1116
    def get_url(self):
1117
        return reverse('email_change_confirm',
1118
                       kwargs={'activation_key': self.activation_key})
1119

    
1120
    def activation_key_expired(self):
1121
        expiration_date = timedelta(
1122
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1123
        return self.requested_at + expiration_date < datetime.now()
1124

    
1125

    
1126
class AdditionalMail(models.Model):
1127
    """
1128
    Model for registring invitations
1129
    """
1130
    owner = models.ForeignKey(AstakosUser)
1131
    email = models.EmailField()
1132

    
1133

    
1134
def _generate_invitation_code():
1135
    while True:
1136
        code = randint(1, 2L ** 63 - 1)
1137
        try:
1138
            Invitation.objects.get(code=code)
1139
            # An invitation with this code already exists, try again
1140
        except Invitation.DoesNotExist:
1141
            return code
1142

    
1143

    
1144
def get_latest_terms():
1145
    try:
1146
        term = ApprovalTerms.objects.order_by('-id')[0]
1147
        return term
1148
    except IndexError:
1149
        pass
1150
    return None
1151

    
1152

    
1153
class PendingThirdPartyUser(models.Model):
1154
    """
1155
    Model for registring successful third party user authentications
1156
    """
1157
    third_party_identifier = models.CharField(
1158
        _('Third-party identifier'), max_length=255, null=True, blank=True)
1159
    provider = models.CharField(_('Provider'), max_length=255, blank=True)
1160
    email = models.EmailField(_('e-mail address'), blank=True, null=True)
1161
    first_name = models.CharField(_('first name'), max_length=30, blank=True,
1162
                                  null=True)
1163
    last_name = models.CharField(_('last name'), max_length=30, blank=True,
1164
                                 null=True)
1165
    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
1166
                                   null=True)
1167
    username = models.CharField(
1168
        _('username'), max_length=30, unique=True,
1169
        help_text=_("Required. 30 characters or fewer. "
1170
                    "Letters, numbers and @/./+/-/_ characters"))
1171
    token = models.CharField(_('Token'), max_length=255, null=True, blank=True)
1172
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
1173
    info = models.TextField(default="", null=True, blank=True)
1174

    
1175
    class Meta:
1176
        unique_together = ("provider", "third_party_identifier")
1177

    
1178
    def get_user_instance(self):
1179
        """
1180
        Create a new AstakosUser instance based on details provided when user
1181
        initially signed up.
1182
        """
1183
        d = copy.copy(self.__dict__)
1184
        d.pop('_state', None)
1185
        d.pop('id', None)
1186
        d.pop('token', None)
1187
        d.pop('created', None)
1188
        d.pop('info', None)
1189
        d.pop('affiliation', None)
1190
        d.pop('provider', None)
1191
        d.pop('third_party_identifier', None)
1192
        user = AstakosUser(**d)
1193

    
1194
        return user
1195

    
1196
    @property
1197
    def realname(self):
1198
        return '%s %s' % (self.first_name, self.last_name)
1199

    
1200
    @realname.setter
1201
    def realname(self, value):
1202
        parts = value.split(' ')
1203
        if len(parts) == 2:
1204
            self.first_name = parts[0]
1205
            self.last_name = parts[1]
1206
        else:
1207
            self.last_name = parts[0]
1208

    
1209
    def save(self, *args, **kwargs):
1210
        if not self.id:
1211
            # set username
1212
            while not self.username:
1213
                username = uuid.uuid4().hex[:30]
1214
                try:
1215
                    AstakosUser.objects.get(username=username)
1216
                except AstakosUser.DoesNotExist:
1217
                    self.username = username
1218
        super(PendingThirdPartyUser, self).save(*args, **kwargs)
1219

    
1220
    def generate_token(self):
1221
        self.password = self.third_party_identifier
1222
        self.last_login = datetime.now()
1223
        self.token = default_token_generator.make_token(self)
1224

    
1225
    def existing_user(self):
1226
        return AstakosUser.objects.filter(
1227
            auth_providers__module=self.provider,
1228
            auth_providers__identifier=self.third_party_identifier)
1229

    
1230
    def get_provider(self, user):
1231
        params = {
1232
            'info_data': self.info,
1233
            'affiliation': self.affiliation
1234
        }
1235
        return auth.get_provider(self.provider, user,
1236
                                 self.third_party_identifier, **params)
1237

    
1238

    
1239
class SessionCatalog(models.Model):
1240
    session_key = models.CharField(_('session key'), max_length=40)
1241
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1242

    
1243

    
1244
class UserSetting(models.Model):
1245
    user = models.ForeignKey(AstakosUser)
1246
    setting = models.CharField(max_length=255)
1247
    value = models.IntegerField()
1248

    
1249
    class Meta:
1250
        unique_together = ("user", "setting")
1251

    
1252

    
1253
### PROJECTS ###
1254
################
1255

    
1256
class Chain(models.Model):
1257
    chain = models.AutoField(primary_key=True)
1258

    
1259
    def __str__(self):
1260
        return "%s" % (self.chain,)
1261

    
1262

    
1263
def new_chain():
1264
    c = Chain.objects.create()
1265
    return c
1266

    
1267

    
1268
class ProjectApplicationManager(models.Manager):
1269

    
1270
    def pending_per_project(self, projects):
1271
        apps = self.filter(state=self.model.PENDING,
1272
                           chain__in=projects).order_by('chain', '-id')
1273
        checked_chain = None
1274
        projs = {}
1275
        for app in apps:
1276
            chain = app.chain_id
1277
            if chain != checked_chain:
1278
                checked_chain = chain
1279
                projs[chain] = app
1280
        return projs
1281

    
1282

    
1283
class ProjectApplication(models.Model):
1284
    applicant = models.ForeignKey(
1285
        AstakosUser,
1286
        related_name='projects_applied',
1287
        db_index=True)
1288

    
1289
    PENDING = 0
1290
    APPROVED = 1
1291
    REPLACED = 2
1292
    DENIED = 3
1293
    DISMISSED = 4
1294
    CANCELLED = 5
1295

    
1296
    state = models.IntegerField(default=PENDING,
1297
                                db_index=True)
1298
    owner = models.ForeignKey(
1299
        AstakosUser,
1300
        related_name='projects_owned',
1301
        db_index=True)
1302
    chain = models.ForeignKey('Project',
1303
                              related_name='chained_apps',
1304
                              db_column='chain')
1305
    name = models.CharField(max_length=80)
1306
    homepage = models.URLField(max_length=255, null=True,
1307
                               verify_exists=False)
1308
    description = models.TextField(null=True, blank=True)
1309
    start_date = models.DateTimeField(null=True, blank=True)
1310
    end_date = models.DateTimeField()
1311
    member_join_policy = models.IntegerField()
1312
    member_leave_policy = models.IntegerField()
1313
    limit_on_members_number = models.PositiveIntegerField(null=True)
1314
    resource_grants = models.ManyToManyField(
1315
        Resource,
1316
        null=True,
1317
        blank=True,
1318
        through='ProjectResourceGrant')
1319
    comments = models.TextField(null=True, blank=True)
1320
    issue_date = models.DateTimeField(auto_now_add=True)
1321
    response_date = models.DateTimeField(null=True, blank=True)
1322
    response = models.TextField(null=True, blank=True)
1323
    response_actor = models.ForeignKey(AstakosUser, null=True,
1324
                                       related_name='responded_apps')
1325
    waive_date = models.DateTimeField(null=True, blank=True)
1326
    waive_reason = models.TextField(null=True, blank=True)
1327
    waive_actor = models.ForeignKey(AstakosUser, null=True,
1328
                                    related_name='waived_apps')
1329

    
1330
    objects = ProjectApplicationManager()
1331

    
1332
    # Compiled queries
1333
    Q_PENDING = Q(state=PENDING)
1334
    Q_APPROVED = Q(state=APPROVED)
1335
    Q_DENIED = Q(state=DENIED)
1336

    
1337
    class Meta:
1338
        unique_together = ("chain", "id")
1339

    
1340
    def __unicode__(self):
1341
        return "%s applied by %s" % (self.name, self.applicant)
1342

    
1343
    # TODO: Move to a more suitable place
1344
    APPLICATION_STATE_DISPLAY = {
1345
        PENDING:   _('Pending review'),
1346
        APPROVED:  _('Approved'),
1347
        REPLACED:  _('Replaced'),
1348
        DENIED:    _('Denied'),
1349
        DISMISSED: _('Dismissed'),
1350
        CANCELLED: _('Cancelled')
1351
    }
1352

    
1353
    @property
1354
    def log_display(self):
1355
        return "application %s (%s) for project %s" % (
1356
            self.id, self.name, self.chain)
1357

    
1358
    def state_display(self):
1359
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1360

    
1361
    @property
1362
    def grants(self):
1363
        return self.projectresourcegrant_set.values('member_capacity',
1364
                                                    'resource__name')
1365

    
1366
    @property
1367
    def resource_policies(self):
1368
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1369

    
1370
    def is_modification(self):
1371
        # if self.state != self.PENDING:
1372
        #     return False
1373
        parents = self.chained_applications().filter(id__lt=self.id)
1374
        parents = parents.filter(state__in=[self.APPROVED])
1375
        return parents.count() > 0
1376

    
1377
    def chained_applications(self):
1378
        return ProjectApplication.objects.filter(chain=self.chain)
1379

    
1380
    def denied_modifications(self):
1381
        q = self.chained_applications()
1382
        q = q.filter(Q(state=self.DENIED))
1383
        q = q.filter(~Q(id=self.id))
1384
        return q
1385

    
1386
    def last_denied(self):
1387
        try:
1388
            return self.denied_modifications().order_by('-id')[0]
1389
        except IndexError:
1390
            return None
1391

    
1392
    def has_denied_modifications(self):
1393
        return bool(self.last_denied())
1394

    
1395
    def can_cancel(self):
1396
        return self.state == self.PENDING
1397

    
1398
    def cancel(self, actor=None, reason=None):
1399
        if not self.can_cancel():
1400
            m = _("cannot cancel: application '%s' in state '%s'") % (
1401
                self.id, self.state)
1402
            raise AssertionError(m)
1403

    
1404
        self.state = self.CANCELLED
1405
        self.waive_date = datetime.now()
1406
        self.waive_reason = reason
1407
        self.waive_actor = actor
1408
        self.save()
1409

    
1410
    def can_dismiss(self):
1411
        return self.state == self.DENIED
1412

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

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

    
1425
    def can_deny(self):
1426
        return self.state == self.PENDING
1427

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

    
1434
        self.state = self.DENIED
1435
        self.response_date = datetime.now()
1436
        self.response = reason
1437
        self.response_actor = actor
1438
        self.save()
1439

    
1440
    def can_approve(self):
1441
        return self.state == self.PENDING
1442

    
1443
    def approve(self, actor=None, reason=None):
1444
        if not self.can_approve():
1445
            m = _("cannot approve: project '%s' in state '%s'") % (
1446
                self.name, self.state)
1447
            raise AssertionError(m)  # invalid argument
1448

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

    
1456
    @property
1457
    def member_join_policy_display(self):
1458
        policy = self.member_join_policy
1459
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1460

    
1461
    @property
1462
    def member_leave_policy_display(self):
1463
        policy = self.member_leave_policy
1464
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1465

    
1466

    
1467
class ProjectResourceGrantManager(models.Manager):
1468
    def grants_per_app(self, applications):
1469
        app_ids = [app.id for app in applications]
1470
        grants = self.filter(
1471
            project_application__in=app_ids).select_related("resource")
1472
        return _partition_by(lambda g: g.project_application_id, grants)
1473

    
1474

    
1475
class ProjectResourceGrant(models.Model):
1476

    
1477
    resource = models.ForeignKey(Resource)
1478
    project_application = models.ForeignKey(ProjectApplication,
1479
                                            null=True)
1480
    project_capacity = models.BigIntegerField(null=True)
1481
    member_capacity = models.BigIntegerField(default=0)
1482

    
1483
    objects = ProjectResourceGrantManager()
1484

    
1485
    class Meta:
1486
        unique_together = ("resource", "project_application")
1487

    
1488
    def display_member_capacity(self):
1489
        return units.show(self.member_capacity, self.resource.unit)
1490

    
1491
    def __str__(self):
1492
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1493
                                        self.display_member_capacity())
1494

    
1495

    
1496
def _distinct(f, l):
1497
    d = {}
1498
    last = None
1499
    for x in l:
1500
        group = f(x)
1501
        if group == last:
1502
            continue
1503
        last = group
1504
        d[group] = x
1505
    return d
1506

    
1507

    
1508
def invert_dict(d):
1509
    return dict((v, k) for k, v in d.iteritems())
1510

    
1511

    
1512
class ProjectManager(models.Manager):
1513

    
1514
    def all_with_pending(self, flt=None):
1515
        flt = Q() if flt is None else flt
1516
        projects = list(self.select_related(
1517
            'application', 'application__owner').filter(flt))
1518

    
1519
        objs = ProjectApplication.objects.select_related('owner')
1520
        apps = objs.filter(state=ProjectApplication.PENDING,
1521
                           chain__in=projects).order_by('chain', '-id')
1522
        app_d = _distinct(lambda app: app.chain_id, apps)
1523
        return [(project, app_d.get(project.pk)) for project in projects]
1524

    
1525
    def expired_projects(self):
1526
        model = self.model
1527
        q = ((model.o_state_q(model.O_ACTIVE) |
1528
              model.o_state_q(model.O_SUSPENDED)) &
1529
             Q(application__end_date__lt=datetime.now()))
1530
        return self.filter(q)
1531

    
1532
    def user_accessible_projects(self, user):
1533
        """
1534
        Return projects accessible by specified user.
1535
        """
1536
        model = self.model
1537
        if user.is_project_admin():
1538
            flt = Q()
1539
        else:
1540
            membs = user.projectmembership_set.associated()
1541
            memb_projects = membs.values_list("project", flat=True)
1542
            flt = (Q(application__owner=user) |
1543
                   Q(application__applicant=user) |
1544
                   Q(id__in=memb_projects))
1545

    
1546
        relevant = model.o_states_q(model.RELEVANT_STATES)
1547
        return self.filter(flt, relevant).order_by(
1548
            'application__issue_date').select_related(
1549
            'application', 'application__owner', 'application__applicant')
1550

    
1551
    def search_by_name(self, *search_strings):
1552
        q = Q()
1553
        for s in search_strings:
1554
            q = q | Q(name__icontains=s)
1555
        return self.filter(q)
1556

    
1557

    
1558
class Project(models.Model):
1559

    
1560
    id = models.BigIntegerField(db_column='id', primary_key=True)
1561

    
1562
    application = models.OneToOneField(
1563
        ProjectApplication,
1564
        related_name='project')
1565

    
1566
    members = models.ManyToManyField(
1567
        AstakosUser,
1568
        through='ProjectMembership')
1569

    
1570
    creation_date = models.DateTimeField(auto_now_add=True)
1571
    name = models.CharField(
1572
        max_length=80,
1573
        null=True,
1574
        db_index=True,
1575
        unique=True)
1576

    
1577
    NORMAL = 1
1578
    SUSPENDED = 10
1579
    TERMINATED = 100
1580

    
1581
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1582

    
1583
    state = models.IntegerField(default=NORMAL,
1584
                                db_index=True)
1585

    
1586
    objects = ProjectManager()
1587

    
1588
    def __str__(self):
1589
        return uenc(_("<project %s '%s'>") %
1590
                    (self.id, udec(self.application.name)))
1591

    
1592
    __repr__ = __str__
1593

    
1594
    def __unicode__(self):
1595
        return _("<project %s '%s'>") % (self.id, self.application.name)
1596

    
1597
    O_PENDING = 0
1598
    O_ACTIVE = 1
1599
    O_DENIED = 3
1600
    O_DISMISSED = 4
1601
    O_CANCELLED = 5
1602
    O_SUSPENDED = 10
1603
    O_TERMINATED = 100
1604

    
1605
    O_STATE_DISPLAY = {
1606
        O_PENDING:    _("Pending"),
1607
        O_ACTIVE:     _("Active"),
1608
        O_DENIED:     _("Denied"),
1609
        O_DISMISSED:  _("Dismissed"),
1610
        O_CANCELLED:  _("Cancelled"),
1611
        O_SUSPENDED:  _("Suspended"),
1612
        O_TERMINATED: _("Terminated"),
1613
    }
1614

    
1615
    OVERALL_STATE = {
1616
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1617
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1618
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1619
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1620
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1621
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1622
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1623
    }
1624

    
1625
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1626

    
1627
    @classmethod
1628
    def o_state_q(cls, o_state):
1629
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1630
        return Q(state=p_state, application__state=a_state)
1631

    
1632
    @classmethod
1633
    def o_states_q(cls, o_states):
1634
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1635

    
1636
    INITIALIZED_STATES = [O_ACTIVE,
1637
                          O_SUSPENDED,
1638
                          O_TERMINATED,
1639
                          ]
1640

    
1641
    RELEVANT_STATES = [O_PENDING,
1642
                       O_DENIED,
1643
                       O_ACTIVE,
1644
                       O_SUSPENDED,
1645
                       O_TERMINATED,
1646
                       ]
1647

    
1648
    SKIP_STATES = [O_DISMISSED,
1649
                   O_CANCELLED,
1650
                   O_TERMINATED,
1651
                   ]
1652

    
1653
    @classmethod
1654
    def _overall_state(cls, project_state, app_state):
1655
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1656

    
1657
    def overall_state(self):
1658
        return self._overall_state(self.state, self.application.state)
1659

    
1660
    def last_pending_application(self):
1661
        apps = self.chained_apps.filter(
1662
            state=ProjectApplication.PENDING).order_by('-id')
1663
        if apps:
1664
            return apps[0]
1665
        return None
1666

    
1667
    def last_pending_modification(self):
1668
        last_pending = self.last_pending_application()
1669
        if last_pending == self.application:
1670
            return None
1671
        return last_pending
1672

    
1673
    def state_display(self):
1674
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1675

    
1676
    def expiration_info(self):
1677
        return (str(self.id), self.name, self.state_display(),
1678
                str(self.application.end_date))
1679

    
1680
    def last_deactivation(self):
1681
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1682
        ls = objs.order_by("-date")
1683
        if not ls:
1684
            return None
1685
        return ls[0]
1686

    
1687
    def is_deactivated(self, reason=None):
1688
        if reason is not None:
1689
            return self.state == reason
1690

    
1691
        return self.state != self.NORMAL
1692

    
1693
    def is_active(self):
1694
        return self.overall_state() == self.O_ACTIVE
1695

    
1696
    def is_initialized(self):
1697
        return self.overall_state() in self.INITIALIZED_STATES
1698

    
1699
    ### Deactivation calls
1700

    
1701
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1702
                    comments=None):
1703
        now = datetime.now()
1704
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1705
                        actor=actor, reason=reason, comments=comments)
1706

    
1707
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1708
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1709
                         comments=comments)
1710
        self.state = to_state
1711
        self.save()
1712

    
1713
    def terminate(self, actor=None, reason=None):
1714
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1715
        self.name = None
1716
        self.save()
1717

    
1718
    def suspend(self, actor=None, reason=None):
1719
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1720

    
1721
    def resume(self, actor=None, reason=None):
1722
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1723
        if self.name is None:
1724
            self.name = self.application.name
1725
            self.save()
1726

    
1727
    ### Logical checks
1728

    
1729
    @property
1730
    def is_alive(self):
1731
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1732

    
1733
    @property
1734
    def is_terminated(self):
1735
        return self.is_deactivated(self.TERMINATED)
1736

    
1737
    @property
1738
    def is_suspended(self):
1739
        return self.is_deactivated(self.SUSPENDED)
1740

    
1741
    def violates_members_limit(self, adding=0):
1742
        application = self.application
1743
        limit = application.limit_on_members_number
1744
        if limit is None:
1745
            return False
1746
        return (len(self.approved_members) + adding > limit)
1747

    
1748
    ### Other
1749

    
1750
    def count_pending_memberships(self):
1751
        return self.projectmembership_set.requested().count()
1752

    
1753
    def members_count(self):
1754
        return self.approved_memberships.count()
1755

    
1756
    @property
1757
    def approved_memberships(self):
1758
        query = ProjectMembership.Q_ACCEPTED_STATES
1759
        return self.projectmembership_set.filter(query)
1760

    
1761
    @property
1762
    def approved_members(self):
1763
        return [m.person for m in self.approved_memberships]
1764

    
1765

    
1766
class ProjectLogManager(models.Manager):
1767
    def last_deactivations(self, projects):
1768
        logs = self.filter(
1769
            project__in=projects,
1770
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1771
        return first_of_group(lambda l: l.project_id, logs)
1772

    
1773

    
1774
class ProjectLog(models.Model):
1775
    project = models.ForeignKey(Project, related_name="log")
1776
    from_state = models.IntegerField(null=True)
1777
    to_state = models.IntegerField()
1778
    date = models.DateTimeField()
1779
    actor = models.ForeignKey(AstakosUser, null=True)
1780
    reason = models.TextField(null=True)
1781
    comments = models.TextField(null=True)
1782

    
1783
    objects = ProjectLogManager()
1784

    
1785

    
1786
class ProjectLock(models.Model):
1787
    pass
1788

    
1789

    
1790
class ProjectMembershipManager(models.Manager):
1791

    
1792
    def any_accepted(self):
1793
        q = self.model.Q_ACCEPTED_STATES
1794
        return self.filter(q)
1795

    
1796
    def actually_accepted(self):
1797
        q = self.model.Q_ACTUALLY_ACCEPTED
1798
        return self.filter(q)
1799

    
1800
    def requested(self):
1801
        return self.filter(state=ProjectMembership.REQUESTED)
1802

    
1803
    def suspended(self):
1804
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1805

    
1806
    def associated(self):
1807
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1808

    
1809
    def any_accepted_per_project(self, projects):
1810
        ms = self.any_accepted().filter(project__in=projects)
1811
        return _partition_by(lambda m: m.project_id, ms)
1812

    
1813
    def requested_per_project(self, projects):
1814
        ms = self.requested().filter(project__in=projects)
1815
        return _partition_by(lambda m: m.project_id, ms)
1816

    
1817
    def one_per_project(self):
1818
        ms = self.all().select_related(
1819
            'project', 'project__application',
1820
            'project__application__owner', 'project_application__applicant',
1821
            'person')
1822
        m_per_p = {}
1823
        for m in ms:
1824
            m_per_p[m.project_id] = m
1825
        return m_per_p
1826

    
1827

    
1828
class ProjectMembership(models.Model):
1829

    
1830
    person = models.ForeignKey(AstakosUser)
1831
    project = models.ForeignKey(Project)
1832

    
1833
    REQUESTED = 0
1834
    ACCEPTED = 1
1835
    LEAVE_REQUESTED = 5
1836
    # User deactivation
1837
    USER_SUSPENDED = 10
1838
    REJECTED = 100
1839
    CANCELLED = 101
1840
    REMOVED = 200
1841

    
1842
    ASSOCIATED_STATES = set([REQUESTED,
1843
                             ACCEPTED,
1844
                             LEAVE_REQUESTED,
1845
                             USER_SUSPENDED,
1846
                             ])
1847

    
1848
    ACCEPTED_STATES = set([ACCEPTED,
1849
                           LEAVE_REQUESTED,
1850
                           USER_SUSPENDED,
1851
                           ])
1852

    
1853
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1854

    
1855
    state = models.IntegerField(default=REQUESTED,
1856
                                db_index=True)
1857

    
1858
    objects = ProjectMembershipManager()
1859

    
1860
    # Compiled queries
1861
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1862
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1863

    
1864
    MEMBERSHIP_STATE_DISPLAY = {
1865
        REQUESTED:       _('Requested'),
1866
        ACCEPTED:        _('Accepted'),
1867
        LEAVE_REQUESTED: _('Leave Requested'),
1868
        USER_SUSPENDED:  _('Suspended'),
1869
        REJECTED:        _('Rejected'),
1870
        CANCELLED:       _('Cancelled'),
1871
        REMOVED:         _('Removed'),
1872
    }
1873

    
1874
    USER_FRIENDLY_STATE_DISPLAY = {
1875
        REQUESTED:       _('Join requested'),
1876
        ACCEPTED:        _('Accepted member'),
1877
        LEAVE_REQUESTED: _('Requested to leave'),
1878
        USER_SUSPENDED:  _('Suspended member'),
1879
        REJECTED:        _('Request rejected'),
1880
        CANCELLED:       _('Request cancelled'),
1881
        REMOVED:         _('Removed member'),
1882
    }
1883

    
1884
    def state_display(self):
1885
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1886

    
1887
    def user_friendly_state_display(self):
1888
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1889

    
1890
    class Meta:
1891
        unique_together = ("person", "project")
1892
        #index_together = [["project", "state"]]
1893

    
1894
    def __str__(self):
1895
        return uenc(_("<'%s' membership in '%s'>") %
1896
                    (self.person.username, self.project))
1897

    
1898
    __repr__ = __str__
1899

    
1900
    def latest_log(self):
1901
        logs = self.log.all()
1902
        logs_d = _partition_by(lambda l: l.to_state, logs)
1903
        for s, s_logs in logs_d.iteritems():
1904
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1905
        return logs_d
1906

    
1907
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1908
                    comments=None):
1909
        now = datetime.now()
1910
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1911
                        actor=actor, reason=reason, comments=comments)
1912

    
1913
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1914
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1915
                         comments=comments)
1916
        self.state = to_state
1917
        self.save()
1918

    
1919
    ACTION_CHECKS = {
1920
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
1921
        "accept": lambda m: m.state == m.REQUESTED,
1922
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
1923
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
1924
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
1925
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1926
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1927
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
1928
        "reject": lambda m: m.state == m.REQUESTED,
1929
        "cancel": lambda m: m.state == m.REQUESTED,
1930
    }
1931

    
1932
    ACTION_STATES = {
1933
        "join":          REQUESTED,
1934
        "accept":        ACCEPTED,
1935
        "enroll":        ACCEPTED,
1936
        "leave_request": LEAVE_REQUESTED,
1937
        "deny_leave":    ACCEPTED,
1938
        "cancel_leave":  ACCEPTED,
1939
        "remove":        REMOVED,
1940
        "reject":        REJECTED,
1941
        "cancel":        CANCELLED,
1942
    }
1943

    
1944
    def check_action(self, action):
1945
        try:
1946
            check = self.ACTION_CHECKS[action]
1947
        except KeyError:
1948
            raise ValueError("No check found for action '%s'" % action)
1949
        return check(self)
1950

    
1951
    def perform_action(self, action, actor=None, reason=None):
1952
        if not self.check_action(action):
1953
            m = _("%s: attempted action '%s' in state '%s'") % (
1954
                self, action, self.state)
1955
            raise AssertionError(m)
1956
        try:
1957
            s = self.ACTION_STATES[action]
1958
        except KeyError:
1959
            raise ValueError("No such action '%s'" % action)
1960
        return self.set_state(s, actor=actor, reason=reason)
1961

    
1962

    
1963
class ProjectMembershipLogManager(models.Manager):
1964
    def last_logs(self, memberships):
1965
        logs = self.filter(membership__in=memberships).order_by("-date")
1966
        logs = _partition_by(lambda l: l.membership_id, logs)
1967

    
1968
        for memb_id, m_logs in logs.iteritems():
1969
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
1970
        return logs
1971

    
1972

    
1973
class ProjectMembershipLog(models.Model):
1974
    membership = models.ForeignKey(ProjectMembership, related_name="log")
1975
    from_state = models.IntegerField(null=True)
1976
    to_state = models.IntegerField()
1977
    date = models.DateTimeField()
1978
    actor = models.ForeignKey(AstakosUser, null=True)
1979
    reason = models.TextField(null=True)
1980
    comments = models.TextField(null=True)
1981

    
1982
    objects = ProjectMembershipLogManager()
1983

    
1984

    
1985
### SIGNALS ###
1986
################
1987

    
1988
def create_astakos_user(u):
1989
    try:
1990
        AstakosUser.objects.get(user_ptr=u.pk)
1991
    except AstakosUser.DoesNotExist:
1992
        extended_user = AstakosUser(user_ptr_id=u.pk)
1993
        extended_user.__dict__.update(u.__dict__)
1994
        extended_user.save()
1995
        if not extended_user.has_auth_provider('local'):
1996
            extended_user.add_auth_provider('local')
1997
    except BaseException, e:
1998
        logger.exception(e)
1999

    
2000

    
2001
def fix_superusers():
2002
    # Associate superusers with AstakosUser
2003
    admins = User.objects.filter(is_superuser=True)
2004
    for u in admins:
2005
        create_astakos_user(u)
2006

    
2007

    
2008
def user_post_save(sender, instance, created, **kwargs):
2009
    if not created:
2010
        return
2011
    create_astakos_user(instance)
2012
post_save.connect(user_post_save, sender=User)
2013

    
2014

    
2015
def astakosuser_post_save(sender, instance, created, **kwargs):
2016
    pass
2017

    
2018
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2019

    
2020

    
2021
def resource_post_save(sender, instance, created, **kwargs):
2022
    pass
2023

    
2024
post_save.connect(resource_post_save, sender=Resource)
2025

    
2026

    
2027
def renew_token(sender, instance, **kwargs):
2028
    if not instance.auth_token:
2029
        instance.renew_token()
2030
pre_save.connect(renew_token, sender=AstakosUser)
2031
pre_save.connect(renew_token, sender=Component)