Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / models.py @ 5764728a

History | View | Annotate | Download (65.7 kB)

1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import 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
        self.delete_online_access_tokens()
566
        msg = 'Token renewed for %s'
567
        logger.log(astakos_settings.LOGGING_LEVEL, msg, self.log_display)
568

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
710
        modules = astakos_settings.IM_MODULES
711

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

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

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

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

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

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

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

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

    
748
        msg_extra = ''
749
        message = ''
750

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

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

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

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

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

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

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

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

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

    
816
    def delete_online_access_tokens(self):
817
        offline_tokens = self.token_set.filter(access_token='online')
818
        logger.info('The following access tokens will be deleted: %s',
819
                    offline_tokens)
820
        offline_tokens.delete()
821

    
822

    
823
class AstakosUserAuthProviderManager(models.Manager):
824

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

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

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

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

    
851

    
852
class AuthProviderPolicyProfileManager(models.Manager):
853

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

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

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

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

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

    
887

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

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

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

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

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

    
915
    objects = AuthProviderPolicyProfileManager()
916

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

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

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

    
936

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

    
955
    objects = AstakosUserAuthProviderManager()
956

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

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

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

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

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

    
981
        extra_data['info'] = info_data
982

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

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

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

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

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

    
1005

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

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

    
1014

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

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

    
1024

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

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

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

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

    
1052

    
1053
class EmailChangeManager(models.Manager):
1054

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

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

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

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

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

1071
        Throws ValueError if there is already
1072
        """
1073
        try:
1074
            email_change = self.model.objects.get(
1075
                activation_key=activation_key)
1076
            if email_change.activation_key_expired():
1077
                email_change.delete()
1078
                raise EmailChange.DoesNotExist
1079
            # is there an active user with this address?
1080
            try:
1081
                AstakosUser.objects.get(
1082
                    email__iexact=email_change.new_email_address)
1083
            except AstakosUser.DoesNotExist:
1084
                pass
1085
            else:
1086
                raise ValueError(_('The new email address is reserved.'))
1087
            # update user
1088
            user = AstakosUser.objects.select_for_update().\
1089
                get(pk=email_change.user_id)
1090
            old_email = user.email
1091
            user.set_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
        first, last = split_realname(value)
1203
        self.first_name = first
1204
        self.last_name = last
1205

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

    
1217
    def generate_token(self):
1218
        self.password = self.third_party_identifier
1219
        self.last_login = datetime.now()
1220
        self.token = default_token_generator.make_token(self)
1221

    
1222
    def existing_user(self):
1223
        return AstakosUser.objects.filter(
1224
            auth_providers__module=self.provider,
1225
            auth_providers__identifier=self.third_party_identifier)
1226

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

    
1235

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

    
1240

    
1241
class UserSetting(models.Model):
1242
    user = models.ForeignKey(AstakosUser)
1243
    setting = models.CharField(max_length=255)
1244
    value = models.IntegerField()
1245

    
1246
    class Meta:
1247
        unique_together = ("user", "setting")
1248

    
1249

    
1250
### PROJECTS ###
1251
################
1252

    
1253
class Chain(models.Model):
1254
    chain = models.AutoField(primary_key=True)
1255

    
1256
    def __str__(self):
1257
        return "%s" % (self.chain,)
1258

    
1259

    
1260
def new_chain():
1261
    c = Chain.objects.create()
1262
    return c
1263

    
1264

    
1265
class ProjectApplicationManager(models.Manager):
1266

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

    
1279

    
1280
class ProjectApplication(models.Model):
1281
    applicant = models.ForeignKey(
1282
        AstakosUser,
1283
        related_name='projects_applied',
1284
        db_index=True)
1285

    
1286
    PENDING = 0
1287
    APPROVED = 1
1288
    REPLACED = 2
1289
    DENIED = 3
1290
    DISMISSED = 4
1291
    CANCELLED = 5
1292

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

    
1327
    objects = ProjectApplicationManager()
1328

    
1329
    # Compiled queries
1330
    Q_PENDING = Q(state=PENDING)
1331
    Q_APPROVED = Q(state=APPROVED)
1332
    Q_DENIED = Q(state=DENIED)
1333

    
1334
    class Meta:
1335
        unique_together = ("chain", "id")
1336

    
1337
    def __unicode__(self):
1338
        return "%s applied by %s" % (self.name, self.applicant)
1339

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

    
1350
    @property
1351
    def log_display(self):
1352
        return "application %s (%s) for project %s" % (
1353
            self.id, self.name, self.chain)
1354

    
1355
    def state_display(self):
1356
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1357

    
1358
    @property
1359
    def grants(self):
1360
        return self.projectresourcegrant_set.values('member_capacity',
1361
                                                    'resource__name')
1362

    
1363
    @property
1364
    def resource_policies(self):
1365
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1366

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

    
1374
    def chained_applications(self):
1375
        return ProjectApplication.objects.filter(chain=self.chain)
1376

    
1377
    def denied_modifications(self):
1378
        q = self.chained_applications()
1379
        q = q.filter(Q(state=self.DENIED))
1380
        q = q.filter(~Q(id=self.id))
1381
        return q
1382

    
1383
    def last_denied(self):
1384
        try:
1385
            return self.denied_modifications().order_by('-id')[0]
1386
        except IndexError:
1387
            return None
1388

    
1389
    def has_denied_modifications(self):
1390
        return bool(self.last_denied())
1391

    
1392
    def can_cancel(self):
1393
        return self.state == self.PENDING
1394

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

    
1401
        self.state = self.CANCELLED
1402
        self.waive_date = datetime.now()
1403
        self.waive_reason = reason
1404
        self.waive_actor = actor
1405
        self.save()
1406

    
1407
    def can_dismiss(self):
1408
        return self.state == self.DENIED
1409

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

    
1416
        self.state = self.DISMISSED
1417
        self.waive_date = datetime.now()
1418
        self.waive_reason = reason
1419
        self.waive_actor = actor
1420
        self.save()
1421

    
1422
    def can_deny(self):
1423
        return self.state == self.PENDING
1424

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

    
1431
        self.state = self.DENIED
1432
        self.response_date = datetime.now()
1433
        self.response = reason
1434
        self.response_actor = actor
1435
        self.save()
1436

    
1437
    def can_approve(self):
1438
        return self.state == self.PENDING
1439

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

    
1446
        now = datetime.now()
1447
        self.state = self.APPROVED
1448
        self.response_date = now
1449
        self.response = reason
1450
        self.response_actor = actor
1451
        self.save()
1452

    
1453
    @property
1454
    def member_join_policy_display(self):
1455
        policy = self.member_join_policy
1456
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1457

    
1458
    @property
1459
    def member_leave_policy_display(self):
1460
        policy = self.member_leave_policy
1461
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1462

    
1463

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

    
1471

    
1472
class ProjectResourceGrant(models.Model):
1473

    
1474
    resource = models.ForeignKey(Resource)
1475
    project_application = models.ForeignKey(ProjectApplication,
1476
                                            null=True)
1477
    project_capacity = models.BigIntegerField(null=True)
1478
    member_capacity = models.BigIntegerField(default=0)
1479

    
1480
    objects = ProjectResourceGrantManager()
1481

    
1482
    class Meta:
1483
        unique_together = ("resource", "project_application")
1484

    
1485
    def display_member_capacity(self):
1486
        return units.show(self.member_capacity, self.resource.unit)
1487

    
1488
    def __str__(self):
1489
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1490
                                        self.display_member_capacity())
1491

    
1492

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

    
1504

    
1505
def invert_dict(d):
1506
    return dict((v, k) for k, v in d.iteritems())
1507

    
1508

    
1509
class ProjectManager(models.Manager):
1510

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

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

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

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

    
1543
        relevant = model.o_states_q(model.RELEVANT_STATES)
1544
        return self.filter(flt, relevant).order_by(
1545
            'application__issue_date').select_related(
1546
            'application', 'application__owner', 'application__applicant')
1547

    
1548
    def search_by_name(self, *search_strings):
1549
        q = Q()
1550
        for s in search_strings:
1551
            q = q | Q(name__icontains=s)
1552
        return self.filter(q)
1553

    
1554

    
1555
class Project(models.Model):
1556

    
1557
    id = models.BigIntegerField(db_column='id', primary_key=True)
1558

    
1559
    application = models.OneToOneField(
1560
        ProjectApplication,
1561
        related_name='project')
1562

    
1563
    members = models.ManyToManyField(
1564
        AstakosUser,
1565
        through='ProjectMembership')
1566

    
1567
    creation_date = models.DateTimeField(auto_now_add=True)
1568
    name = models.CharField(
1569
        max_length=80,
1570
        null=True,
1571
        db_index=True,
1572
        unique=True)
1573

    
1574
    NORMAL = 1
1575
    SUSPENDED = 10
1576
    TERMINATED = 100
1577

    
1578
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1579

    
1580
    state = models.IntegerField(default=NORMAL,
1581
                                db_index=True)
1582

    
1583
    objects = ProjectManager()
1584

    
1585
    def __str__(self):
1586
        return uenc(_("<project %s '%s'>") %
1587
                    (self.id, udec(self.application.name)))
1588

    
1589
    __repr__ = __str__
1590

    
1591
    def __unicode__(self):
1592
        return _("<project %s '%s'>") % (self.id, self.application.name)
1593

    
1594
    O_PENDING = 0
1595
    O_ACTIVE = 1
1596
    O_DENIED = 3
1597
    O_DISMISSED = 4
1598
    O_CANCELLED = 5
1599
    O_SUSPENDED = 10
1600
    O_TERMINATED = 100
1601

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

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

    
1622
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1623

    
1624
    @classmethod
1625
    def o_state_q(cls, o_state):
1626
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1627
        return Q(state=p_state, application__state=a_state)
1628

    
1629
    @classmethod
1630
    def o_states_q(cls, o_states):
1631
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1632

    
1633
    INITIALIZED_STATES = [O_ACTIVE,
1634
                          O_SUSPENDED,
1635
                          O_TERMINATED,
1636
                          ]
1637

    
1638
    RELEVANT_STATES = [O_PENDING,
1639
                       O_DENIED,
1640
                       O_ACTIVE,
1641
                       O_SUSPENDED,
1642
                       O_TERMINATED,
1643
                       ]
1644

    
1645
    SKIP_STATES = [O_DISMISSED,
1646
                   O_CANCELLED,
1647
                   O_TERMINATED,
1648
                   ]
1649

    
1650
    @classmethod
1651
    def _overall_state(cls, project_state, app_state):
1652
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1653

    
1654
    def overall_state(self):
1655
        return self._overall_state(self.state, self.application.state)
1656

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

    
1664
    def last_pending_modification(self):
1665
        last_pending = self.last_pending_application()
1666
        if last_pending == self.application:
1667
            return None
1668
        return last_pending
1669

    
1670
    def state_display(self):
1671
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1672

    
1673
    def expiration_info(self):
1674
        return (str(self.id), self.name, self.state_display(),
1675
                str(self.application.end_date))
1676

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

    
1684
    def is_deactivated(self, reason=None):
1685
        if reason is not None:
1686
            return self.state == reason
1687

    
1688
        return self.state != self.NORMAL
1689

    
1690
    def is_active(self):
1691
        return self.overall_state() == self.O_ACTIVE
1692

    
1693
    def is_initialized(self):
1694
        return self.overall_state() in self.INITIALIZED_STATES
1695

    
1696
    ### Deactivation calls
1697

    
1698
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1699
                    comments=None):
1700
        now = datetime.now()
1701
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1702
                        actor=actor, reason=reason, comments=comments)
1703

    
1704
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1705
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1706
                         comments=comments)
1707
        self.state = to_state
1708
        self.save()
1709

    
1710
    def terminate(self, actor=None, reason=None):
1711
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1712
        self.name = None
1713
        self.save()
1714

    
1715
    def suspend(self, actor=None, reason=None):
1716
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1717

    
1718
    def resume(self, actor=None, reason=None):
1719
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1720
        if self.name is None:
1721
            self.name = self.application.name
1722
            self.save()
1723

    
1724
    ### Logical checks
1725

    
1726
    @property
1727
    def is_alive(self):
1728
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1729

    
1730
    @property
1731
    def is_terminated(self):
1732
        return self.is_deactivated(self.TERMINATED)
1733

    
1734
    @property
1735
    def is_suspended(self):
1736
        return self.is_deactivated(self.SUSPENDED)
1737

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

    
1745
    ### Other
1746

    
1747
    def count_pending_memberships(self):
1748
        return self.projectmembership_set.requested().count()
1749

    
1750
    def members_count(self):
1751
        return self.approved_memberships.count()
1752

    
1753
    @property
1754
    def approved_memberships(self):
1755
        query = ProjectMembership.Q_ACCEPTED_STATES
1756
        return self.projectmembership_set.filter(query)
1757

    
1758
    @property
1759
    def approved_members(self):
1760
        return [m.person for m in self.approved_memberships]
1761

    
1762

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

    
1770

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

    
1780
    objects = ProjectLogManager()
1781

    
1782

    
1783
class ProjectLock(models.Model):
1784
    pass
1785

    
1786

    
1787
class ProjectMembershipManager(models.Manager):
1788

    
1789
    def any_accepted(self):
1790
        q = self.model.Q_ACCEPTED_STATES
1791
        return self.filter(q)
1792

    
1793
    def actually_accepted(self):
1794
        q = self.model.Q_ACTUALLY_ACCEPTED
1795
        return self.filter(q)
1796

    
1797
    def requested(self):
1798
        return self.filter(state=ProjectMembership.REQUESTED)
1799

    
1800
    def suspended(self):
1801
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1802

    
1803
    def associated(self):
1804
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1805

    
1806
    def any_accepted_per_project(self, projects):
1807
        ms = self.any_accepted().filter(project__in=projects)
1808
        return _partition_by(lambda m: m.project_id, ms)
1809

    
1810
    def requested_per_project(self, projects):
1811
        ms = self.requested().filter(project__in=projects)
1812
        return _partition_by(lambda m: m.project_id, ms)
1813

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

    
1824

    
1825
class ProjectMembership(models.Model):
1826

    
1827
    person = models.ForeignKey(AstakosUser)
1828
    project = models.ForeignKey(Project)
1829

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

    
1839
    ASSOCIATED_STATES = set([REQUESTED,
1840
                             ACCEPTED,
1841
                             LEAVE_REQUESTED,
1842
                             USER_SUSPENDED,
1843
                             ])
1844

    
1845
    ACCEPTED_STATES = set([ACCEPTED,
1846
                           LEAVE_REQUESTED,
1847
                           USER_SUSPENDED,
1848
                           ])
1849

    
1850
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1851

    
1852
    state = models.IntegerField(default=REQUESTED,
1853
                                db_index=True)
1854

    
1855
    objects = ProjectMembershipManager()
1856

    
1857
    # Compiled queries
1858
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1859
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1860

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

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

    
1881
    def state_display(self):
1882
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1883

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

    
1887
    class Meta:
1888
        unique_together = ("person", "project")
1889
        #index_together = [["project", "state"]]
1890

    
1891
    def __str__(self):
1892
        return uenc(_("<'%s' membership in '%s'>") %
1893
                    (self.person.username, self.project))
1894

    
1895
    __repr__ = __str__
1896

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

    
1904
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1905
                    comments=None):
1906
        now = datetime.now()
1907
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1908
                        actor=actor, reason=reason, comments=comments)
1909

    
1910
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1911
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1912
                         comments=comments)
1913
        self.state = to_state
1914
        self.save()
1915

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

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

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

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

    
1959

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

    
1965
        for memb_id, m_logs in logs.iteritems():
1966
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
1967
        return logs
1968

    
1969

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

    
1979
    objects = ProjectMembershipLogManager()
1980

    
1981

    
1982
### SIGNALS ###
1983
################
1984

    
1985
def resource_post_save(sender, instance, created, **kwargs):
1986
    pass
1987

    
1988
post_save.connect(resource_post_save, sender=Resource)
1989

    
1990

    
1991
def renew_token(sender, instance, **kwargs):
1992
    if not instance.auth_token:
1993
        instance.renew_token()
1994
pre_save.connect(renew_token, sender=Component)