Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (65.4 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
def split_realname(value):
307
    parts = value.split(' ')
308
    if len(parts) == 2:
309
        return parts
310
    else:
311
        return ('', value)
312

    
313

    
314
class AstakosUserManager(UserManager):
315

    
316
    def get_auth_provider_user(self, provider, **kwargs):
317
        """
318
        Retrieve AstakosUser instance associated with the specified third party
319
        id.
320
        """
321
        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
322
                          kwargs.iteritems()))
323
        return self.get(auth_providers__module=provider, **kwargs)
324

    
325
    def get_by_email(self, email):
326
        return self.get(email=email)
327

    
328
    def get_by_identifier(self, email_or_username, **kwargs):
329
        try:
330
            return self.get(email__iexact=email_or_username, **kwargs)
331
        except AstakosUser.DoesNotExist:
332
            return self.get(username__iexact=email_or_username, **kwargs)
333

    
334
    def user_exists(self, email_or_username, **kwargs):
335
        qemail = Q(email__iexact=email_or_username)
336
        qusername = Q(username__iexact=email_or_username)
337
        qextra = Q(**kwargs)
338
        return self.filter((qemail | qusername) & qextra).exists()
339

    
340
    def unverified_namesakes(self, email_or_username):
341
        q = Q(email__iexact=email_or_username)
342
        q |= Q(username__iexact=email_or_username)
343
        return self.filter(q & Q(email_verified=False))
344

    
345
    def verified_user_exists(self, email_or_username):
346
        return self.user_exists(email_or_username, email_verified=True)
347

    
348
    def verified(self):
349
        return self.filter(email_verified=True)
350

    
351
    def accepted(self):
352
        return self.filter(moderated=True, is_rejected=False)
353

    
354
    def uuid_catalog(self, l=None):
355
        """
356
        Returns a uuid to username mapping for the uuids appearing in l.
357
        If l is None returns the mapping for all existing users.
358
        """
359
        q = self.filter(uuid__in=l) if l is not None else self
360
        return dict(q.values_list('uuid', 'username'))
361

    
362
    def displayname_catalog(self, l=None):
363
        """
364
        Returns a username to uuid mapping for the usernames appearing in l.
365
        If l is None returns the mapping for all existing users.
366
        """
367
        if l is not None:
368
            lmap = dict((x.lower(), x) for x in l)
369
            q = self.filter(username__in=lmap.keys())
370
            values = ((lmap[n], u)
371
                      for n, u in q.values_list('username', 'uuid'))
372
        else:
373
            q = self
374
            values = self.values_list('username', 'uuid')
375
        return dict(values)
376

    
377

    
378
class AstakosUser(User):
379
    """
380
    Extends ``django.contrib.auth.models.User`` by defining additional fields.
381
    """
382
    affiliation = models.CharField(_('Affiliation'), max_length=255,
383
                                   blank=True, null=True)
384

    
385
    #for invitations
386
    user_level = astakos_settings.DEFAULT_USER_LEVEL
387
    level = models.IntegerField(_('Inviter level'), default=user_level)
388
    invitations = models.IntegerField(
389
        _('Invitations left'),
390
        default=astakos_settings.INVITATIONS_PER_LEVEL.get(user_level, 0))
391

    
392
    auth_token = models.CharField(
393
        _('Authentication Token'),
394
        max_length=64,
395
        unique=True,
396
        null=True,
397
        blank=True,
398
        help_text=_('Renew your authentication '
399
                    'token. Make sure to set the new '
400
                    'token in any client you may be '
401
                    'using, to preserve its '
402
                    'functionality.'))
403
    auth_token_created = models.DateTimeField(_('Token creation date'),
404
                                              null=True)
405
    auth_token_expires = models.DateTimeField(
406
        _('Token expiration date'), null=True)
407

    
408
    updated = models.DateTimeField(_('Update date'))
409

    
410
    # Arbitrary text to identify the reason user got deactivated.
411
    # To be used as a reference from administrators.
412
    deactivated_reason = models.TextField(
413
        _('Reason the user was disabled for'),
414
        default=None, null=True)
415
    deactivated_at = models.DateTimeField(_('User deactivated at'), null=True,
416
                                          blank=True)
417

    
418
    has_credits = models.BooleanField(_('Has credits?'), default=False)
419

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

    
423
    # user email is verified
424
    email_verified = models.BooleanField(_('Email verified?'), default=False)
425

    
426
    # unique string used in user email verification url
427
    verification_code = models.CharField(max_length=255, null=True,
428
                                         blank=False, unique=True)
429

    
430
    # date user email verified
431
    verified_at = models.DateTimeField(_('User verified email at'), null=True,
432
                                       blank=True)
433

    
434
    # email verification notice was sent to the user at this time
435
    activation_sent = models.DateTimeField(_('Activation sent date'),
436
                                           null=True, blank=True)
437

    
438
    # user got rejected during moderation process
439
    is_rejected = models.BooleanField(_('Account rejected'),
440
                                      default=False)
441
    # reason user got rejected
442
    rejected_reason = models.TextField(_('User rejected reason'), null=True,
443
                                       blank=True)
444
    # moderation status
445
    moderated = models.BooleanField(_('User moderated'), default=False)
446
    # date user moderated (either accepted or rejected)
447
    moderated_at = models.DateTimeField(_('Date moderated'), default=None,
448
                                        blank=True, null=True)
449
    # a snapshot of user instance the time got moderated
450
    moderated_data = models.TextField(null=True, default=None, blank=True)
451
    # a string which identifies how the user got moderated
452
    accepted_policy = models.CharField(_('Accepted policy'), max_length=255,
453
                                       default=None, null=True, blank=True)
454
    # the email used to accept the user
455
    accepted_email = models.EmailField(null=True, default=None, blank=True)
456

    
457
    has_signed_terms = models.BooleanField(_('I agree with the terms'),
458
                                           default=False)
459
    date_signed_terms = models.DateTimeField(_('Signed terms date'),
460
                                             null=True, blank=True)
461
    # permanent unique user identifier
462
    uuid = models.CharField(max_length=255, null=False, blank=False,
463
                            unique=True)
464

    
465
    policy = models.ManyToManyField(
466
        Resource, null=True, through='AstakosUserQuota')
467

    
468
    disturbed_quota = models.BooleanField(_('Needs quotaholder syncing'),
469
                                          default=False, db_index=True)
470

    
471
    objects = AstakosUserManager()
472

    
473
    @property
474
    def realname(self):
475
        return '%s %s' % (self.first_name, self.last_name)
476

    
477
    @property
478
    def log_display(self):
479
        """
480
        Should be used in all logger.* calls that refer to a user so that
481
        user display is consistent across log entries.
482
        """
483
        return '%s::%s' % (self.uuid, self.email)
484

    
485
    @realname.setter
486
    def realname(self, value):
487
        first, last = split_realname(value)
488
        self.first_name = first
489
        self.last_name = last
490

    
491
    def add_permission(self, pname):
492
        if self.has_perm(pname):
493
            return
494
        p, created = Permission.objects.get_or_create(
495
            codename=pname,
496
            name=pname.capitalize(),
497
            content_type=get_content_type())
498
        self.user_permissions.add(p)
499

    
500
    def remove_permission(self, pname):
501
        if self.has_perm(pname):
502
            return
503
        p = Permission.objects.get(codename=pname,
504
                                   content_type=get_content_type())
505
        self.user_permissions.remove(p)
506

    
507
    def add_group(self, gname):
508
        group, _ = Group.objects.get_or_create(name=gname)
509
        self.groups.add(group)
510

    
511
    def is_accepted(self):
512
        return self.moderated and not self.is_rejected
513

    
514
    def is_project_admin(self, application_id=None):
515
        return self.uuid in astakos_settings.PROJECT_ADMINS
516

    
517
    @property
518
    def invitation(self):
519
        try:
520
            return Invitation.objects.get(username=self.email)
521
        except Invitation.DoesNotExist:
522
            return None
523

    
524
    @property
525
    def policies(self):
526
        return self.astakosuserquota_set.select_related().all()
527

    
528
    def get_resource_policy(self, resource):
529
        return AstakosUserQuota.objects.select_related("resource").\
530
            get(user=self, resource__name=resource)
531

    
532
    def fix_username(self):
533
        self.username = self.email.lower()
534

    
535
    def set_email(self, email):
536
        self.email = email
537
        self.fix_username()
538

    
539
    def save(self, update_timestamps=True, **kwargs):
540
        if update_timestamps:
541
            self.updated = datetime.now()
542

    
543
        super(AstakosUser, self).save(**kwargs)
544

    
545
    def renew_verification_code(self):
546
        self.verification_code = str(uuid.uuid4())
547
        logger.info("Verification code renewed for %s" % self.log_display)
548

    
549
    def renew_token(self, flush_sessions=False, current_key=None):
550
        for i in range(10):
551
            new_token = generate_token()
552
            count = AstakosUser.objects.filter(auth_token=new_token).count()
553
            if count == 0:
554
                break
555
            continue
556
        else:
557
            raise ValueError('Could not generate a token')
558

    
559
        self.auth_token = new_token
560
        self.auth_token_created = datetime.now()
561
        self.auth_token_expires = self.auth_token_created + \
562
            timedelta(hours=astakos_settings.AUTH_TOKEN_DURATION)
563
        if flush_sessions:
564
            self.flush_sessions(current_key)
565
        msg = 'Token renewed for %s'
566
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
567

    
568
    def token_expired(self):
569
        return self.auth_token_expires < datetime.now()
570

    
571
    def flush_sessions(self, current_key=None):
572
        q = self.sessions
573
        if current_key:
574
            q = q.exclude(session_key=current_key)
575

    
576
        keys = q.values_list('session_key', flat=True)
577
        if keys:
578
            msg = 'Flushing sessions: %s'
579
            logger.log(astakos_settings.LOGGING_LEVEL, msg, ','.join(keys))
580
        engine = import_module(settings.SESSION_ENGINE)
581
        for k in keys:
582
            s = engine.SessionStore(k)
583
            s.flush()
584

    
585
    def __unicode__(self):
586
        return '%s (%s)' % (self.realname, self.email)
587

    
588
    def conflicting_email(self):
589
        q = AstakosUser.objects.exclude(username=self.username)
590
        q = q.filter(email__iexact=self.email)
591
        if q.count() != 0:
592
            return True
593
        return False
594

    
595
    def email_change_is_pending(self):
596
        return self.emailchanges.count() > 0
597

    
598
    @property
599
    def status_display(self):
600
        msg = ""
601
        if self.is_active:
602
            msg = "Accepted/Active"
603
        if self.is_rejected:
604
            msg = "Rejected"
605
            if self.rejected_reason:
606
                msg += " (%s)" % self.rejected_reason
607
        if not self.email_verified:
608
            msg = "Pending email verification"
609
        if not self.moderated:
610
            msg = "Pending moderation"
611
        if not self.is_active and self.email_verified:
612
            msg = "Accepted/Inactive"
613
            if self.deactivated_reason:
614
                msg += " (%s)" % (self.deactivated_reason)
615

    
616
        if self.moderated and not self.is_rejected:
617
            if self.accepted_policy == 'manual':
618
                msg += " (manually accepted)"
619
            else:
620
                msg += " (accepted policy: %s)" % \
621
                    self.accepted_policy
622
        return msg
623

    
624
    @property
625
    def signed_terms(self):
626
        return self.has_signed_terms
627

    
628
    def set_invitations_level(self):
629
        """
630
        Update user invitation level
631
        """
632
        level = self.invitation.inviter.level + 1
633
        self.level = level
634
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
635

    
636
    def can_change_password(self):
637
        return self.has_auth_provider('local', auth_backend='astakos')
638

    
639
    def can_change_email(self):
640
        if not self.has_auth_provider('local'):
641
            return True
642

    
643
        local = self.get_auth_provider('local')._instance
644
        return local.auth_backend == 'astakos'
645

    
646
    # Auth providers related methods
647
    def get_auth_provider(self, module=None, identifier=None, **filters):
648
        if not module:
649
            return self.auth_providers.active()[0].settings
650

    
651
        params = {'module': module}
652
        if identifier:
653
            params['identifier'] = identifier
654
        params.update(filters)
655
        return self.auth_providers.active().get(**params).settings
656

    
657
    def has_auth_provider(self, provider, **kwargs):
658
        return bool(self.auth_providers.active().filter(module=provider,
659
                                                        **kwargs).count())
660

    
661
    def get_required_providers(self, **kwargs):
662
        return auth.REQUIRED_PROVIDERS.keys()
663

    
664
    def missing_required_providers(self):
665
        required = self.get_required_providers()
666
        missing = []
667
        for provider in required:
668
            if not self.has_auth_provider(provider):
669
                missing.append(auth.get_provider(provider, self))
670
        return missing
671

    
672
    def get_available_auth_providers(self, **filters):
673
        """
674
        Returns a list of providers available for add by the user.
675
        """
676
        modules = astakos_settings.IM_MODULES
677
        providers = []
678
        for p in modules:
679
            providers.append(auth.get_provider(p, self))
680
        available = []
681

    
682
        for p in providers:
683
            if p.get_add_policy:
684
                available.append(p)
685
        return available
686

    
687
    def get_disabled_auth_providers(self, **filters):
688
        providers = self.get_auth_providers(**filters)
689
        disabled = []
690
        for p in providers:
691
            if not p.get_login_policy:
692
                disabled.append(p)
693
        return disabled
694

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

    
703
    def get_auth_providers(self, **filters):
704
        providers = []
705
        for provider in self.auth_providers.active(**filters):
706
            if provider.settings.module_enabled:
707
                providers.append(provider.settings)
708

    
709
        modules = astakos_settings.IM_MODULES
710

    
711
        def key(p):
712
            if not p.module in modules:
713
                return 100
714
            return modules.index(p.module)
715

    
716
        providers = sorted(providers, key=key)
717
        return providers
718

    
719
    # URL methods
720
    @property
721
    def auth_providers_display(self):
722
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
723
                         self.get_enabled_auth_providers()])
724

    
725
    def add_auth_provider(self, module='local', identifier=None, **params):
726
        provider = auth.get_provider(module, self, identifier, **params)
727
        provider.add_to_user()
728

    
729
    def get_resend_activation_url(self):
730
        return reverse('send_activation', kwargs={'user_id': self.pk})
731

    
732
    def get_activation_url(self, nxt=False):
733
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
734
                              quote(self.verification_code))
735
        if nxt:
736
            url += "&next=%s" % quote(nxt)
737
        return url
738

    
739
    def get_password_reset_url(self, token_generator=default_token_generator):
740
        return reverse('astakos.im.views.target.local.password_reset_confirm',
741
                       kwargs={'uidb36': int_to_base36(self.id),
742
                               'token': token_generator.make_token(self)})
743

    
744
    def get_inactive_message(self, provider_module, identifier=None):
745
        provider = self.get_auth_provider(provider_module, identifier)
746

    
747
        msg_extra = ''
748
        message = ''
749

    
750
        msg_inactive = provider.get_account_inactive_msg
751
        msg_pending = provider.get_pending_activation_msg
752
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
753
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
754
        msg_pending_mod = provider.get_pending_moderation_msg
755
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
756
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
757

    
758
        if not self.email_verified:
759
            message = msg_pending
760
            url = self.get_resend_activation_url()
761
            msg_extra = msg_pending_help + \
762
                u' ' + \
763
                '<a href="%s">%s?</a>' % (url, msg_resend)
764
        else:
765
            if not self.moderated:
766
                message = msg_pending_mod
767
            else:
768
                if self.is_rejected:
769
                    message = msg_rejected
770
                else:
771
                    message = msg_inactive
772

    
773
        return mark_safe(message + u' ' + msg_extra)
774

    
775
    def owns_application(self, application):
776
        return application.owner == self
777

    
778
    def owns_project(self, project):
779
        return project.application.owner == self
780

    
781
    def is_associated(self, project):
782
        try:
783
            m = ProjectMembership.objects.get(person=self, project=project)
784
            return m.state in ProjectMembership.ASSOCIATED_STATES
785
        except ProjectMembership.DoesNotExist:
786
            return False
787

    
788
    def get_membership(self, project):
789
        try:
790
            return ProjectMembership.objects.get(
791
                project=project,
792
                person=self)
793
        except ProjectMembership.DoesNotExist:
794
            return None
795

    
796
    def membership_display(self, project):
797
        m = self.get_membership(project)
798
        if m is None:
799
            return _('Not a member')
800
        else:
801
            return m.user_friendly_state_display()
802

    
803
    def non_owner_can_view(self, maybe_project):
804
        if self.is_project_admin():
805
            return True
806
        if maybe_project is None:
807
            return False
808
        project = maybe_project
809
        if self.is_associated(project):
810
            return True
811
        if project.is_deactivated():
812
            return False
813
        return True
814

    
815

    
816
class AstakosUserAuthProviderManager(models.Manager):
817

    
818
    def active(self, **filters):
819
        return self.filter(active=True, **filters)
820

    
821
    def remove_unverified_providers(self, provider, **filters):
822
        try:
823
            existing = self.filter(module=provider, user__email_verified=False,
824
                                   **filters)
825
            for p in existing:
826
                p.user.delete()
827
        except:
828
            pass
829

    
830
    def unverified(self, provider, **filters):
831
        try:
832
            return self.get(module=provider, user__email_verified=False,
833
                            **filters).settings
834
        except AstakosUserAuthProvider.DoesNotExist:
835
            return None
836

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

    
844

    
845
class AuthProviderPolicyProfileManager(models.Manager):
846

    
847
    def active(self):
848
        return self.filter(active=True)
849

    
850
    def for_user(self, user, provider):
851
        policies = {}
852
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
853
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
854
        exclusive_q = exclusive_q1 | exclusive_q2
855

    
856
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
857
            policies.update(profile.policies)
858

    
859
        user_groups = user.groups.all().values('pk')
860
        for profile in self.active().filter(groups__in=user_groups).filter(
861
                exclusive_q):
862
            policies.update(profile.policies)
863
        return policies
864

    
865
    def add_policy(self, name, provider, group_or_user, exclusive=False,
866
                   **policies):
867
        is_group = isinstance(group_or_user, Group)
868
        profile, created = self.get_or_create(name=name, provider=provider,
869
                                              is_exclusive=exclusive)
870
        profile.is_exclusive = exclusive
871
        profile.save()
872
        if is_group:
873
            profile.groups.add(group_or_user)
874
        else:
875
            profile.users.add(group_or_user)
876
        profile.set_policies(policies)
877
        profile.save()
878
        return profile
879

    
880

    
881
class AuthProviderPolicyProfile(models.Model):
882
    name = models.CharField(_('Name'), max_length=255, blank=False,
883
                            null=False, db_index=True)
884
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
885
                                null=False)
886

    
887
    # apply policies to all providers excluding the one set in provider field
888
    is_exclusive = models.BooleanField(default=False)
889

    
890
    policy_add = models.NullBooleanField(null=True, default=None)
891
    policy_remove = models.NullBooleanField(null=True, default=None)
892
    policy_create = models.NullBooleanField(null=True, default=None)
893
    policy_login = models.NullBooleanField(null=True, default=None)
894
    policy_limit = models.IntegerField(null=True, default=None)
895
    policy_required = models.NullBooleanField(null=True, default=None)
896
    policy_automoderate = models.NullBooleanField(null=True, default=None)
897
    policy_switch = models.NullBooleanField(null=True, default=None)
898

    
899
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
900
                     'automoderate')
901

    
902
    priority = models.IntegerField(null=False, default=1)
903
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
904
    users = models.ManyToManyField(AstakosUser,
905
                                   related_name='authpolicy_profiles')
906
    active = models.BooleanField(default=True)
907

    
908
    objects = AuthProviderPolicyProfileManager()
909

    
910
    class Meta:
911
        ordering = ['priority']
912

    
913
    @property
914
    def policies(self):
915
        policies = {}
916
        for pkey in self.POLICY_FIELDS:
917
            value = getattr(self, 'policy_%s' % pkey, None)
918
            if value is None:
919
                continue
920
            policies[pkey] = value
921
        return policies
922

    
923
    def set_policies(self, policies_dict):
924
        for key, value in policies_dict.iteritems():
925
            if key in self.POLICY_FIELDS:
926
                setattr(self, 'policy_%s' % key, value)
927
        return self.policies
928

    
929

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

    
948
    objects = AstakosUserAuthProviderManager()
949

    
950
    class Meta:
951
        unique_together = (('identifier', 'module', 'user'), )
952
        ordering = ('module', 'created')
953

    
954
    def __init__(self, *args, **kwargs):
955
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
956
        try:
957
            self.info = json.loads(self.info_data)
958
            if not self.info:
959
                self.info = {}
960
        except Exception:
961
            self.info = {}
962

    
963
        for key, value in self.info.iteritems():
964
            setattr(self, 'info_%s' % key, value)
965

    
966
    @property
967
    def settings(self):
968
        extra_data = {}
969

    
970
        info_data = {}
971
        if self.info_data:
972
            info_data = json.loads(self.info_data)
973

    
974
        extra_data['info'] = info_data
975

    
976
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
977
            extra_data[key] = getattr(self, key)
978

    
979
        extra_data['instance'] = self
980
        return auth.get_provider(self.module, self.user,
981
                                 self.identifier, **extra_data)
982

    
983
    def __repr__(self):
984
        return '<AstakosUserAuthProvider %s:%s>' % (
985
            self.module, self.identifier)
986

    
987
    def __unicode__(self):
988
        if self.identifier:
989
            return "%s:%s" % (self.module, self.identifier)
990
        if self.auth_backend:
991
            return "%s:%s" % (self.module, self.auth_backend)
992
        return self.module
993

    
994
    def save(self, *args, **kwargs):
995
        self.info_data = json.dumps(self.info)
996
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
997

    
998

    
999
class AstakosUserQuota(models.Model):
1000
    capacity = models.BigIntegerField()
1001
    resource = models.ForeignKey(Resource)
1002
    user = models.ForeignKey(AstakosUser)
1003

    
1004
    class Meta:
1005
        unique_together = ("resource", "user")
1006

    
1007

    
1008
class ApprovalTerms(models.Model):
1009
    """
1010
    Model for approval terms
1011
    """
1012

    
1013
    date = models.DateTimeField(
1014
        _('Issue date'), db_index=True, auto_now_add=True)
1015
    location = models.CharField(_('Terms location'), max_length=255)
1016

    
1017

    
1018
class Invitation(models.Model):
1019
    """
1020
    Model for registring invitations
1021
    """
1022
    inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
1023
                                null=True)
1024
    realname = models.CharField(_('Real name'), max_length=255)
1025
    username = models.CharField(_('Unique ID'), max_length=255, unique=True)
1026
    code = models.BigIntegerField(_('Invitation code'), db_index=True)
1027
    is_consumed = models.BooleanField(_('Consumed?'), default=False)
1028
    created = models.DateTimeField(_('Creation date'), auto_now_add=True)
1029
    consumed = models.DateTimeField(_('Consumption date'),
1030
                                    null=True, blank=True)
1031

    
1032
    def __init__(self, *args, **kwargs):
1033
        super(Invitation, self).__init__(*args, **kwargs)
1034
        if not self.id:
1035
            self.code = _generate_invitation_code()
1036

    
1037
    def consume(self):
1038
        self.is_consumed = True
1039
        self.consumed = datetime.now()
1040
        self.save()
1041

    
1042
    def __unicode__(self):
1043
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1044

    
1045

    
1046
class EmailChangeManager(models.Manager):
1047

    
1048
    @transaction.commit_on_success
1049
    def change_email(self, activation_key):
1050
        """
1051
        Validate an activation key and change the corresponding
1052
        ``User`` if valid.
1053

1054
        If the key is valid and has not expired, return the ``User``
1055
        after activating.
1056

1057
        If the key is not valid or has expired, return ``None``.
1058

1059
        If the key is valid but the ``User`` is already active,
1060
        return ``None``.
1061

1062
        After successful email change the activation record is deleted.
1063

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

    
1093

    
1094
class EmailChange(models.Model):
1095
    new_email_address = models.EmailField(
1096
        _(u'new e-mail address'),
1097
        help_text=_('Provide a new email address. Until you verify the new '
1098
                    'address by following the activation link that will be '
1099
                    'sent to it, your old email address will remain active.'))
1100
    user = models.ForeignKey(
1101
        AstakosUser, unique=True, related_name='emailchanges')
1102
    requested_at = models.DateTimeField(auto_now_add=True)
1103
    activation_key = models.CharField(
1104
        max_length=40, unique=True, db_index=True)
1105

    
1106
    objects = EmailChangeManager()
1107

    
1108
    def get_url(self):
1109
        return reverse('email_change_confirm',
1110
                       kwargs={'activation_key': self.activation_key})
1111

    
1112
    def activation_key_expired(self):
1113
        expiration_date = timedelta(
1114
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1115
        return self.requested_at + expiration_date < datetime.now()
1116

    
1117

    
1118
class AdditionalMail(models.Model):
1119
    """
1120
    Model for registring invitations
1121
    """
1122
    owner = models.ForeignKey(AstakosUser)
1123
    email = models.EmailField()
1124

    
1125

    
1126
def _generate_invitation_code():
1127
    while True:
1128
        code = randint(1, 2L ** 63 - 1)
1129
        try:
1130
            Invitation.objects.get(code=code)
1131
            # An invitation with this code already exists, try again
1132
        except Invitation.DoesNotExist:
1133
            return code
1134

    
1135

    
1136
def get_latest_terms():
1137
    try:
1138
        term = ApprovalTerms.objects.order_by('-id')[0]
1139
        return term
1140
    except IndexError:
1141
        pass
1142
    return None
1143

    
1144

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

    
1167
    class Meta:
1168
        unique_together = ("provider", "third_party_identifier")
1169

    
1170
    def get_user_instance(self):
1171
        """
1172
        Create a new AstakosUser instance based on details provided when user
1173
        initially signed up.
1174
        """
1175
        d = copy.copy(self.__dict__)
1176
        d.pop('_state', None)
1177
        d.pop('id', None)
1178
        d.pop('token', None)
1179
        d.pop('created', None)
1180
        d.pop('info', None)
1181
        d.pop('affiliation', None)
1182
        d.pop('provider', None)
1183
        d.pop('third_party_identifier', None)
1184
        user = AstakosUser(**d)
1185

    
1186
        return user
1187

    
1188
    @property
1189
    def realname(self):
1190
        return '%s %s' % (self.first_name, self.last_name)
1191

    
1192
    @realname.setter
1193
    def realname(self, value):
1194
        first, last = split_realname(value)
1195
        self.first_name = first
1196
        self.last_name = last
1197

    
1198
    def save(self, *args, **kwargs):
1199
        if not self.id:
1200
            # set username
1201
            while not self.username:
1202
                username = uuid.uuid4().hex[:30]
1203
                try:
1204
                    AstakosUser.objects.get(username=username)
1205
                except AstakosUser.DoesNotExist:
1206
                    self.username = username
1207
        super(PendingThirdPartyUser, self).save(*args, **kwargs)
1208

    
1209
    def generate_token(self):
1210
        self.password = self.third_party_identifier
1211
        self.last_login = datetime.now()
1212
        self.token = default_token_generator.make_token(self)
1213

    
1214
    def existing_user(self):
1215
        return AstakosUser.objects.filter(
1216
            auth_providers__module=self.provider,
1217
            auth_providers__identifier=self.third_party_identifier)
1218

    
1219
    def get_provider(self, user):
1220
        params = {
1221
            'info_data': self.info,
1222
            'affiliation': self.affiliation
1223
        }
1224
        return auth.get_provider(self.provider, user,
1225
                                 self.third_party_identifier, **params)
1226

    
1227

    
1228
class SessionCatalog(models.Model):
1229
    session_key = models.CharField(_('session key'), max_length=40)
1230
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1231

    
1232

    
1233
class UserSetting(models.Model):
1234
    user = models.ForeignKey(AstakosUser)
1235
    setting = models.CharField(max_length=255)
1236
    value = models.IntegerField()
1237

    
1238
    class Meta:
1239
        unique_together = ("user", "setting")
1240

    
1241

    
1242
### PROJECTS ###
1243
################
1244

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

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

    
1251

    
1252
def new_chain():
1253
    c = Chain.objects.create()
1254
    return c
1255

    
1256

    
1257
class ProjectApplicationManager(models.Manager):
1258

    
1259
    def pending_per_project(self, projects):
1260
        apps = self.filter(state=self.model.PENDING,
1261
                           chain__in=projects).order_by('chain', '-id')
1262
        checked_chain = None
1263
        projs = {}
1264
        for app in apps:
1265
            chain = app.chain_id
1266
            if chain != checked_chain:
1267
                checked_chain = chain
1268
                projs[chain] = app
1269
        return projs
1270

    
1271

    
1272
class ProjectApplication(models.Model):
1273
    applicant = models.ForeignKey(
1274
        AstakosUser,
1275
        related_name='projects_applied',
1276
        db_index=True)
1277

    
1278
    PENDING = 0
1279
    APPROVED = 1
1280
    REPLACED = 2
1281
    DENIED = 3
1282
    DISMISSED = 4
1283
    CANCELLED = 5
1284

    
1285
    state = models.IntegerField(default=PENDING,
1286
                                db_index=True)
1287
    owner = models.ForeignKey(
1288
        AstakosUser,
1289
        related_name='projects_owned',
1290
        db_index=True)
1291
    chain = models.ForeignKey('Project',
1292
                              related_name='chained_apps',
1293
                              db_column='chain')
1294
    name = models.CharField(max_length=80)
1295
    homepage = models.URLField(max_length=255, null=True,
1296
                               verify_exists=False)
1297
    description = models.TextField(null=True, blank=True)
1298
    start_date = models.DateTimeField(null=True, blank=True)
1299
    end_date = models.DateTimeField()
1300
    member_join_policy = models.IntegerField()
1301
    member_leave_policy = models.IntegerField()
1302
    limit_on_members_number = models.PositiveIntegerField(null=True)
1303
    resource_grants = models.ManyToManyField(
1304
        Resource,
1305
        null=True,
1306
        blank=True,
1307
        through='ProjectResourceGrant')
1308
    comments = models.TextField(null=True, blank=True)
1309
    issue_date = models.DateTimeField(auto_now_add=True)
1310
    response_date = models.DateTimeField(null=True, blank=True)
1311
    response = models.TextField(null=True, blank=True)
1312
    response_actor = models.ForeignKey(AstakosUser, null=True,
1313
                                       related_name='responded_apps')
1314
    waive_date = models.DateTimeField(null=True, blank=True)
1315
    waive_reason = models.TextField(null=True, blank=True)
1316
    waive_actor = models.ForeignKey(AstakosUser, null=True,
1317
                                    related_name='waived_apps')
1318

    
1319
    objects = ProjectApplicationManager()
1320

    
1321
    # Compiled queries
1322
    Q_PENDING = Q(state=PENDING)
1323
    Q_APPROVED = Q(state=APPROVED)
1324
    Q_DENIED = Q(state=DENIED)
1325

    
1326
    class Meta:
1327
        unique_together = ("chain", "id")
1328

    
1329
    def __unicode__(self):
1330
        return "%s applied by %s" % (self.name, self.applicant)
1331

    
1332
    # TODO: Move to a more suitable place
1333
    APPLICATION_STATE_DISPLAY = {
1334
        PENDING:   _('Pending review'),
1335
        APPROVED:  _('Approved'),
1336
        REPLACED:  _('Replaced'),
1337
        DENIED:    _('Denied'),
1338
        DISMISSED: _('Dismissed'),
1339
        CANCELLED: _('Cancelled')
1340
    }
1341

    
1342
    @property
1343
    def log_display(self):
1344
        return "application %s (%s) for project %s" % (
1345
            self.id, self.name, self.chain)
1346

    
1347
    def state_display(self):
1348
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1349

    
1350
    @property
1351
    def grants(self):
1352
        return self.projectresourcegrant_set.values('member_capacity',
1353
                                                    'resource__name')
1354

    
1355
    @property
1356
    def resource_policies(self):
1357
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1358

    
1359
    def is_modification(self):
1360
        # if self.state != self.PENDING:
1361
        #     return False
1362
        parents = self.chained_applications().filter(id__lt=self.id)
1363
        parents = parents.filter(state__in=[self.APPROVED])
1364
        return parents.count() > 0
1365

    
1366
    def chained_applications(self):
1367
        return ProjectApplication.objects.filter(chain=self.chain)
1368

    
1369
    def denied_modifications(self):
1370
        q = self.chained_applications()
1371
        q = q.filter(Q(state=self.DENIED))
1372
        q = q.filter(~Q(id=self.id))
1373
        return q
1374

    
1375
    def last_denied(self):
1376
        try:
1377
            return self.denied_modifications().order_by('-id')[0]
1378
        except IndexError:
1379
            return None
1380

    
1381
    def has_denied_modifications(self):
1382
        return bool(self.last_denied())
1383

    
1384
    def can_cancel(self):
1385
        return self.state == self.PENDING
1386

    
1387
    def cancel(self, actor=None, reason=None):
1388
        if not self.can_cancel():
1389
            m = _("cannot cancel: application '%s' in state '%s'") % (
1390
                self.id, self.state)
1391
            raise AssertionError(m)
1392

    
1393
        self.state = self.CANCELLED
1394
        self.waive_date = datetime.now()
1395
        self.waive_reason = reason
1396
        self.waive_actor = actor
1397
        self.save()
1398

    
1399
    def can_dismiss(self):
1400
        return self.state == self.DENIED
1401

    
1402
    def dismiss(self, actor=None, reason=None):
1403
        if not self.can_dismiss():
1404
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1405
                self.id, self.state)
1406
            raise AssertionError(m)
1407

    
1408
        self.state = self.DISMISSED
1409
        self.waive_date = datetime.now()
1410
        self.waive_reason = reason
1411
        self.waive_actor = actor
1412
        self.save()
1413

    
1414
    def can_deny(self):
1415
        return self.state == self.PENDING
1416

    
1417
    def deny(self, actor=None, reason=None):
1418
        if not self.can_deny():
1419
            m = _("cannot deny: application '%s' in state '%s'") % (
1420
                self.id, self.state)
1421
            raise AssertionError(m)
1422

    
1423
        self.state = self.DENIED
1424
        self.response_date = datetime.now()
1425
        self.response = reason
1426
        self.response_actor = actor
1427
        self.save()
1428

    
1429
    def can_approve(self):
1430
        return self.state == self.PENDING
1431

    
1432
    def approve(self, actor=None, reason=None):
1433
        if not self.can_approve():
1434
            m = _("cannot approve: project '%s' in state '%s'") % (
1435
                self.name, self.state)
1436
            raise AssertionError(m)  # invalid argument
1437

    
1438
        now = datetime.now()
1439
        self.state = self.APPROVED
1440
        self.response_date = now
1441
        self.response = reason
1442
        self.response_actor = actor
1443
        self.save()
1444

    
1445
    @property
1446
    def member_join_policy_display(self):
1447
        policy = self.member_join_policy
1448
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1449

    
1450
    @property
1451
    def member_leave_policy_display(self):
1452
        policy = self.member_leave_policy
1453
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1454

    
1455

    
1456
class ProjectResourceGrantManager(models.Manager):
1457
    def grants_per_app(self, applications):
1458
        app_ids = [app.id for app in applications]
1459
        grants = self.filter(
1460
            project_application__in=app_ids).select_related("resource")
1461
        return _partition_by(lambda g: g.project_application_id, grants)
1462

    
1463

    
1464
class ProjectResourceGrant(models.Model):
1465

    
1466
    resource = models.ForeignKey(Resource)
1467
    project_application = models.ForeignKey(ProjectApplication,
1468
                                            null=True)
1469
    project_capacity = models.BigIntegerField(null=True)
1470
    member_capacity = models.BigIntegerField(default=0)
1471

    
1472
    objects = ProjectResourceGrantManager()
1473

    
1474
    class Meta:
1475
        unique_together = ("resource", "project_application")
1476

    
1477
    def display_member_capacity(self):
1478
        return units.show(self.member_capacity, self.resource.unit)
1479

    
1480
    def __str__(self):
1481
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1482
                                        self.display_member_capacity())
1483

    
1484

    
1485
def _distinct(f, l):
1486
    d = {}
1487
    last = None
1488
    for x in l:
1489
        group = f(x)
1490
        if group == last:
1491
            continue
1492
        last = group
1493
        d[group] = x
1494
    return d
1495

    
1496

    
1497
def invert_dict(d):
1498
    return dict((v, k) for k, v in d.iteritems())
1499

    
1500

    
1501
class ProjectManager(models.Manager):
1502

    
1503
    def all_with_pending(self, flt=None):
1504
        flt = Q() if flt is None else flt
1505
        projects = list(self.select_related(
1506
            'application', 'application__owner').filter(flt))
1507

    
1508
        objs = ProjectApplication.objects.select_related('owner')
1509
        apps = objs.filter(state=ProjectApplication.PENDING,
1510
                           chain__in=projects).order_by('chain', '-id')
1511
        app_d = _distinct(lambda app: app.chain_id, apps)
1512
        return [(project, app_d.get(project.pk)) for project in projects]
1513

    
1514
    def expired_projects(self):
1515
        model = self.model
1516
        q = ((model.o_state_q(model.O_ACTIVE) |
1517
              model.o_state_q(model.O_SUSPENDED)) &
1518
             Q(application__end_date__lt=datetime.now()))
1519
        return self.filter(q)
1520

    
1521
    def user_accessible_projects(self, user):
1522
        """
1523
        Return projects accessible by specified user.
1524
        """
1525
        model = self.model
1526
        if user.is_project_admin():
1527
            flt = Q()
1528
        else:
1529
            membs = user.projectmembership_set.associated()
1530
            memb_projects = membs.values_list("project", flat=True)
1531
            flt = (Q(application__owner=user) |
1532
                   Q(application__applicant=user) |
1533
                   Q(id__in=memb_projects))
1534

    
1535
        relevant = model.o_states_q(model.RELEVANT_STATES)
1536
        return self.filter(flt, relevant).order_by(
1537
            'application__issue_date').select_related(
1538
            'application', 'application__owner', 'application__applicant')
1539

    
1540
    def search_by_name(self, *search_strings):
1541
        q = Q()
1542
        for s in search_strings:
1543
            q = q | Q(name__icontains=s)
1544
        return self.filter(q)
1545

    
1546

    
1547
class Project(models.Model):
1548

    
1549
    id = models.BigIntegerField(db_column='id', primary_key=True)
1550

    
1551
    application = models.OneToOneField(
1552
        ProjectApplication,
1553
        related_name='project')
1554

    
1555
    members = models.ManyToManyField(
1556
        AstakosUser,
1557
        through='ProjectMembership')
1558

    
1559
    creation_date = models.DateTimeField(auto_now_add=True)
1560
    name = models.CharField(
1561
        max_length=80,
1562
        null=True,
1563
        db_index=True,
1564
        unique=True)
1565

    
1566
    NORMAL = 1
1567
    SUSPENDED = 10
1568
    TERMINATED = 100
1569

    
1570
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1571

    
1572
    state = models.IntegerField(default=NORMAL,
1573
                                db_index=True)
1574

    
1575
    objects = ProjectManager()
1576

    
1577
    def __str__(self):
1578
        return uenc(_("<project %s '%s'>") %
1579
                    (self.id, udec(self.application.name)))
1580

    
1581
    __repr__ = __str__
1582

    
1583
    def __unicode__(self):
1584
        return _("<project %s '%s'>") % (self.id, self.application.name)
1585

    
1586
    O_PENDING = 0
1587
    O_ACTIVE = 1
1588
    O_DENIED = 3
1589
    O_DISMISSED = 4
1590
    O_CANCELLED = 5
1591
    O_SUSPENDED = 10
1592
    O_TERMINATED = 100
1593

    
1594
    O_STATE_DISPLAY = {
1595
        O_PENDING:    _("Pending"),
1596
        O_ACTIVE:     _("Active"),
1597
        O_DENIED:     _("Denied"),
1598
        O_DISMISSED:  _("Dismissed"),
1599
        O_CANCELLED:  _("Cancelled"),
1600
        O_SUSPENDED:  _("Suspended"),
1601
        O_TERMINATED: _("Terminated"),
1602
    }
1603

    
1604
    OVERALL_STATE = {
1605
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1606
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1607
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1608
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1609
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1610
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1611
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1612
    }
1613

    
1614
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1615

    
1616
    @classmethod
1617
    def o_state_q(cls, o_state):
1618
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1619
        return Q(state=p_state, application__state=a_state)
1620

    
1621
    @classmethod
1622
    def o_states_q(cls, o_states):
1623
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1624

    
1625
    INITIALIZED_STATES = [O_ACTIVE,
1626
                          O_SUSPENDED,
1627
                          O_TERMINATED,
1628
                          ]
1629

    
1630
    RELEVANT_STATES = [O_PENDING,
1631
                       O_DENIED,
1632
                       O_ACTIVE,
1633
                       O_SUSPENDED,
1634
                       O_TERMINATED,
1635
                       ]
1636

    
1637
    SKIP_STATES = [O_DISMISSED,
1638
                   O_CANCELLED,
1639
                   O_TERMINATED,
1640
                   ]
1641

    
1642
    @classmethod
1643
    def _overall_state(cls, project_state, app_state):
1644
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1645

    
1646
    def overall_state(self):
1647
        return self._overall_state(self.state, self.application.state)
1648

    
1649
    def last_pending_application(self):
1650
        apps = self.chained_apps.filter(
1651
            state=ProjectApplication.PENDING).order_by('-id')
1652
        if apps:
1653
            return apps[0]
1654
        return None
1655

    
1656
    def last_pending_modification(self):
1657
        last_pending = self.last_pending_application()
1658
        if last_pending == self.application:
1659
            return None
1660
        return last_pending
1661

    
1662
    def state_display(self):
1663
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1664

    
1665
    def expiration_info(self):
1666
        return (str(self.id), self.name, self.state_display(),
1667
                str(self.application.end_date))
1668

    
1669
    def last_deactivation(self):
1670
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1671
        ls = objs.order_by("-date")
1672
        if not ls:
1673
            return None
1674
        return ls[0]
1675

    
1676
    def is_deactivated(self, reason=None):
1677
        if reason is not None:
1678
            return self.state == reason
1679

    
1680
        return self.state != self.NORMAL
1681

    
1682
    def is_active(self):
1683
        return self.overall_state() == self.O_ACTIVE
1684

    
1685
    def is_initialized(self):
1686
        return self.overall_state() in self.INITIALIZED_STATES
1687

    
1688
    ### Deactivation calls
1689

    
1690
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1691
                    comments=None):
1692
        now = datetime.now()
1693
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1694
                        actor=actor, reason=reason, comments=comments)
1695

    
1696
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1697
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1698
                         comments=comments)
1699
        self.state = to_state
1700
        self.save()
1701

    
1702
    def terminate(self, actor=None, reason=None):
1703
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1704
        self.name = None
1705
        self.save()
1706

    
1707
    def suspend(self, actor=None, reason=None):
1708
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1709

    
1710
    def resume(self, actor=None, reason=None):
1711
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1712
        if self.name is None:
1713
            self.name = self.application.name
1714
            self.save()
1715

    
1716
    ### Logical checks
1717

    
1718
    @property
1719
    def is_alive(self):
1720
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1721

    
1722
    @property
1723
    def is_terminated(self):
1724
        return self.is_deactivated(self.TERMINATED)
1725

    
1726
    @property
1727
    def is_suspended(self):
1728
        return self.is_deactivated(self.SUSPENDED)
1729

    
1730
    def violates_members_limit(self, adding=0):
1731
        application = self.application
1732
        limit = application.limit_on_members_number
1733
        if limit is None:
1734
            return False
1735
        return (len(self.approved_members) + adding > limit)
1736

    
1737
    ### Other
1738

    
1739
    def count_pending_memberships(self):
1740
        return self.projectmembership_set.requested().count()
1741

    
1742
    def members_count(self):
1743
        return self.approved_memberships.count()
1744

    
1745
    @property
1746
    def approved_memberships(self):
1747
        query = ProjectMembership.Q_ACCEPTED_STATES
1748
        return self.projectmembership_set.filter(query)
1749

    
1750
    @property
1751
    def approved_members(self):
1752
        return [m.person for m in self.approved_memberships]
1753

    
1754

    
1755
class ProjectLogManager(models.Manager):
1756
    def last_deactivations(self, projects):
1757
        logs = self.filter(
1758
            project__in=projects,
1759
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1760
        return first_of_group(lambda l: l.project_id, logs)
1761

    
1762

    
1763
class ProjectLog(models.Model):
1764
    project = models.ForeignKey(Project, related_name="log")
1765
    from_state = models.IntegerField(null=True)
1766
    to_state = models.IntegerField()
1767
    date = models.DateTimeField()
1768
    actor = models.ForeignKey(AstakosUser, null=True)
1769
    reason = models.TextField(null=True)
1770
    comments = models.TextField(null=True)
1771

    
1772
    objects = ProjectLogManager()
1773

    
1774

    
1775
class ProjectLock(models.Model):
1776
    pass
1777

    
1778

    
1779
class ProjectMembershipManager(models.Manager):
1780

    
1781
    def any_accepted(self):
1782
        q = self.model.Q_ACCEPTED_STATES
1783
        return self.filter(q)
1784

    
1785
    def actually_accepted(self):
1786
        q = self.model.Q_ACTUALLY_ACCEPTED
1787
        return self.filter(q)
1788

    
1789
    def requested(self):
1790
        return self.filter(state=ProjectMembership.REQUESTED)
1791

    
1792
    def suspended(self):
1793
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1794

    
1795
    def associated(self):
1796
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1797

    
1798
    def any_accepted_per_project(self, projects):
1799
        ms = self.any_accepted().filter(project__in=projects)
1800
        return _partition_by(lambda m: m.project_id, ms)
1801

    
1802
    def requested_per_project(self, projects):
1803
        ms = self.requested().filter(project__in=projects)
1804
        return _partition_by(lambda m: m.project_id, ms)
1805

    
1806
    def one_per_project(self):
1807
        ms = self.all().select_related(
1808
            'project', 'project__application',
1809
            'project__application__owner', 'project_application__applicant',
1810
            'person')
1811
        m_per_p = {}
1812
        for m in ms:
1813
            m_per_p[m.project_id] = m
1814
        return m_per_p
1815

    
1816

    
1817
class ProjectMembership(models.Model):
1818

    
1819
    person = models.ForeignKey(AstakosUser)
1820
    project = models.ForeignKey(Project)
1821

    
1822
    REQUESTED = 0
1823
    ACCEPTED = 1
1824
    LEAVE_REQUESTED = 5
1825
    # User deactivation
1826
    USER_SUSPENDED = 10
1827
    REJECTED = 100
1828
    CANCELLED = 101
1829
    REMOVED = 200
1830

    
1831
    ASSOCIATED_STATES = set([REQUESTED,
1832
                             ACCEPTED,
1833
                             LEAVE_REQUESTED,
1834
                             USER_SUSPENDED,
1835
                             ])
1836

    
1837
    ACCEPTED_STATES = set([ACCEPTED,
1838
                           LEAVE_REQUESTED,
1839
                           USER_SUSPENDED,
1840
                           ])
1841

    
1842
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1843

    
1844
    state = models.IntegerField(default=REQUESTED,
1845
                                db_index=True)
1846

    
1847
    objects = ProjectMembershipManager()
1848

    
1849
    # Compiled queries
1850
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1851
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1852

    
1853
    MEMBERSHIP_STATE_DISPLAY = {
1854
        REQUESTED:       _('Requested'),
1855
        ACCEPTED:        _('Accepted'),
1856
        LEAVE_REQUESTED: _('Leave Requested'),
1857
        USER_SUSPENDED:  _('Suspended'),
1858
        REJECTED:        _('Rejected'),
1859
        CANCELLED:       _('Cancelled'),
1860
        REMOVED:         _('Removed'),
1861
    }
1862

    
1863
    USER_FRIENDLY_STATE_DISPLAY = {
1864
        REQUESTED:       _('Join requested'),
1865
        ACCEPTED:        _('Accepted member'),
1866
        LEAVE_REQUESTED: _('Requested to leave'),
1867
        USER_SUSPENDED:  _('Suspended member'),
1868
        REJECTED:        _('Request rejected'),
1869
        CANCELLED:       _('Request cancelled'),
1870
        REMOVED:         _('Removed member'),
1871
    }
1872

    
1873
    def state_display(self):
1874
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1875

    
1876
    def user_friendly_state_display(self):
1877
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1878

    
1879
    class Meta:
1880
        unique_together = ("person", "project")
1881
        #index_together = [["project", "state"]]
1882

    
1883
    def __str__(self):
1884
        return uenc(_("<'%s' membership in '%s'>") %
1885
                    (self.person.username, self.project))
1886

    
1887
    __repr__ = __str__
1888

    
1889
    def latest_log(self):
1890
        logs = self.log.all()
1891
        logs_d = _partition_by(lambda l: l.to_state, logs)
1892
        for s, s_logs in logs_d.iteritems():
1893
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1894
        return logs_d
1895

    
1896
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1897
                    comments=None):
1898
        now = datetime.now()
1899
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1900
                        actor=actor, reason=reason, comments=comments)
1901

    
1902
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1903
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1904
                         comments=comments)
1905
        self.state = to_state
1906
        self.save()
1907

    
1908
    ACTION_CHECKS = {
1909
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
1910
        "accept": lambda m: m.state == m.REQUESTED,
1911
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
1912
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
1913
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
1914
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1915
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1916
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
1917
        "reject": lambda m: m.state == m.REQUESTED,
1918
        "cancel": lambda m: m.state == m.REQUESTED,
1919
    }
1920

    
1921
    ACTION_STATES = {
1922
        "join":          REQUESTED,
1923
        "accept":        ACCEPTED,
1924
        "enroll":        ACCEPTED,
1925
        "leave_request": LEAVE_REQUESTED,
1926
        "deny_leave":    ACCEPTED,
1927
        "cancel_leave":  ACCEPTED,
1928
        "remove":        REMOVED,
1929
        "reject":        REJECTED,
1930
        "cancel":        CANCELLED,
1931
    }
1932

    
1933
    def check_action(self, action):
1934
        try:
1935
            check = self.ACTION_CHECKS[action]
1936
        except KeyError:
1937
            raise ValueError("No check found for action '%s'" % action)
1938
        return check(self)
1939

    
1940
    def perform_action(self, action, actor=None, reason=None):
1941
        if not self.check_action(action):
1942
            m = _("%s: attempted action '%s' in state '%s'") % (
1943
                self, action, self.state)
1944
            raise AssertionError(m)
1945
        try:
1946
            s = self.ACTION_STATES[action]
1947
        except KeyError:
1948
            raise ValueError("No such action '%s'" % action)
1949
        return self.set_state(s, actor=actor, reason=reason)
1950

    
1951

    
1952
class ProjectMembershipLogManager(models.Manager):
1953
    def last_logs(self, memberships):
1954
        logs = self.filter(membership__in=memberships).order_by("-date")
1955
        logs = _partition_by(lambda l: l.membership_id, logs)
1956

    
1957
        for memb_id, m_logs in logs.iteritems():
1958
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
1959
        return logs
1960

    
1961

    
1962
class ProjectMembershipLog(models.Model):
1963
    membership = models.ForeignKey(ProjectMembership, related_name="log")
1964
    from_state = models.IntegerField(null=True)
1965
    to_state = models.IntegerField()
1966
    date = models.DateTimeField()
1967
    actor = models.ForeignKey(AstakosUser, null=True)
1968
    reason = models.TextField(null=True)
1969
    comments = models.TextField(null=True)
1970

    
1971
    objects = ProjectMembershipLogManager()
1972

    
1973

    
1974
### SIGNALS ###
1975
################
1976

    
1977
def resource_post_save(sender, instance, created, **kwargs):
1978
    pass
1979

    
1980
post_save.connect(resource_post_save, sender=Resource)
1981

    
1982

    
1983
def renew_token(sender, instance, **kwargs):
1984
    if not instance.auth_token:
1985
        instance.renew_token()
1986
pre_save.connect(renew_token, sender=Component)