Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (67 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
    allow_in_projects = models.BooleanField(default=True)
237

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

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

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

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

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

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

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

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

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

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

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

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

    
296

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

    
303

    
304
class AstakosUserManager(UserManager):
305

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

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

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

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

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

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

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

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

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

    
362

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
456
    objects = AstakosUserManager()
457

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
540
        self.update_uuid()
541

    
542
        if not self.verification_code:
543
            self.renew_verification_code()
544

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

    
549
        super(AstakosUser, self).save(**kwargs)
550

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

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

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

    
574
    def token_expired(self):
575
        return self.auth_token_expires < datetime.now()
576

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

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

    
591
    def __unicode__(self):
592
        return '%s (%s)' % (self.realname, self.email)
593

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

    
601
    def email_change_is_pending(self):
602
        return self.emailchanges.count() > 0
603

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

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

    
630
    @property
631
    def signed_terms(self):
632
        term = get_latest_terms()
633
        if not term:
634
            return True
635
        if not self.has_signed_terms:
636
            return False
637
        if not self.date_signed_terms:
638
            return False
639
        if self.date_signed_terms < term.date:
640
            self.has_signed_terms = False
641
            self.date_signed_terms = None
642
            self.save()
643
            return False
644
        return True
645

    
646
    def set_invitations_level(self):
647
        """
648
        Update user invitation level
649
        """
650
        level = self.invitation.inviter.level + 1
651
        self.level = level
652
        self.invitations = astakos_settings.INVITATIONS_PER_LEVEL.get(level, 0)
653

    
654
    def can_change_password(self):
655
        return self.has_auth_provider('local', auth_backend='astakos')
656

    
657
    def can_change_email(self):
658
        if not self.has_auth_provider('local'):
659
            return True
660

    
661
        local = self.get_auth_provider('local')._instance
662
        return local.auth_backend == 'astakos'
663

    
664
    # Auth providers related methods
665
    def get_auth_provider(self, module=None, identifier=None, **filters):
666
        if not module:
667
            return self.auth_providers.active()[0].settings
668

    
669
        params = {'module': module}
670
        if identifier:
671
            params['identifier'] = identifier
672
        params.update(filters)
673
        return self.auth_providers.active().get(**params).settings
674

    
675
    def has_auth_provider(self, provider, **kwargs):
676
        return bool(self.auth_providers.active().filter(module=provider,
677
                                                        **kwargs).count())
678

    
679
    def get_required_providers(self, **kwargs):
680
        return auth.REQUIRED_PROVIDERS.keys()
681

    
682
    def missing_required_providers(self):
683
        required = self.get_required_providers()
684
        missing = []
685
        for provider in required:
686
            if not self.has_auth_provider(provider):
687
                missing.append(auth.get_provider(provider, self))
688
        return missing
689

    
690
    def get_available_auth_providers(self, **filters):
691
        """
692
        Returns a list of providers available for add by the user.
693
        """
694
        modules = astakos_settings.IM_MODULES
695
        providers = []
696
        for p in modules:
697
            providers.append(auth.get_provider(p, self))
698
        available = []
699

    
700
        for p in providers:
701
            if p.get_add_policy:
702
                available.append(p)
703
        return available
704

    
705
    def get_disabled_auth_providers(self, **filters):
706
        providers = self.get_auth_providers(**filters)
707
        disabled = []
708
        for p in providers:
709
            if not p.get_login_policy:
710
                disabled.append(p)
711
        return disabled
712

    
713
    def get_enabled_auth_providers(self, **filters):
714
        providers = self.get_auth_providers(**filters)
715
        enabled = []
716
        for p in providers:
717
            if p.get_login_policy:
718
                enabled.append(p)
719
        return enabled
720

    
721
    def get_auth_providers(self, **filters):
722
        providers = []
723
        for provider in self.auth_providers.active(**filters):
724
            if provider.settings.module_enabled:
725
                providers.append(provider.settings)
726

    
727
        modules = astakos_settings.IM_MODULES
728

    
729
        def key(p):
730
            if not p.module in modules:
731
                return 100
732
            return modules.index(p.module)
733

    
734
        providers = sorted(providers, key=key)
735
        return providers
736

    
737
    # URL methods
738
    @property
739
    def auth_providers_display(self):
740
        return ",".join(["%s:%s" % (p.module, p.get_username_msg) for p in
741
                         self.get_enabled_auth_providers()])
742

    
743
    def add_auth_provider(self, module='local', identifier=None, **params):
744
        provider = auth.get_provider(module, self, identifier, **params)
745
        provider.add_to_user()
746

    
747
    def get_resend_activation_url(self):
748
        return reverse('send_activation', kwargs={'user_id': self.pk})
749

    
750
    def get_activation_url(self, nxt=False):
751
        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
752
                              quote(self.verification_code))
753
        if nxt:
754
            url += "&next=%s" % quote(nxt)
755
        return url
756

    
757
    def get_password_reset_url(self, token_generator=default_token_generator):
758
        return reverse('astakos.im.views.target.local.password_reset_confirm',
759
                       kwargs={'uidb36': int_to_base36(self.id),
760
                               'token': token_generator.make_token(self)})
761

    
762
    def get_inactive_message(self, provider_module, identifier=None):
763
        provider = self.get_auth_provider(provider_module, identifier)
764

    
765
        msg_extra = ''
766
        message = ''
767

    
768
        msg_inactive = provider.get_account_inactive_msg
769
        msg_pending = provider.get_pending_activation_msg
770
        msg_pending_help = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION_HELP)
771
        #msg_resend_prompt = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
772
        msg_pending_mod = provider.get_pending_moderation_msg
773
        msg_rejected = _(astakos_messages.ACCOUNT_REJECTED)
774
        msg_resend = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION)
775

    
776
        if not self.email_verified:
777
            message = msg_pending
778
            url = self.get_resend_activation_url()
779
            msg_extra = msg_pending_help + \
780
                u' ' + \
781
                '<a href="%s">%s?</a>' % (url, msg_resend)
782
        else:
783
            if not self.moderated:
784
                message = msg_pending_mod
785
            else:
786
                if self.is_rejected:
787
                    message = msg_rejected
788
                else:
789
                    message = msg_inactive
790

    
791
        return mark_safe(message + u' ' + msg_extra)
792

    
793
    def owns_application(self, application):
794
        return application.owner == self
795

    
796
    def owns_project(self, project):
797
        return project.application.owner == self
798

    
799
    def is_associated(self, project):
800
        try:
801
            m = ProjectMembership.objects.get(person=self, project=project)
802
            return m.state in ProjectMembership.ASSOCIATED_STATES
803
        except ProjectMembership.DoesNotExist:
804
            return False
805

    
806
    def get_membership(self, project):
807
        try:
808
            return ProjectMembership.objects.get(
809
                project=project,
810
                person=self)
811
        except ProjectMembership.DoesNotExist:
812
            return None
813

    
814
    def membership_display(self, project):
815
        m = self.get_membership(project)
816
        if m is None:
817
            return _('Not a member')
818
        else:
819
            return m.user_friendly_state_display()
820

    
821
    def non_owner_can_view(self, maybe_project):
822
        if self.is_project_admin():
823
            return True
824
        if maybe_project is None:
825
            return False
826
        project = maybe_project
827
        if self.is_associated(project):
828
            return True
829
        if project.is_deactivated():
830
            return False
831
        return True
832

    
833

    
834
class AstakosUserAuthProviderManager(models.Manager):
835

    
836
    def active(self, **filters):
837
        return self.filter(active=True, **filters)
838

    
839
    def remove_unverified_providers(self, provider, **filters):
840
        try:
841
            existing = self.filter(module=provider, user__email_verified=False,
842
                                   **filters)
843
            for p in existing:
844
                p.user.delete()
845
        except:
846
            pass
847

    
848
    def unverified(self, provider, **filters):
849
        try:
850
            return self.get(module=provider, user__email_verified=False,
851
                            **filters).settings
852
        except AstakosUserAuthProvider.DoesNotExist:
853
            return None
854

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

    
862

    
863
class AuthProviderPolicyProfileManager(models.Manager):
864

    
865
    def active(self):
866
        return self.filter(active=True)
867

    
868
    def for_user(self, user, provider):
869
        policies = {}
870
        exclusive_q1 = Q(provider=provider) & Q(is_exclusive=False)
871
        exclusive_q2 = ~Q(provider=provider) & Q(is_exclusive=True)
872
        exclusive_q = exclusive_q1 | exclusive_q2
873

    
874
        for profile in user.authpolicy_profiles.active().filter(exclusive_q):
875
            policies.update(profile.policies)
876

    
877
        user_groups = user.groups.all().values('pk')
878
        for profile in self.active().filter(groups__in=user_groups).filter(
879
                exclusive_q):
880
            policies.update(profile.policies)
881
        return policies
882

    
883
    def add_policy(self, name, provider, group_or_user, exclusive=False,
884
                   **policies):
885
        is_group = isinstance(group_or_user, Group)
886
        profile, created = self.get_or_create(name=name, provider=provider,
887
                                              is_exclusive=exclusive)
888
        profile.is_exclusive = exclusive
889
        profile.save()
890
        if is_group:
891
            profile.groups.add(group_or_user)
892
        else:
893
            profile.users.add(group_or_user)
894
        profile.set_policies(policies)
895
        profile.save()
896
        return profile
897

    
898

    
899
class AuthProviderPolicyProfile(models.Model):
900
    name = models.CharField(_('Name'), max_length=255, blank=False,
901
                            null=False, db_index=True)
902
    provider = models.CharField(_('Provider'), max_length=255, blank=False,
903
                                null=False)
904

    
905
    # apply policies to all providers excluding the one set in provider field
906
    is_exclusive = models.BooleanField(default=False)
907

    
908
    policy_add = models.NullBooleanField(null=True, default=None)
909
    policy_remove = models.NullBooleanField(null=True, default=None)
910
    policy_create = models.NullBooleanField(null=True, default=None)
911
    policy_login = models.NullBooleanField(null=True, default=None)
912
    policy_limit = models.IntegerField(null=True, default=None)
913
    policy_required = models.NullBooleanField(null=True, default=None)
914
    policy_automoderate = models.NullBooleanField(null=True, default=None)
915
    policy_switch = models.NullBooleanField(null=True, default=None)
916

    
917
    POLICY_FIELDS = ('add', 'remove', 'create', 'login', 'limit', 'required',
918
                     'automoderate')
919

    
920
    priority = models.IntegerField(null=False, default=1)
921
    groups = models.ManyToManyField(Group, related_name='authpolicy_profiles')
922
    users = models.ManyToManyField(AstakosUser,
923
                                   related_name='authpolicy_profiles')
924
    active = models.BooleanField(default=True)
925

    
926
    objects = AuthProviderPolicyProfileManager()
927

    
928
    class Meta:
929
        ordering = ['priority']
930

    
931
    @property
932
    def policies(self):
933
        policies = {}
934
        for pkey in self.POLICY_FIELDS:
935
            value = getattr(self, 'policy_%s' % pkey, None)
936
            if value is None:
937
                continue
938
            policies[pkey] = value
939
        return policies
940

    
941
    def set_policies(self, policies_dict):
942
        for key, value in policies_dict.iteritems():
943
            if key in self.POLICY_FIELDS:
944
                setattr(self, 'policy_%s' % key, value)
945
        return self.policies
946

    
947

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

    
966
    objects = AstakosUserAuthProviderManager()
967

    
968
    class Meta:
969
        unique_together = (('identifier', 'module', 'user'), )
970
        ordering = ('module', 'created')
971

    
972
    def __init__(self, *args, **kwargs):
973
        super(AstakosUserAuthProvider, self).__init__(*args, **kwargs)
974
        try:
975
            self.info = json.loads(self.info_data)
976
            if not self.info:
977
                self.info = {}
978
        except Exception:
979
            self.info = {}
980

    
981
        for key, value in self.info.iteritems():
982
            setattr(self, 'info_%s' % key, value)
983

    
984
    @property
985
    def settings(self):
986
        extra_data = {}
987

    
988
        info_data = {}
989
        if self.info_data:
990
            info_data = json.loads(self.info_data)
991

    
992
        extra_data['info'] = info_data
993

    
994
        for key in ['active', 'auth_backend', 'created', 'pk', 'affiliation']:
995
            extra_data[key] = getattr(self, key)
996

    
997
        extra_data['instance'] = self
998
        return auth.get_provider(self.module, self.user,
999
                                 self.identifier, **extra_data)
1000

    
1001
    def __repr__(self):
1002
        return '<AstakosUserAuthProvider %s:%s>' % (
1003
            self.module, self.identifier)
1004

    
1005
    def __unicode__(self):
1006
        if self.identifier:
1007
            return "%s:%s" % (self.module, self.identifier)
1008
        if self.auth_backend:
1009
            return "%s:%s" % (self.module, self.auth_backend)
1010
        return self.module
1011

    
1012
    def save(self, *args, **kwargs):
1013
        self.info_data = json.dumps(self.info)
1014
        return super(AstakosUserAuthProvider, self).save(*args, **kwargs)
1015

    
1016

    
1017
class AstakosUserQuota(models.Model):
1018
    capacity = models.BigIntegerField()
1019
    resource = models.ForeignKey(Resource)
1020
    user = models.ForeignKey(AstakosUser)
1021

    
1022
    class Meta:
1023
        unique_together = ("resource", "user")
1024

    
1025

    
1026
class ApprovalTerms(models.Model):
1027
    """
1028
    Model for approval terms
1029
    """
1030

    
1031
    date = models.DateTimeField(
1032
        _('Issue date'), db_index=True, auto_now_add=True)
1033
    location = models.CharField(_('Terms location'), max_length=255)
1034

    
1035

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

    
1050
    def __init__(self, *args, **kwargs):
1051
        super(Invitation, self).__init__(*args, **kwargs)
1052
        if not self.id:
1053
            self.code = _generate_invitation_code()
1054

    
1055
    def consume(self):
1056
        self.is_consumed = True
1057
        self.consumed = datetime.now()
1058
        self.save()
1059

    
1060
    def __unicode__(self):
1061
        return '%s -> %s [%d]' % (self.inviter, self.username, self.code)
1062

    
1063

    
1064
class EmailChangeManager(models.Manager):
1065

    
1066
    @transaction.commit_on_success
1067
    def change_email(self, activation_key):
1068
        """
1069
        Validate an activation key and change the corresponding
1070
        ``User`` if valid.
1071

1072
        If the key is valid and has not expired, return the ``User``
1073
        after activating.
1074

1075
        If the key is not valid or has expired, return ``None``.
1076

1077
        If the key is valid but the ``User`` is already active,
1078
        return ``None``.
1079

1080
        After successful email change the activation record is deleted.
1081

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

    
1111

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

    
1124
    objects = EmailChangeManager()
1125

    
1126
    def get_url(self):
1127
        return reverse('email_change_confirm',
1128
                       kwargs={'activation_key': self.activation_key})
1129

    
1130
    def activation_key_expired(self):
1131
        expiration_date = timedelta(
1132
            days=astakos_settings.EMAILCHANGE_ACTIVATION_DAYS)
1133
        return self.requested_at + expiration_date < datetime.now()
1134

    
1135

    
1136
class AdditionalMail(models.Model):
1137
    """
1138
    Model for registring invitations
1139
    """
1140
    owner = models.ForeignKey(AstakosUser)
1141
    email = models.EmailField()
1142

    
1143

    
1144
def _generate_invitation_code():
1145
    while True:
1146
        code = randint(1, 2L ** 63 - 1)
1147
        try:
1148
            Invitation.objects.get(code=code)
1149
            # An invitation with this code already exists, try again
1150
        except Invitation.DoesNotExist:
1151
            return code
1152

    
1153

    
1154
def get_latest_terms():
1155
    try:
1156
        term = ApprovalTerms.objects.order_by('-id')[0]
1157
        return term
1158
    except IndexError:
1159
        pass
1160
    return None
1161

    
1162

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

    
1185
    class Meta:
1186
        unique_together = ("provider", "third_party_identifier")
1187

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

    
1204
        return user
1205

    
1206
    @property
1207
    def realname(self):
1208
        return '%s %s' % (self.first_name, self.last_name)
1209

    
1210
    @realname.setter
1211
    def realname(self, value):
1212
        parts = value.split(' ')
1213
        if len(parts) == 2:
1214
            self.first_name = parts[0]
1215
            self.last_name = parts[1]
1216
        else:
1217
            self.last_name = parts[0]
1218

    
1219
    def save(self, *args, **kwargs):
1220
        if not self.id:
1221
            # set username
1222
            while not self.username:
1223
                username = uuid.uuid4().hex[:30]
1224
                try:
1225
                    AstakosUser.objects.get(username=username)
1226
                except AstakosUser.DoesNotExist:
1227
                    self.username = username
1228
        super(PendingThirdPartyUser, self).save(*args, **kwargs)
1229

    
1230
    def generate_token(self):
1231
        self.password = self.third_party_identifier
1232
        self.last_login = datetime.now()
1233
        self.token = default_token_generator.make_token(self)
1234

    
1235
    def existing_user(self):
1236
        return AstakosUser.objects.filter(
1237
            auth_providers__module=self.provider,
1238
            auth_providers__identifier=self.third_party_identifier)
1239

    
1240
    def get_provider(self, user):
1241
        params = {
1242
            'info_data': self.info,
1243
            'affiliation': self.affiliation
1244
        }
1245
        return auth.get_provider(self.provider, user,
1246
                                 self.third_party_identifier, **params)
1247

    
1248

    
1249
class SessionCatalog(models.Model):
1250
    session_key = models.CharField(_('session key'), max_length=40)
1251
    user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
1252

    
1253

    
1254
class UserSetting(models.Model):
1255
    user = models.ForeignKey(AstakosUser)
1256
    setting = models.CharField(max_length=255)
1257
    value = models.IntegerField()
1258

    
1259
    class Meta:
1260
        unique_together = ("user", "setting")
1261

    
1262

    
1263
### PROJECTS ###
1264
################
1265

    
1266
class Chain(models.Model):
1267
    chain = models.AutoField(primary_key=True)
1268

    
1269
    def __str__(self):
1270
        return "%s" % (self.chain,)
1271

    
1272

    
1273
def new_chain():
1274
    c = Chain.objects.create()
1275
    return c
1276

    
1277

    
1278
class ProjectApplicationManager(models.Manager):
1279

    
1280
    def pending_per_project(self, projects):
1281
        apps = self.filter(state=self.model.PENDING,
1282
                           chain__in=projects).order_by('chain', '-id')
1283
        checked_chain = None
1284
        projs = {}
1285
        for app in apps:
1286
            chain = app.chain_id
1287
            if chain != checked_chain:
1288
                checked_chain = chain
1289
                projs[chain] = app
1290
        return projs
1291

    
1292

    
1293
class ProjectApplication(models.Model):
1294
    applicant = models.ForeignKey(
1295
        AstakosUser,
1296
        related_name='projects_applied',
1297
        db_index=True)
1298

    
1299
    PENDING = 0
1300
    APPROVED = 1
1301
    REPLACED = 2
1302
    DENIED = 3
1303
    DISMISSED = 4
1304
    CANCELLED = 5
1305

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

    
1340
    objects = ProjectApplicationManager()
1341

    
1342
    # Compiled queries
1343
    Q_PENDING = Q(state=PENDING)
1344
    Q_APPROVED = Q(state=APPROVED)
1345
    Q_DENIED = Q(state=DENIED)
1346

    
1347
    class Meta:
1348
        unique_together = ("chain", "id")
1349

    
1350
    def __unicode__(self):
1351
        return "%s applied by %s" % (self.name, self.applicant)
1352

    
1353
    # TODO: Move to a more suitable place
1354
    APPLICATION_STATE_DISPLAY = {
1355
        PENDING:   _('Pending review'),
1356
        APPROVED:  _('Approved'),
1357
        REPLACED:  _('Replaced'),
1358
        DENIED:    _('Denied'),
1359
        DISMISSED: _('Dismissed'),
1360
        CANCELLED: _('Cancelled')
1361
    }
1362

    
1363
    @property
1364
    def log_display(self):
1365
        return "application %s (%s) for project %s" % (
1366
            self.id, self.name, self.chain)
1367

    
1368
    def state_display(self):
1369
        return self.APPLICATION_STATE_DISPLAY.get(self.state, _('Unknown'))
1370

    
1371
    @property
1372
    def grants(self):
1373
        return self.projectresourcegrant_set.values('member_capacity',
1374
                                                    'resource__name')
1375

    
1376
    @property
1377
    def resource_policies(self):
1378
        return [str(rp) for rp in self.projectresourcegrant_set.all()]
1379

    
1380
    def is_modification(self):
1381
        # if self.state != self.PENDING:
1382
        #     return False
1383
        parents = self.chained_applications().filter(id__lt=self.id)
1384
        parents = parents.filter(state__in=[self.APPROVED])
1385
        return parents.count() > 0
1386

    
1387
    def chained_applications(self):
1388
        return ProjectApplication.objects.filter(chain=self.chain)
1389

    
1390
    def denied_modifications(self):
1391
        q = self.chained_applications()
1392
        q = q.filter(Q(state=self.DENIED))
1393
        q = q.filter(~Q(id=self.id))
1394
        return q
1395

    
1396
    def last_denied(self):
1397
        try:
1398
            return self.denied_modifications().order_by('-id')[0]
1399
        except IndexError:
1400
            return None
1401

    
1402
    def has_denied_modifications(self):
1403
        return bool(self.last_denied())
1404

    
1405
    def can_cancel(self):
1406
        return self.state == self.PENDING
1407

    
1408
    def cancel(self, actor=None, reason=None):
1409
        if not self.can_cancel():
1410
            m = _("cannot cancel: application '%s' in state '%s'") % (
1411
                self.id, self.state)
1412
            raise AssertionError(m)
1413

    
1414
        self.state = self.CANCELLED
1415
        self.waive_date = datetime.now()
1416
        self.waive_reason = reason
1417
        self.waive_actor = actor
1418
        self.save()
1419

    
1420
    def can_dismiss(self):
1421
        return self.state == self.DENIED
1422

    
1423
    def dismiss(self, actor=None, reason=None):
1424
        if not self.can_dismiss():
1425
            m = _("cannot dismiss: application '%s' in state '%s'") % (
1426
                self.id, self.state)
1427
            raise AssertionError(m)
1428

    
1429
        self.state = self.DISMISSED
1430
        self.waive_date = datetime.now()
1431
        self.waive_reason = reason
1432
        self.waive_actor = actor
1433
        self.save()
1434

    
1435
    def can_deny(self):
1436
        return self.state == self.PENDING
1437

    
1438
    def deny(self, actor=None, reason=None):
1439
        if not self.can_deny():
1440
            m = _("cannot deny: application '%s' in state '%s'") % (
1441
                self.id, self.state)
1442
            raise AssertionError(m)
1443

    
1444
        self.state = self.DENIED
1445
        self.response_date = datetime.now()
1446
        self.response = reason
1447
        self.response_actor = actor
1448
        self.save()
1449

    
1450
    def can_approve(self):
1451
        return self.state == self.PENDING
1452

    
1453
    def approve(self, actor=None, reason=None):
1454
        if not self.can_approve():
1455
            m = _("cannot approve: project '%s' in state '%s'") % (
1456
                self.name, self.state)
1457
            raise AssertionError(m)  # invalid argument
1458

    
1459
        now = datetime.now()
1460
        self.state = self.APPROVED
1461
        self.response_date = now
1462
        self.response = reason
1463
        self.response_actor = actor
1464
        self.save()
1465

    
1466
    @property
1467
    def member_join_policy_display(self):
1468
        policy = self.member_join_policy
1469
        return presentation.PROJECT_MEMBER_JOIN_POLICIES.get(policy)
1470

    
1471
    @property
1472
    def member_leave_policy_display(self):
1473
        policy = self.member_leave_policy
1474
        return presentation.PROJECT_MEMBER_LEAVE_POLICIES.get(policy)
1475

    
1476

    
1477
class ProjectResourceGrantManager(models.Manager):
1478
    def grants_per_app(self, applications):
1479
        app_ids = [app.id for app in applications]
1480
        grants = self.filter(
1481
            project_application__in=app_ids).select_related("resource")
1482
        return _partition_by(lambda g: g.project_application_id, grants)
1483

    
1484

    
1485
class ProjectResourceGrant(models.Model):
1486

    
1487
    resource = models.ForeignKey(Resource)
1488
    project_application = models.ForeignKey(ProjectApplication,
1489
                                            null=True)
1490
    project_capacity = models.BigIntegerField(null=True)
1491
    member_capacity = models.BigIntegerField(default=0)
1492

    
1493
    objects = ProjectResourceGrantManager()
1494

    
1495
    class Meta:
1496
        unique_together = ("resource", "project_application")
1497

    
1498
    def display_member_capacity(self):
1499
        return units.show(self.member_capacity, self.resource.unit)
1500

    
1501
    def __str__(self):
1502
        return 'Max %s per user: %s' % (self.resource.pluralized_display_name,
1503
                                        self.display_member_capacity())
1504

    
1505

    
1506
def _distinct(f, l):
1507
    d = {}
1508
    last = None
1509
    for x in l:
1510
        group = f(x)
1511
        if group == last:
1512
            continue
1513
        last = group
1514
        d[group] = x
1515
    return d
1516

    
1517

    
1518
def invert_dict(d):
1519
    return dict((v, k) for k, v in d.iteritems())
1520

    
1521

    
1522
class ProjectManager(models.Manager):
1523

    
1524
    def all_with_pending(self, flt=None):
1525
        flt = Q() if flt is None else flt
1526
        projects = list(self.select_related(
1527
            'application', 'application__owner').filter(flt))
1528

    
1529
        objs = ProjectApplication.objects.select_related('owner')
1530
        apps = objs.filter(state=ProjectApplication.PENDING,
1531
                           chain__in=projects).order_by('chain', '-id')
1532
        app_d = _distinct(lambda app: app.chain_id, apps)
1533
        return [(project, app_d.get(project.pk)) for project in projects]
1534

    
1535
    def expired_projects(self):
1536
        model = self.model
1537
        q = ((model.o_state_q(model.O_ACTIVE) |
1538
              model.o_state_q(model.O_SUSPENDED)) &
1539
             Q(application__end_date__lt=datetime.now()))
1540
        return self.filter(q)
1541

    
1542
    def user_accessible_projects(self, user):
1543
        """
1544
        Return projects accessible by specified user.
1545
        """
1546
        model = self.model
1547
        if user.is_project_admin():
1548
            flt = Q()
1549
        else:
1550
            membs = user.projectmembership_set.associated()
1551
            memb_projects = membs.values_list("project", flat=True)
1552
            flt = (Q(application__owner=user) |
1553
                   Q(application__applicant=user) |
1554
                   Q(id__in=memb_projects))
1555

    
1556
        relevant = model.o_states_q(model.RELEVANT_STATES)
1557
        return self.filter(flt, relevant).order_by(
1558
            'application__issue_date').select_related(
1559
            'application', 'application__owner', 'application__applicant')
1560

    
1561
    def search_by_name(self, *search_strings):
1562
        q = Q()
1563
        for s in search_strings:
1564
            q = q | Q(name__icontains=s)
1565
        return self.filter(q)
1566

    
1567

    
1568
class Project(models.Model):
1569

    
1570
    id = models.BigIntegerField(db_column='id', primary_key=True)
1571

    
1572
    application = models.OneToOneField(
1573
        ProjectApplication,
1574
        related_name='project')
1575

    
1576
    members = models.ManyToManyField(
1577
        AstakosUser,
1578
        through='ProjectMembership')
1579

    
1580
    creation_date = models.DateTimeField(auto_now_add=True)
1581
    name = models.CharField(
1582
        max_length=80,
1583
        null=True,
1584
        db_index=True,
1585
        unique=True)
1586

    
1587
    NORMAL = 1
1588
    SUSPENDED = 10
1589
    TERMINATED = 100
1590

    
1591
    DEACTIVATED_STATES = [SUSPENDED, TERMINATED]
1592

    
1593
    state = models.IntegerField(default=NORMAL,
1594
                                db_index=True)
1595

    
1596
    objects = ProjectManager()
1597

    
1598
    def __str__(self):
1599
        return uenc(_("<project %s '%s'>") %
1600
                    (self.id, udec(self.application.name)))
1601

    
1602
    __repr__ = __str__
1603

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

    
1607
    O_PENDING = 0
1608
    O_ACTIVE = 1
1609
    O_DENIED = 3
1610
    O_DISMISSED = 4
1611
    O_CANCELLED = 5
1612
    O_SUSPENDED = 10
1613
    O_TERMINATED = 100
1614

    
1615
    O_STATE_DISPLAY = {
1616
        O_PENDING:    _("Pending"),
1617
        O_ACTIVE:     _("Active"),
1618
        O_DENIED:     _("Denied"),
1619
        O_DISMISSED:  _("Dismissed"),
1620
        O_CANCELLED:  _("Cancelled"),
1621
        O_SUSPENDED:  _("Suspended"),
1622
        O_TERMINATED: _("Terminated"),
1623
    }
1624

    
1625
    OVERALL_STATE = {
1626
        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
1627
        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
1628
        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
1629
        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
1630
        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
1631
        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
1632
        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
1633
    }
1634

    
1635
    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
1636

    
1637
    @classmethod
1638
    def o_state_q(cls, o_state):
1639
        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
1640
        return Q(state=p_state, application__state=a_state)
1641

    
1642
    @classmethod
1643
    def o_states_q(cls, o_states):
1644
        return reduce(lambda x, y: x | y, map(cls.o_state_q, o_states), Q())
1645

    
1646
    INITIALIZED_STATES = [O_ACTIVE,
1647
                          O_SUSPENDED,
1648
                          O_TERMINATED,
1649
                          ]
1650

    
1651
    RELEVANT_STATES = [O_PENDING,
1652
                       O_DENIED,
1653
                       O_ACTIVE,
1654
                       O_SUSPENDED,
1655
                       O_TERMINATED,
1656
                       ]
1657

    
1658
    SKIP_STATES = [O_DISMISSED,
1659
                   O_CANCELLED,
1660
                   O_TERMINATED,
1661
                   ]
1662

    
1663
    @classmethod
1664
    def _overall_state(cls, project_state, app_state):
1665
        return cls.OVERALL_STATE.get((project_state, app_state), None)
1666

    
1667
    def overall_state(self):
1668
        return self._overall_state(self.state, self.application.state)
1669

    
1670
    def last_pending_application(self):
1671
        apps = self.chained_apps.filter(
1672
            state=ProjectApplication.PENDING).order_by('-id')
1673
        if apps:
1674
            return apps[0]
1675
        return None
1676

    
1677
    def last_pending_modification(self):
1678
        last_pending = self.last_pending_application()
1679
        if last_pending == self.application:
1680
            return None
1681
        return last_pending
1682

    
1683
    def state_display(self):
1684
        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
1685

    
1686
    def expiration_info(self):
1687
        return (str(self.id), self.name, self.state_display(),
1688
                str(self.application.end_date))
1689

    
1690
    def last_deactivation(self):
1691
        objs = self.log.filter(to_state__in=self.DEACTIVATED_STATES)
1692
        ls = objs.order_by("-date")
1693
        if not ls:
1694
            return None
1695
        return ls[0]
1696

    
1697
    def is_deactivated(self, reason=None):
1698
        if reason is not None:
1699
            return self.state == reason
1700

    
1701
        return self.state != self.NORMAL
1702

    
1703
    def is_active(self):
1704
        return self.overall_state() == self.O_ACTIVE
1705

    
1706
    def is_initialized(self):
1707
        return self.overall_state() in self.INITIALIZED_STATES
1708

    
1709
    ### Deactivation calls
1710

    
1711
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1712
                    comments=None):
1713
        now = datetime.now()
1714
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1715
                        actor=actor, reason=reason, comments=comments)
1716

    
1717
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1718
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1719
                         comments=comments)
1720
        self.state = to_state
1721
        self.save()
1722

    
1723
    def terminate(self, actor=None, reason=None):
1724
        self.set_state(self.TERMINATED, actor=actor, reason=reason)
1725
        self.name = None
1726
        self.save()
1727

    
1728
    def suspend(self, actor=None, reason=None):
1729
        self.set_state(self.SUSPENDED, actor=actor, reason=reason)
1730

    
1731
    def resume(self, actor=None, reason=None):
1732
        self.set_state(self.NORMAL, actor=actor, reason=reason)
1733
        if self.name is None:
1734
            self.name = self.application.name
1735
            self.save()
1736

    
1737
    ### Logical checks
1738

    
1739
    @property
1740
    def is_alive(self):
1741
        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
1742

    
1743
    @property
1744
    def is_terminated(self):
1745
        return self.is_deactivated(self.TERMINATED)
1746

    
1747
    @property
1748
    def is_suspended(self):
1749
        return self.is_deactivated(self.SUSPENDED)
1750

    
1751
    def violates_members_limit(self, adding=0):
1752
        application = self.application
1753
        limit = application.limit_on_members_number
1754
        if limit is None:
1755
            return False
1756
        return (len(self.approved_members) + adding > limit)
1757

    
1758
    ### Other
1759

    
1760
    def count_pending_memberships(self):
1761
        return self.projectmembership_set.requested().count()
1762

    
1763
    def members_count(self):
1764
        return self.approved_memberships.count()
1765

    
1766
    @property
1767
    def approved_memberships(self):
1768
        query = ProjectMembership.Q_ACCEPTED_STATES
1769
        return self.projectmembership_set.filter(query)
1770

    
1771
    @property
1772
    def approved_members(self):
1773
        return [m.person for m in self.approved_memberships]
1774

    
1775

    
1776
class ProjectLogManager(models.Manager):
1777
    def last_deactivations(self, projects):
1778
        logs = self.filter(
1779
            project__in=projects,
1780
            to_state__in=Project.DEACTIVATED_STATES).order_by("-date")
1781
        return first_of_group(lambda l: l.project_id, logs)
1782

    
1783

    
1784
class ProjectLog(models.Model):
1785
    project = models.ForeignKey(Project, related_name="log")
1786
    from_state = models.IntegerField(null=True)
1787
    to_state = models.IntegerField()
1788
    date = models.DateTimeField()
1789
    actor = models.ForeignKey(AstakosUser, null=True)
1790
    reason = models.TextField(null=True)
1791
    comments = models.TextField(null=True)
1792

    
1793
    objects = ProjectLogManager()
1794

    
1795

    
1796
class ProjectLock(models.Model):
1797
    pass
1798

    
1799

    
1800
class ProjectMembershipManager(models.Manager):
1801

    
1802
    def any_accepted(self):
1803
        q = self.model.Q_ACCEPTED_STATES
1804
        return self.filter(q)
1805

    
1806
    def actually_accepted(self):
1807
        q = self.model.Q_ACTUALLY_ACCEPTED
1808
        return self.filter(q)
1809

    
1810
    def requested(self):
1811
        return self.filter(state=ProjectMembership.REQUESTED)
1812

    
1813
    def suspended(self):
1814
        return self.filter(state=ProjectMembership.USER_SUSPENDED)
1815

    
1816
    def associated(self):
1817
        return self.filter(state__in=ProjectMembership.ASSOCIATED_STATES)
1818

    
1819
    def any_accepted_per_project(self, projects):
1820
        ms = self.any_accepted().filter(project__in=projects)
1821
        return _partition_by(lambda m: m.project_id, ms)
1822

    
1823
    def requested_per_project(self, projects):
1824
        ms = self.requested().filter(project__in=projects)
1825
        return _partition_by(lambda m: m.project_id, ms)
1826

    
1827
    def one_per_project(self):
1828
        ms = self.all().select_related(
1829
            'project', 'project__application',
1830
            'project__application__owner', 'project_application__applicant',
1831
            'person')
1832
        m_per_p = {}
1833
        for m in ms:
1834
            m_per_p[m.project_id] = m
1835
        return m_per_p
1836

    
1837

    
1838
class ProjectMembership(models.Model):
1839

    
1840
    person = models.ForeignKey(AstakosUser)
1841
    project = models.ForeignKey(Project)
1842

    
1843
    REQUESTED = 0
1844
    ACCEPTED = 1
1845
    LEAVE_REQUESTED = 5
1846
    # User deactivation
1847
    USER_SUSPENDED = 10
1848
    REJECTED = 100
1849
    CANCELLED = 101
1850
    REMOVED = 200
1851

    
1852
    ASSOCIATED_STATES = set([REQUESTED,
1853
                             ACCEPTED,
1854
                             LEAVE_REQUESTED,
1855
                             USER_SUSPENDED,
1856
                             ])
1857

    
1858
    ACCEPTED_STATES = set([ACCEPTED,
1859
                           LEAVE_REQUESTED,
1860
                           USER_SUSPENDED,
1861
                           ])
1862

    
1863
    ACTUALLY_ACCEPTED = set([ACCEPTED, LEAVE_REQUESTED])
1864

    
1865
    state = models.IntegerField(default=REQUESTED,
1866
                                db_index=True)
1867

    
1868
    objects = ProjectMembershipManager()
1869

    
1870
    # Compiled queries
1871
    Q_ACCEPTED_STATES = Q(state__in=ACCEPTED_STATES)
1872
    Q_ACTUALLY_ACCEPTED = Q(state=ACCEPTED) | Q(state=LEAVE_REQUESTED)
1873

    
1874
    MEMBERSHIP_STATE_DISPLAY = {
1875
        REQUESTED:       _('Requested'),
1876
        ACCEPTED:        _('Accepted'),
1877
        LEAVE_REQUESTED: _('Leave Requested'),
1878
        USER_SUSPENDED:  _('Suspended'),
1879
        REJECTED:        _('Rejected'),
1880
        CANCELLED:       _('Cancelled'),
1881
        REMOVED:         _('Removed'),
1882
    }
1883

    
1884
    USER_FRIENDLY_STATE_DISPLAY = {
1885
        REQUESTED:       _('Join requested'),
1886
        ACCEPTED:        _('Accepted member'),
1887
        LEAVE_REQUESTED: _('Requested to leave'),
1888
        USER_SUSPENDED:  _('Suspended member'),
1889
        REJECTED:        _('Request rejected'),
1890
        CANCELLED:       _('Request cancelled'),
1891
        REMOVED:         _('Removed member'),
1892
    }
1893

    
1894
    def state_display(self):
1895
        return self.MEMBERSHIP_STATE_DISPLAY.get(self.state, _('Unknown'))
1896

    
1897
    def user_friendly_state_display(self):
1898
        return self.USER_FRIENDLY_STATE_DISPLAY.get(self.state, _('Unknown'))
1899

    
1900
    class Meta:
1901
        unique_together = ("person", "project")
1902
        #index_together = [["project", "state"]]
1903

    
1904
    def __str__(self):
1905
        return uenc(_("<'%s' membership in '%s'>") %
1906
                    (self.person.username, self.project))
1907

    
1908
    __repr__ = __str__
1909

    
1910
    def latest_log(self):
1911
        logs = self.log.all()
1912
        logs_d = _partition_by(lambda l: l.to_state, logs)
1913
        for s, s_logs in logs_d.iteritems():
1914
            logs_d[s] = max(s_logs, key=(lambda l: l.date))
1915
        return logs_d
1916

    
1917
    def _log_create(self, from_state, to_state, actor=None, reason=None,
1918
                    comments=None):
1919
        now = datetime.now()
1920
        self.log.create(from_state=from_state, to_state=to_state, date=now,
1921
                        actor=actor, reason=reason, comments=comments)
1922

    
1923
    def set_state(self, to_state, actor=None, reason=None, comments=None):
1924
        self._log_create(self.state, to_state, actor=actor, reason=reason,
1925
                         comments=comments)
1926
        self.state = to_state
1927
        self.save()
1928

    
1929
    ACTION_CHECKS = {
1930
        "join": lambda m: m.state not in m.ASSOCIATED_STATES,
1931
        "accept": lambda m: m.state == m.REQUESTED,
1932
        "enroll": lambda m: m.state not in m.ACCEPTED_STATES,
1933
        "leave": lambda m: m.state in m.ACCEPTED_STATES,
1934
        "leave_request": lambda m: m.state in m.ACCEPTED_STATES,
1935
        "deny_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1936
        "cancel_leave": lambda m: m.state == m.LEAVE_REQUESTED,
1937
        "remove": lambda m: m.state in m.ACCEPTED_STATES,
1938
        "reject": lambda m: m.state == m.REQUESTED,
1939
        "cancel": lambda m: m.state == m.REQUESTED,
1940
    }
1941

    
1942
    ACTION_STATES = {
1943
        "join":          REQUESTED,
1944
        "accept":        ACCEPTED,
1945
        "enroll":        ACCEPTED,
1946
        "leave_request": LEAVE_REQUESTED,
1947
        "deny_leave":    ACCEPTED,
1948
        "cancel_leave":  ACCEPTED,
1949
        "remove":        REMOVED,
1950
        "reject":        REJECTED,
1951
        "cancel":        CANCELLED,
1952
    }
1953

    
1954
    def check_action(self, action):
1955
        try:
1956
            check = self.ACTION_CHECKS[action]
1957
        except KeyError:
1958
            raise ValueError("No check found for action '%s'" % action)
1959
        return check(self)
1960

    
1961
    def perform_action(self, action, actor=None, reason=None):
1962
        if not self.check_action(action):
1963
            m = _("%s: attempted action '%s' in state '%s'") % (
1964
                self, action, self.state)
1965
            raise AssertionError(m)
1966
        try:
1967
            s = self.ACTION_STATES[action]
1968
        except KeyError:
1969
            raise ValueError("No such action '%s'" % action)
1970
        return self.set_state(s, actor=actor, reason=reason)
1971

    
1972

    
1973
class ProjectMembershipLogManager(models.Manager):
1974
    def last_logs(self, memberships):
1975
        logs = self.filter(membership__in=memberships).order_by("-date")
1976
        logs = _partition_by(lambda l: l.membership_id, logs)
1977

    
1978
        for memb_id, m_logs in logs.iteritems():
1979
            logs[memb_id] = first_of_group(lambda l: l.to_state, m_logs)
1980
        return logs
1981

    
1982

    
1983
class ProjectMembershipLog(models.Model):
1984
    membership = models.ForeignKey(ProjectMembership, related_name="log")
1985
    from_state = models.IntegerField(null=True)
1986
    to_state = models.IntegerField()
1987
    date = models.DateTimeField()
1988
    actor = models.ForeignKey(AstakosUser, null=True)
1989
    reason = models.TextField(null=True)
1990
    comments = models.TextField(null=True)
1991

    
1992
    objects = ProjectMembershipLogManager()
1993

    
1994

    
1995
### SIGNALS ###
1996
################
1997

    
1998
def create_astakos_user(u):
1999
    try:
2000
        AstakosUser.objects.get(user_ptr=u.pk)
2001
    except AstakosUser.DoesNotExist:
2002
        extended_user = AstakosUser(user_ptr_id=u.pk)
2003
        extended_user.__dict__.update(u.__dict__)
2004
        extended_user.save()
2005
        if not extended_user.has_auth_provider('local'):
2006
            extended_user.add_auth_provider('local')
2007
    except BaseException, e:
2008
        logger.exception(e)
2009

    
2010

    
2011
def fix_superusers():
2012
    # Associate superusers with AstakosUser
2013
    admins = User.objects.filter(is_superuser=True)
2014
    for u in admins:
2015
        create_astakos_user(u)
2016

    
2017

    
2018
def user_post_save(sender, instance, created, **kwargs):
2019
    if not created:
2020
        return
2021
    create_astakos_user(instance)
2022
post_save.connect(user_post_save, sender=User)
2023

    
2024

    
2025
def astakosuser_post_save(sender, instance, created, **kwargs):
2026
    pass
2027

    
2028
post_save.connect(astakosuser_post_save, sender=AstakosUser)
2029

    
2030

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

    
2034
post_save.connect(resource_post_save, sender=Resource)
2035

    
2036

    
2037
def renew_token(sender, instance, **kwargs):
2038
    if not instance.auth_token:
2039
        instance.renew_token()
2040
pre_save.connect(renew_token, sender=AstakosUser)
2041
pre_save.connect(renew_token, sender=Component)