Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ aa27f246

History | View | Annotate | Download (30.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 logging
35
import socket
36

    
37
from django.utils.translation import ugettext as _
38
from django.template.loader import render_to_string
39
from django.core.mail import send_mail, get_connection
40
from django.core.urlresolvers import reverse
41
from django.template import Context, loader
42
from django.contrib.auth import (
43
    login as auth_login,
44
    logout as auth_logout)
45
from django.conf import settings
46
from django.contrib.auth.models import AnonymousUser
47
from django.core.exceptions import PermissionDenied
48
from django.db import IntegrityError
49
from django.http import Http404
50

    
51
from urllib import quote
52
from urlparse import urljoin
53
from smtplib import SMTPException
54
from datetime import datetime
55
from functools import wraps
56

    
57
import astakos.im.settings as astakos_settings
58
from astakos.im.settings import (
59
    CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL,
60
    VERIFICATION_EMAIL_SUBJECT, ACCOUNT_CREATION_SUBJECT,
61
    GROUP_CREATION_SUBJECT, HELPDESK_NOTIFICATION_EMAIL_SUBJECT,
62
    INVITATION_EMAIL_SUBJECT, GREETING_EMAIL_SUBJECT, FEEDBACK_EMAIL_SUBJECT,
63
    EMAIL_CHANGE_EMAIL_SUBJECT,
64
    PROJECT_CREATION_SUBJECT, PROJECT_APPROVED_SUBJECT,
65
    PROJECT_TERMINATION_SUBJECT, PROJECT_SUSPENSION_SUBJECT,
66
    PROJECT_MEMBERSHIP_CHANGE_SUBJECT,
67
    PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES, HELPDESK,
68
    ADMINS, MANAGERS)
69
from astakos.im.notifications import build_notification, NotificationError
70
from astakos.im.models import (
71
    AstakosUser, Invitation, ProjectMembership, ProjectApplication, Project,
72
    UserSetting,
73
    get_resource_names, new_chain)
74
from astakos.im.quotas import users_quotas
75
from astakos.im.project_notif import (
76
    membership_change_notify, membership_enroll_notify,
77
    membership_request_notify, membership_leave_request_notify,
78
    application_submit_notify, application_approve_notify,
79
    application_deny_notify,
80
    project_termination_notify, project_suspension_notify)
81
from astakos.im.endpoints.qh import (
82
    register_quotas, qh_get_quota, qh_add_quota)
83

    
84
import astakos.im.messages as astakos_messages
85

    
86
logger = logging.getLogger(__name__)
87

    
88

    
89
def login(request, user):
90
    auth_login(request, user)
91
    from astakos.im.models import SessionCatalog
92
    SessionCatalog(
93
        session_key=request.session.session_key,
94
        user=user
95
    ).save()
96
    logger.info('%s logged in.', user.log_display)
97

    
98

    
99
def logout(request, *args, **kwargs):
100
    user = request.user
101
    auth_logout(request, *args, **kwargs)
102
    logger.info('%s logged out.', user.log_display)
103

    
104

    
105
def send_verification(user, template_name='im/activation_email.txt'):
106
    """
107
    Send email to user to verify his/her email and activate his/her account.
108

109
    Raises SendVerificationError
110
    """
111
    url = '%s?auth=%s&next=%s' % (urljoin(BASEURL, reverse('activate')),
112
                                  quote(user.auth_token),
113
                                  quote(urljoin(BASEURL, reverse('index'))))
114
    message = render_to_string(template_name, {
115
                               'user': user,
116
                               'url': url,
117
                               'baseurl': BASEURL,
118
                               'site_name': SITENAME,
119
                               'support': CONTACT_EMAIL})
120
    sender = settings.SERVER_EMAIL
121
    try:
122
        send_mail(_(VERIFICATION_EMAIL_SUBJECT), message, sender, [user.email],
123
                  connection=get_connection())
124

    
125
    except (SMTPException, socket.error) as e:
126
        logger.exception(e)
127
        raise SendVerificationError()
128
    else:
129
        msg = 'Sent activation %s' % user.email
130
        logger.log(LOGGING_LEVEL, msg)
131

    
132

    
133
def send_activation(user, template_name='im/activation_email.txt'):
134
    send_verification(user, template_name)
135
    user.activation_sent = datetime.now()
136
    user.save()
137

    
138

    
139
def _send_admin_notification(template_name,
140
                             dictionary=None,
141
                             subject='alpha2 testing notification',):
142
    """
143
    Send notification email to settings.HELPDESK + settings.MANAGERS.
144

145
    Raises SendNotificationError
146
    """
147
    dictionary = dictionary or {}
148
    message = render_to_string(template_name, dictionary)
149
    sender = settings.SERVER_EMAIL
150
    recipient_list = [e[1] for e in HELPDESK + MANAGERS]
151
    try:
152
        send_mail(subject, message, sender, recipient_list,
153
                  connection=get_connection())
154
    except (SMTPException, socket.error) as e:
155
        logger.exception(e)
156
        raise SendNotificationError()
157
    else:
158
        user = dictionary.get('user')
159
        msg = 'Sent admin notification for user %s' % user.log_display
160
        logger.log(LOGGING_LEVEL, msg)
161

    
162

    
163
def send_account_creation_notification(template_name, dictionary=None):
164
    user = dictionary.get('user')
165
    subject = _(ACCOUNT_CREATION_SUBJECT) % {'user': user.email}
166
    return _send_admin_notification(template_name, dictionary, subject=subject)
167

    
168

    
169
def send_helpdesk_notification(user, template_name='im/helpdesk_notification.txt'):
170
    """
171
    Send email to settings.HELPDESK list to notify for a new user activation.
172

173
    Raises SendNotificationError
174
    """
175
    message = render_to_string(
176
        template_name,
177
        {'user': user}
178
    )
179
    sender = settings.SERVER_EMAIL
180
    recipient_list = [e[1] for e in HELPDESK + MANAGERS]
181
    try:
182
        send_mail(_(HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % {'user': user.email},
183
                  message, sender, recipient_list, connection=get_connection())
184
    except (SMTPException, socket.error) as e:
185
        logger.exception(e)
186
        raise SendNotificationError()
187
    else:
188
        msg = 'Sent helpdesk admin notification for %s' % user.email
189
        logger.log(LOGGING_LEVEL, msg)
190

    
191

    
192
def send_invitation(invitation, template_name='im/invitation.txt'):
193
    """
194
    Send invitation email.
195

196
    Raises SendInvitationError
197
    """
198
    subject = _(INVITATION_EMAIL_SUBJECT)
199
    url = '%s?code=%d' % (urljoin(BASEURL, reverse('index')), invitation.code)
200
    message = render_to_string(template_name, {
201
                               'invitation': invitation,
202
                               'url': url,
203
                               'baseurl': BASEURL,
204
                               'site_name': SITENAME,
205
                               'support': CONTACT_EMAIL})
206
    sender = settings.SERVER_EMAIL
207
    try:
208
        send_mail(subject, message, sender, [invitation.username],
209
                  connection=get_connection())
210
    except (SMTPException, socket.error) as e:
211
        logger.exception(e)
212
        raise SendInvitationError()
213
    else:
214
        msg = 'Sent invitation %s' % invitation
215
        logger.log(LOGGING_LEVEL, msg)
216
        inviter_invitations = invitation.inviter.invitations
217
        invitation.inviter.invitations = max(0, inviter_invitations - 1)
218
        invitation.inviter.save()
219

    
220

    
221
def send_greeting(user, email_template_name='im/welcome_email.txt'):
222
    """
223
    Send welcome email.
224

225
    Raises SMTPException, socket.error
226
    """
227
    subject = _(GREETING_EMAIL_SUBJECT)
228
    message = render_to_string(email_template_name, {
229
                               'user': user,
230
                               'url': urljoin(BASEURL, reverse('index')),
231
                               'baseurl': BASEURL,
232
                               'site_name': SITENAME,
233
                               'support': CONTACT_EMAIL})
234
    sender = settings.SERVER_EMAIL
235
    try:
236
        send_mail(subject, message, sender, [user.email],
237
                  connection=get_connection())
238
    except (SMTPException, socket.error) as e:
239
        logger.exception(e)
240
        raise SendGreetingError()
241
    else:
242
        msg = 'Sent greeting %s' % user.log_display
243
        logger.log(LOGGING_LEVEL, msg)
244

    
245

    
246
def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'):
247
    subject = _(FEEDBACK_EMAIL_SUBJECT)
248
    from_email = settings.SERVER_EMAIL
249
    recipient_list = [e[1] for e in HELPDESK]
250
    content = render_to_string(email_template_name, {
251
        'message': msg,
252
        'data': data,
253
        'user': user})
254
    try:
255
        send_mail(subject, content, from_email, recipient_list,
256
                  connection=get_connection())
257
    except (SMTPException, socket.error) as e:
258
        logger.exception(e)
259
        raise SendFeedbackError()
260
    else:
261
        msg = 'Sent feedback from %s' % user.log_display
262
        logger.log(LOGGING_LEVEL, msg)
263

    
264

    
265
def send_change_email(
266
    ec, request, email_template_name='registration/email_change_email.txt'):
267
    try:
268
        url = ec.get_url()
269
        url = request.build_absolute_uri(url)
270
        t = loader.get_template(email_template_name)
271
        c = {'url': url, 'site_name': SITENAME,
272
             'support': CONTACT_EMAIL, 'ec': ec}
273
        from_email = settings.SERVER_EMAIL
274
        send_mail(_(EMAIL_CHANGE_EMAIL_SUBJECT), t.render(Context(c)),
275
                  from_email, [ec.new_email_address],
276
                  connection=get_connection())
277
    except (SMTPException, socket.error) as e:
278
        logger.exception(e)
279
        raise ChangeEmailError()
280
    else:
281
        msg = 'Sent change email for %s' % ec.user.log_display
282
        logger.log(LOGGING_LEVEL, msg)
283

    
284

    
285
def activate(
286
    user,
287
    email_template_name='im/welcome_email.txt',
288
    helpdesk_email_template_name='im/helpdesk_notification.txt',
289
    verify_email=False):
290
    """
291
    Activates the specific user and sends email.
292

293
    Raises SendGreetingError, ValidationError
294
    """
295
    user.is_active = True
296
    user.email_verified = True
297
    if not user.activation_sent:
298
        user.activation_sent = datetime.now()
299
    user.save()
300
    register_user_quotas(user)
301
    send_helpdesk_notification(user, helpdesk_email_template_name)
302
    send_greeting(user, email_template_name)
303

    
304
def deactivate(user):
305
    user.is_active = False
306
    user.save()
307

    
308
def invite(inviter, email, realname):
309
    inv = Invitation(inviter=inviter, username=email, realname=realname)
310
    inv.save()
311
    send_invitation(inv)
312
    inviter.invitations = max(0, inviter.invitations - 1)
313
    inviter.save()
314

    
315
def switch_account_to_shibboleth(user, local_user,
316
                                 greeting_template_name='im/welcome_email.txt'):
317
    try:
318
        provider = user.provider
319
    except AttributeError:
320
        return
321
    else:
322
        if not provider == 'shibboleth':
323
            return
324
        user.delete()
325
        local_user.provider = 'shibboleth'
326
        local_user.third_party_identifier = user.third_party_identifier
327
        local_user.save()
328
        send_greeting(local_user, greeting_template_name)
329
        return local_user
330

    
331

    
332
class SendMailError(Exception):
333
    pass
334

    
335

    
336
class SendAdminNotificationError(SendMailError):
337
    def __init__(self):
338
        self.message = _(astakos_messages.ADMIN_NOTIFICATION_SEND_ERR)
339
        super(SendAdminNotificationError, self).__init__()
340

    
341

    
342
class SendVerificationError(SendMailError):
343
    def __init__(self):
344
        self.message = _(astakos_messages.VERIFICATION_SEND_ERR)
345
        super(SendVerificationError, self).__init__()
346

    
347

    
348
class SendInvitationError(SendMailError):
349
    def __init__(self):
350
        self.message = _(astakos_messages.INVITATION_SEND_ERR)
351
        super(SendInvitationError, self).__init__()
352

    
353

    
354
class SendGreetingError(SendMailError):
355
    def __init__(self):
356
        self.message = _(astakos_messages.GREETING_SEND_ERR)
357
        super(SendGreetingError, self).__init__()
358

    
359

    
360
class SendFeedbackError(SendMailError):
361
    def __init__(self):
362
        self.message = _(astakos_messages.FEEDBACK_SEND_ERR)
363
        super(SendFeedbackError, self).__init__()
364

    
365

    
366
class ChangeEmailError(SendMailError):
367
    def __init__(self):
368
        self.message = _(astakos_messages.CHANGE_EMAIL_SEND_ERR)
369
        super(ChangeEmailError, self).__init__()
370

    
371

    
372
class SendNotificationError(SendMailError):
373
    def __init__(self):
374
        self.message = _(astakos_messages.NOTIFICATION_SEND_ERR)
375
        super(SendNotificationError, self).__init__()
376

    
377

    
378
def register_user_quotas(user):
379
    quotas = users_quotas([user])
380
    register_quotas(quotas)
381

    
382

    
383
def get_quota(users):
384
    resources = get_resource_names()
385
    return qh_get_quota(users, resources)
386

    
387

    
388
### PROJECT VIEWS ###
389

    
390
AUTO_ACCEPT_POLICY = 1
391
MODERATED_POLICY   = 2
392
CLOSED_POLICY      = 3
393

    
394
POLICIES = [ AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY ]
395

    
396
def get_project_by_application_id(project_application_id):
397
    try:
398
        return Project.objects.get(application__id=project_application_id)
399
    except Project.DoesNotExist:
400
        raise IOError(
401
            _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % project_application_id)
402

    
403
def get_related_project_id(application_id):
404
    try:
405
        app = ProjectApplication.objects.get(id=application_id)
406
        chain = app.chain
407
        project = Project.objects.get(id=chain)
408
        return chain
409
    except:
410
        return None
411

    
412
def get_chain_of_application_id(application_id):
413
    try:
414
        app = ProjectApplication.objects.get(id=application_id)
415
        chain = app.chain
416
        return chain.chain
417
    except:
418
        return None
419

    
420
def get_project_by_id(project_id):
421
    try:
422
        return Project.objects.get(id=project_id)
423
    except Project.DoesNotExist:
424
        raise IOError(
425
            _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id)
426

    
427
def get_project_by_name(name):
428
    try:
429
        return Project.objects.get(name=name)
430
    except Project.DoesNotExist:
431
        raise IOError(
432
            _(astakos_messages.UNKNOWN_PROJECT_ID) % name)
433

    
434

    
435
def get_project_for_update(project_id):
436
    try:
437
        return Project.objects.get_for_update(id=project_id)
438
    except Project.DoesNotExist:
439
        raise IOError(
440
            _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id)
441

    
442
def get_application_for_update(application_id):
443
    try:
444
        return ProjectApplication.objects.get_for_update(id=application_id)
445
    except ProjectApplication.DoesNotExist:
446
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % application_id
447
        raise IOError(m)
448

    
449
def get_user_by_id(user_id):
450
    try:
451
        return AstakosUser.objects.get(id=user_id)
452
    except AstakosUser.DoesNotExist:
453
        raise IOError(_(astakos_messages.UNKNOWN_USER_ID) % user_id)
454

    
455
def get_user_by_uuid(uuid):
456
    try:
457
        return AstakosUser.objects.get(uuid=uuid)
458
    except AstakosUser.DoesNotExist:
459
        raise IOError(_(astakos_messages.UNKNOWN_USER_ID) % uuid)
460

    
461
def create_membership(project, user):
462
    if isinstance(user, (int, long)):
463
        user = get_user_by_id(user)
464

    
465
    if not user.is_active:
466
        m = _(astakos_messages.ACCOUNT_NOT_ACTIVE)
467
        raise PermissionDenied(m)
468

    
469
    m, created = ProjectMembership.objects.get_or_create(
470
        project=project,
471
        person=user)
472

    
473
    if created:
474
        return m
475
    else:
476
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
477
        raise PermissionDenied(msg)
478

    
479

    
480
def get_membership_for_update(project, user):
481
    if isinstance(user, (int, long)):
482
        user = get_user_by_id(user)
483
    try:
484
        objs = ProjectMembership.objects
485
        m = objs.get_for_update(project=project, person=user)
486
        return m
487
    except ProjectMembership.DoesNotExist:
488
        raise IOError(_(astakos_messages.NOT_MEMBERSHIP_REQUEST))
489

    
490
def checkAllowed(entity, request_user):
491
    if isinstance(entity, Project):
492
        application = entity.application
493
    elif isinstance(entity, ProjectApplication):
494
        application = entity
495
    else:
496
        m = "%s not a Project nor a ProjectApplication" % (entity,)
497
        raise ValueError(m)
498

    
499
    if request_user and \
500
        (not application.owner == request_user and \
501
            not request_user.is_superuser):
502
        raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
503

    
504
def checkAlive(project):
505
    if not project.is_alive:
506
        raise PermissionDenied(
507
            _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
508

    
509
def accept_membership_checks(project, request_user):
510
    checkAllowed(project, request_user)
511
    checkAlive(project)
512

    
513
    join_policy = project.application.member_join_policy
514
    if join_policy == CLOSED_POLICY:
515
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
516

    
517
    if project.violates_members_limit(adding=1):
518
        raise PermissionDenied(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
519

    
520
def accept_membership(project_id, user, request_user=None):
521
    project = get_project_for_update(project_id)
522
    accept_membership_checks(project, request_user)
523

    
524
    membership = get_membership_for_update(project, user)
525
    if not membership.can_accept():
526
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
527
        raise PermissionDenied(m)
528

    
529
    membership.accept()
530
    qh_sync([membership])
531
    logger.info("User %s has been accepted in %s." %
532
                (membership.person.log_display, project))
533

    
534
    membership_change_notify(project, membership.person, 'accepted')
535

    
536
    return membership
537

    
538
def reject_membership_checks(project, request_user):
539
    checkAllowed(project, request_user)
540
    checkAlive(project)
541

    
542
def reject_membership(project_id, user, request_user=None):
543
    project = get_project_for_update(project_id)
544
    reject_membership_checks(project, request_user)
545
    membership = get_membership_for_update(project, user)
546
    if not membership.can_reject():
547
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
548
        raise PermissionDenied(m)
549

    
550
    membership.reject()
551
    logger.info("Request of user %s for %s has been rejected." %
552
                (membership.person.log_display, project))
553

    
554
    membership_change_notify(project, membership.person, 'rejected')
555

    
556
    return membership
557

    
558
def cancel_membership_checks(project):
559
    checkAlive(project)
560

    
561
def cancel_membership(project_id, user_id):
562
    project = get_project_for_update(project_id)
563
    cancel_membership_checks(project)
564
    membership = get_membership_for_update(project, user_id)
565
    if not membership.can_cancel():
566
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
567
        raise PermissionDenied(m)
568

    
569
    membership.cancel()
570
    logger.info("Request of user %s for %s has been cancelled." %
571
                (membership.person.log_display, project))
572

    
573
def remove_membership_checks(project, request_user=None):
574
    checkAllowed(project, request_user)
575
    checkAlive(project)
576

    
577
    leave_policy = project.application.member_leave_policy
578
    if leave_policy == CLOSED_POLICY:
579
        raise PermissionDenied(_(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED))
580

    
581
def remove_membership(project_id, user, request_user=None):
582
    project = get_project_for_update(project_id)
583
    remove_membership_checks(project, request_user)
584
    membership = get_membership_for_update(project, user)
585
    if not membership.can_remove():
586
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
587
        raise PermissionDenied(m)
588

    
589
    membership.remove()
590
    qh_sync([membership])
591
    logger.info("User %s has been removed from %s." %
592
                (membership.person.log_display, project))
593

    
594
    membership_change_notify(project, membership.person, 'removed')
595

    
596
    return membership
597

    
598
def enroll_member(project_id, user, request_user=None):
599
    project = get_project_for_update(project_id)
600
    accept_membership_checks(project, request_user)
601
    membership = create_membership(project, user)
602

    
603
    if not membership.can_accept():
604
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
605
        raise PermissionDenied(m)
606

    
607
    membership.accept()
608
    qh_sync([membership])
609
    logger.info("User %s has been enrolled in %s." %
610
                (membership.person.log_display, project))
611

    
612
    membership_enroll_notify(project, membership.person)
613

    
614
    return membership
615

    
616
def leave_project_checks(project):
617
    checkAlive(project)
618

    
619
    leave_policy = project.application.member_leave_policy
620
    if leave_policy == CLOSED_POLICY:
621
        raise PermissionDenied(_(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED))
622

    
623
def can_leave_request(project, user):
624
    leave_policy = project.application.member_leave_policy
625
    if leave_policy == CLOSED_POLICY:
626
        return False
627
    m = user.get_membership(project)
628
    if m is None:
629
        return False
630
    if m.state != ProjectMembership.ACCEPTED:
631
        return False
632
    return True
633

    
634
def leave_project(project_id, user_id):
635
    project = get_project_for_update(project_id)
636
    leave_project_checks(project)
637
    membership = get_membership_for_update(project, user_id)
638
    if not membership.can_leave():
639
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
640
        raise PermissionDenied(m)
641

    
642
    auto_accepted = False
643
    leave_policy = project.application.member_leave_policy
644
    if leave_policy == AUTO_ACCEPT_POLICY:
645
        membership.remove()
646
        qh_sync([membership])
647
        logger.info("User %s has left %s." %
648
                    (membership.person.log_display, project))
649
        auto_accepted = True
650
    else:
651
        membership.leave_request()
652
        logger.info("User %s requested to leave %s." %
653
                    (membership.person.log_display, project))
654
        membership_leave_request_notify(project, membership.person)
655
    return auto_accepted
656

    
657
def join_project_checks(project):
658
    checkAlive(project)
659

    
660
    join_policy = project.application.member_join_policy
661
    if join_policy == CLOSED_POLICY:
662
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
663

    
664
def can_join_request(project, user):
665
    join_policy = project.application.member_join_policy
666
    if join_policy == CLOSED_POLICY:
667
        return False
668
    m = user.get_membership(project)
669
    if m:
670
        return False
671
    return True
672

    
673
def join_project(project_id, user_id):
674
    project = get_project_for_update(project_id)
675
    join_project_checks(project)
676
    membership = create_membership(project, user_id)
677

    
678
    auto_accepted = False
679
    join_policy = project.application.member_join_policy
680
    if (join_policy == AUTO_ACCEPT_POLICY and
681
        not project.violates_members_limit(adding=1)):
682
        membership.accept()
683
        qh_sync([membership])
684
        logger.info("User %s joined %s." %
685
                    (membership.person.log_display, project))
686
        auto_accepted = True
687
    else:
688
        membership_request_notify(project, membership.person)
689
        logger.info("User %s requested to join %s." %
690
                    (membership.person.log_display, project))
691

    
692
    return auto_accepted
693

    
694
def submit_application(kw, request_user=None):
695

    
696
    kw['applicant'] = request_user
697
    resource_policies = kw.pop('resource_policies', None)
698

    
699
    precursor = None
700
    precursor_id = kw.get('precursor_application', None)
701
    if precursor_id is not None:
702
        objs = ProjectApplication.objects
703
        precursor = objs.get_for_update(id=precursor_id)
704
        kw['precursor_application'] = precursor
705

    
706
        if (request_user and
707
            (not precursor.owner == request_user and
708
             not request_user.is_superuser
709
             and not request_user.is_project_admin())):
710
            m = _(astakos_messages.NOT_ALLOWED)
711
            raise PermissionDenied(m)
712

    
713
    owner = kw['owner']
714
    reached, limit = reached_pending_application_limit(owner.id, precursor)
715
    if not request_user.is_project_admin() and reached:
716
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
717
        raise PermissionDenied(m)
718

    
719
    application = ProjectApplication(**kw)
720

    
721
    if precursor is None:
722
        application.chain = new_chain()
723
    else:
724
        chain = precursor.chain
725
        application.chain = chain
726
        objs = ProjectApplication.objects
727
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
728
        pending = q.select_for_update()
729
        for app in pending:
730
            app.state = ProjectApplication.REPLACED
731
            app.save()
732

    
733
    application.save()
734
    application.resource_policies = resource_policies
735
    logger.info("User %s submitted %s." %
736
                (request_user.log_display, application.log_display))
737
    application_submit_notify(application)
738
    return application
739

    
740
def cancel_application(application_id, request_user=None):
741
    application = get_application_for_update(application_id)
742
    checkAllowed(application, request_user)
743

    
744
    if not application.can_cancel():
745
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL % (
746
                application.id, application.state_display()))
747
        raise PermissionDenied(m)
748

    
749
    application.cancel()
750
    logger.info("%s has been cancelled." % (application.log_display))
751

    
752
def dismiss_application(application_id, request_user=None):
753
    application = get_application_for_update(application_id)
754
    checkAllowed(application, request_user)
755

    
756
    if not application.can_dismiss():
757
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS % (
758
                application.id, application.state_display()))
759
        raise PermissionDenied(m)
760

    
761
    application.dismiss()
762
    logger.info("%s has been dismissed." % (application.log_display))
763

    
764
def deny_application(application_id, reason=None):
765
    application = get_application_for_update(application_id)
766

    
767
    if not application.can_deny():
768
        m = _(astakos_messages.APPLICATION_CANNOT_DENY % (
769
                application.id, application.state_display()))
770
        raise PermissionDenied(m)
771

    
772
    if reason is None:
773
        reason = ""
774
    application.deny(reason)
775
    logger.info("%s has been denied with reason \"%s\"." %
776
                (application.log_display, reason))
777
    application_deny_notify(application)
778

    
779
def approve_application(app_id):
780

    
781
    try:
782
        objects = ProjectApplication.objects
783
        application = objects.get_for_update(id=app_id)
784
    except ProjectApplication.DoesNotExist:
785
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID % (app_id,))
786
        raise PermissionDenied(m)
787

    
788
    if not application.can_approve():
789
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE % (
790
                application.id, application.state_display()))
791
        raise PermissionDenied(m)
792

    
793
    project = application.approve()
794
    qh_sync_projects([project])
795
    logger.info("%s has been approved." % (application.log_display))
796
    application_approve_notify(application)
797

    
798
def check_expiration(execute=False):
799
    objects = Project.objects
800
    expired = objects.expired_projects()
801
    if execute:
802
        for project in expired:
803
            terminate(project.id)
804

    
805
    return [project.expiration_info() for project in expired]
806

    
807
def terminate(project_id):
808
    project = get_project_for_update(project_id)
809
    checkAlive(project)
810

    
811
    project.terminate()
812
    qh_sync_projects([project])
813
    logger.info("%s has been terminated." % (project))
814

    
815
    project_termination_notify(project)
816

    
817
def suspend(project_id):
818
    project = get_project_by_id(project_id)
819
    checkAlive(project)
820

    
821
    project.suspend()
822
    qh_sync_projects([project])
823
    logger.info("%s has been suspended." % (project))
824

    
825
    project_suspension_notify(project)
826

    
827
def resume(project_id):
828
    project = get_project_for_update(project_id)
829

    
830
    if not project.is_suspended:
831
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
832
        raise PermissionDenied(m)
833

    
834
    project.resume()
835
    qh_sync_projects([project])
836
    logger.info("%s has been unsuspended." % (project))
837

    
838
def get_by_chain_or_404(chain_id):
839
    try:
840
        project = Project.objects.get(id=chain_id)
841
        application = project.application
842
        return project, application
843
    except:
844
        application = ProjectApplication.objects.latest_of_chain(chain_id)
845
        if application is None:
846
            raise Http404
847
        else:
848
            return None, application
849

    
850

    
851
def get_user_setting(user_id, key):
852
    try:
853
        setting = UserSetting.objects.get(
854
            user=user_id, setting=key)
855
        return setting.value
856
    except UserSetting.DoesNotExist:
857
        return getattr(astakos_settings, key)
858

    
859

    
860
def set_user_setting(user_id, key, value):
861
    try:
862
        setting = UserSetting.objects.get_for_update(
863
            user=user_id, setting=key)
864
    except UserSetting.DoesNotExist:
865
        setting = UserSetting(user_id=user_id, setting=key)
866
    setting.value = value
867
    setting.save()
868

    
869

    
870
def unset_user_setting(user_id, key):
871
    UserSetting.objects.filter(user=user_id, setting=key).delete()
872

    
873

    
874
PENDING_APPLICATION_LIMIT_SETTING = 'PENDING_APPLICATION_LIMIT'
875

    
876
def get_pending_application_limit(user_id):
877
    key = PENDING_APPLICATION_LIMIT_SETTING
878
    return get_user_setting(user_id, key)
879

    
880

    
881
def set_pending_application_limit(user_id, value):
882
    key = PENDING_APPLICATION_LIMIT_SETTING
883
    return set_user_setting(user_id, key, value)
884

    
885

    
886
def unset_pending_application_limit(user_id):
887
    key = PENDING_APPLICATION_LIMIT_SETTING
888
    return unset_user_setting(user_id, key)
889

    
890

    
891
def _reached_pending_application_limit(user_id):
892
    limit = get_pending_application_limit(user_id)
893

    
894
    PENDING = ProjectApplication.PENDING
895
    pending = ProjectApplication.objects.filter(
896
        owner__id=user_id, state=PENDING).count()
897

    
898
    return pending >= limit, limit
899

    
900

    
901
def reached_pending_application_limit(user_id, precursor=None):
902
    reached, limit = _reached_pending_application_limit(user_id)
903

    
904
    if precursor is None:
905
        return reached, limit
906

    
907
    chain = precursor.chain
908
    objs = ProjectApplication.objects
909
    q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
910
    has_pending = q.exists()
911

    
912
    if not has_pending:
913
        return reached, limit
914

    
915
    return False, limit
916

    
917

    
918
def qh_sync_projects(projects):
919
    memberships = []
920
    append = memberships.append
921
    for project in projects:
922
        ms = project.projectmembership_set.all().select_for_update()
923
        memberships += list(ms)
924

    
925
    qh_sync(memberships)
926

    
927

    
928
def qh_sync(memberships):
929
    sub_quota, add_quota = [], []
930
    for membership in memberships:
931
        pending_application = membership.get_pending_application()
932
        membership.get_diff_quotas(sub_quota, add_quota, pending_application)
933
        if membership.state == membership.REMOVED:
934
            membership.delete()
935
        else:
936
            membership.application = pending_application
937
            membership.save()
938
    qh_add_quota(sub_quota, add_quota)