Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 570015d2

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, users_quotas)
74
from astakos.im.project_notif import (
75
    membership_change_notify, membership_enroll_notify,
76
    membership_request_notify, membership_leave_request_notify,
77
    application_submit_notify, application_approve_notify,
78
    application_deny_notify,
79
    project_termination_notify, project_suspension_notify)
80
from astakos.im.endpoints.qh import (
81
    register_quotas, qh_get_quota, qh_add_quota)
82

    
83
import astakos.im.messages as astakos_messages
84

    
85
logger = logging.getLogger(__name__)
86

    
87

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

    
97

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

    
103

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

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

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

    
131

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

    
137

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

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

    
161

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

    
167

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

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

    
190

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

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

    
219

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

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

    
244

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

    
263

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

    
283

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

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

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

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

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

    
330

    
331
class SendMailError(Exception):
332
    pass
333

    
334

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

    
340

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

    
346

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

    
352

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

    
358

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

    
364

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

    
370

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

    
376

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

    
381

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

    
386

    
387
### PROJECT VIEWS ###
388

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

    
393
POLICIES = [ AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY ]
394

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

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

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

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

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

    
433

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

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

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

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

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

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

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

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

    
478

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

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

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

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

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

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

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

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

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

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

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

    
535
    return membership
536

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

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

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

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

    
555
    return membership
556

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

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

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

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

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

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

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

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

    
595
    return membership
596

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

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

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

    
611
    membership_enroll_notify(project, membership.person)
612

    
613
    return membership
614

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

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

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

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

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

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

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

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

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

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

    
691
    return auto_accepted
692

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

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

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

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

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

    
718
    application = ProjectApplication(**kw)
719

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

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

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

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

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

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

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

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

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

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

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

    
778
def approve_application(app_id):
779

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

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

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

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

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

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

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

    
814
    project_termination_notify(project)
815

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

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

    
824
    project_suspension_notify(project)
825

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

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

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

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

    
849

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

    
858

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

    
868

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

    
872

    
873
PENDING_APPLICATION_LIMIT_SETTING = 'PENDING_APPLICATION_LIMIT'
874

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

    
879

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

    
884

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

    
889

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

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

    
897
    return pending >= limit, limit
898

    
899

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

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

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

    
911
    if not has_pending:
912
        return reached, limit
913

    
914
    return False, limit
915

    
916

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

    
924
    qh_sync(memberships)
925

    
926

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