Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 3f5851eb

History | View | Annotate | Download (30.8 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.contrib.auth.models import AnonymousUser
46
from django.core.exceptions import PermissionDenied
47
from django.db import IntegrityError
48
from django.http import Http404
49

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

    
56
from astakos.im.settings import (
57
    CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL,
58
    VERIFICATION_EMAIL_SUBJECT, ACCOUNT_CREATION_SUBJECT,
59
    GROUP_CREATION_SUBJECT, HELPDESK_NOTIFICATION_EMAIL_SUBJECT,
60
    INVITATION_EMAIL_SUBJECT, GREETING_EMAIL_SUBJECT, FEEDBACK_EMAIL_SUBJECT,
61
    EMAIL_CHANGE_EMAIL_SUBJECT,
62
    PROJECT_CREATION_SUBJECT, PROJECT_APPROVED_SUBJECT,
63
    PROJECT_TERMINATION_SUBJECT, PROJECT_SUSPENSION_SUBJECT,
64
    PROJECT_MEMBERSHIP_CHANGE_SUBJECT,
65
    PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES)
66
from astakos.im.notifications import build_notification, NotificationError
67
from astakos.im.models import (
68
    AstakosUser, Invitation, ProjectMembership, ProjectApplication, Project,
69
    UserSetting,
70
    get_resource_names, new_chain)
71
from astakos.im.quotas import (qh_sync_user, qh_sync_users,
72
                               register_pending_apps, resolve_pending_serial)
73
from astakos.im.project_notif import (
74
    membership_change_notify, membership_enroll_notify,
75
    membership_request_notify, membership_leave_request_notify,
76
    application_submit_notify, application_approve_notify,
77
    application_deny_notify,
78
    project_termination_notify, project_suspension_notify)
79
from astakos.im import settings
80
import astakos.im.messages as astakos_messages
81
from astakos.quotaholder_app.exception import NoCapacityError
82

    
83
logger = logging.getLogger(__name__)
84

    
85

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

    
95

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

    
101

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

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

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

    
129

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

    
135

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

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

    
159

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

    
165

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

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

    
188

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

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

    
217

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

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

    
242

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

    
261

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

    
281

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

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

    
301
def deactivate(user):
302
    user.is_active = False
303
    user.save()
304

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

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

    
328

    
329
class SendMailError(Exception):
330
    pass
331

    
332

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

    
338

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

    
344

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

    
350

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

    
356

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

    
362

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

    
368

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

    
374

    
375
def get_quota(users):
376
    pass
377

    
378

    
379
### PROJECT FUNCTIONS ###
380

    
381
AUTO_ACCEPT_POLICY = 1
382
MODERATED_POLICY = 2
383
CLOSED_POLICY = 3
384

    
385
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
386

    
387

    
388
def get_project_by_application_id(project_application_id):
389
    try:
390
        return Project.objects.get(application__id=project_application_id)
391
    except Project.DoesNotExist:
392
        m = (_(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) %
393
             project_application_id)
394
        raise IOError(m)
395

    
396

    
397
def get_related_project_id(application_id):
398
    try:
399
        app = ProjectApplication.objects.get(id=application_id)
400
        chain = app.chain
401
        Project.objects.get(id=chain)
402
        return chain
403
    except ProjectApplication.DoesNotExist, Project.DoesNotExist:
404
        return None
405

    
406

    
407
def get_chain_of_application_id(application_id):
408
    try:
409
        app = ProjectApplication.objects.get(id=application_id)
410
        chain = app.chain
411
        return chain.chain
412
    except ProjectApplication.DoesNotExist:
413
        return None
414

    
415

    
416
def get_project_by_id(project_id):
417
    try:
418
        return Project.objects.get(id=project_id)
419
    except Project.DoesNotExist:
420
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
421
        raise IOError(m)
422

    
423

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

    
431

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

    
439

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

    
447

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

    
455

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

    
463

    
464
def get_membership_for_update(project_id, user_id):
465
    try:
466
        objs = ProjectMembership.objects
467
        return objs.get_for_update(project__id=project_id,
468
                                   person__id=user_id)
469
    except ProjectMembership.DoesNotExist:
470
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
471
        raise IOError(m)
472

    
473

    
474
def checkAllowed(entity, request_user, admin_only=False):
475
    if isinstance(entity, Project):
476
        application = entity.application
477
    elif isinstance(entity, ProjectApplication):
478
        application = entity
479
    else:
480
        m = "%s not a Project nor a ProjectApplication" % (entity,)
481
        raise ValueError(m)
482

    
483
    if not request_user or request_user.is_project_admin():
484
        return
485

    
486
    if not admin_only and application.owner == request_user:
487
        return
488

    
489
    m = _(astakos_messages.NOT_ALLOWED)
490
    raise PermissionDenied(m)
491

    
492

    
493
def checkAlive(project):
494
    if not project.is_alive:
495
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__
496
        raise PermissionDenied(m)
497

    
498

    
499
def accept_membership_checks(project, request_user):
500
    checkAllowed(project, request_user)
501
    checkAlive(project)
502

    
503
    join_policy = project.application.member_join_policy
504
    if join_policy == CLOSED_POLICY:
505
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
506
        raise PermissionDenied(m)
507

    
508
    if project.violates_members_limit(adding=1):
509
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
510
        raise PermissionDenied(m)
511

    
512

    
513
def accept_membership(project_id, user_id, request_user=None):
514
    project = get_project_for_update(project_id)
515
    accept_membership_checks(project, request_user)
516

    
517
    membership = get_membership_for_update(project_id, user_id)
518
    if not membership.can_accept():
519
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
520
        raise PermissionDenied(m)
521

    
522
    membership.accept()
523
    qh_sync_user(user_id)
524
    logger.info("User %s has been accepted in %s." %
525
                (membership.person.log_display, project))
526

    
527
    membership_change_notify(project, membership.person, 'accepted')
528
    return membership
529

    
530

    
531
def reject_membership_checks(project, request_user):
532
    checkAllowed(project, request_user)
533
    checkAlive(project)
534

    
535

    
536
def reject_membership(project_id, user_id, request_user=None):
537
    project = get_project_for_update(project_id)
538
    reject_membership_checks(project, request_user)
539
    membership = get_membership_for_update(project_id, user_id)
540
    if not membership.can_reject():
541
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
542
        raise PermissionDenied(m)
543

    
544
    membership.reject()
545
    logger.info("Request of user %s for %s has been rejected." %
546
                (membership.person.log_display, project))
547

    
548
    membership_change_notify(project, membership.person, 'rejected')
549
    return membership
550

    
551

    
552
def cancel_membership_checks(project):
553
    checkAlive(project)
554

    
555

    
556
def cancel_membership(project_id, request_user):
557
    project = get_project_for_update(project_id)
558
    cancel_membership_checks(project)
559
    membership = get_membership_for_update(project_id, request_user.id)
560
    if not membership.can_cancel():
561
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
562
        raise PermissionDenied(m)
563

    
564
    membership.cancel()
565
    logger.info("Request of user %s for %s has been cancelled." %
566
                (membership.person.log_display, project))
567

    
568

    
569
def remove_membership_checks(project, request_user=None):
570
    checkAllowed(project, request_user)
571
    checkAlive(project)
572

    
573
    leave_policy = project.application.member_leave_policy
574
    if leave_policy == CLOSED_POLICY:
575
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
576
        raise PermissionDenied(m)
577

    
578

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

    
587
    membership.remove()
588
    qh_sync_user(user_id)
589
    logger.info("User %s has been removed from %s." %
590
                (membership.person.log_display, project))
591

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

    
595

    
596
def enroll_member(project_id, user, request_user=None):
597
    project = get_project_for_update(project_id)
598
    accept_membership_checks(project, request_user)
599

    
600
    membership, created = ProjectMembership.objects.get_or_create(
601
        project=project,
602
        person=user)
603

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

    
608
    membership.accept()
609
    qh_sync_user(user.id)
610
    logger.info("User %s has been enrolled in %s." %
611
                (membership.person.log_display, project))
612

    
613
    membership_enroll_notify(project, membership.person)
614
    return membership
615

    
616

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

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

    
625

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

    
637

    
638
def leave_project(project_id, request_user):
639
    project = get_project_for_update(project_id)
640
    leave_project_checks(project)
641
    membership = get_membership_for_update(project_id, request_user.id)
642
    if not membership.can_leave():
643
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
644
        raise PermissionDenied(m)
645

    
646
    auto_accepted = False
647
    leave_policy = project.application.member_leave_policy
648
    if leave_policy == AUTO_ACCEPT_POLICY:
649
        membership.remove()
650
        qh_sync_user(request_user.id)
651
        logger.info("User %s has left %s." %
652
                    (membership.person.log_display, project))
653
        auto_accepted = True
654
    else:
655
        membership.leave_request()
656
        logger.info("User %s requested to leave %s." %
657
                    (membership.person.log_display, project))
658
        membership_leave_request_notify(project, membership.person)
659
    return auto_accepted
660

    
661

    
662
def join_project_checks(project):
663
    checkAlive(project)
664

    
665
    join_policy = project.application.member_join_policy
666
    if join_policy == CLOSED_POLICY:
667
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
668
        raise PermissionDenied(m)
669

    
670

    
671
def can_join_request(project, user):
672
    join_policy = project.application.member_join_policy
673
    if join_policy == CLOSED_POLICY:
674
        return False
675
    m = user.get_membership(project)
676
    if m:
677
        return False
678
    return True
679

    
680

    
681
def join_project(project_id, request_user):
682
    project = get_project_for_update(project_id)
683
    join_project_checks(project)
684

    
685
    membership, created = ProjectMembership.objects.get_or_create(
686
        project=project,
687
        person=request_user)
688

    
689
    if not created:
690
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
691
        raise PermissionDenied(msg)
692

    
693
    auto_accepted = False
694
    join_policy = project.application.member_join_policy
695
    if (join_policy == AUTO_ACCEPT_POLICY and (
696
            not project.violates_members_limit(adding=1))):
697
        membership.accept()
698
        qh_sync_user(request_user.id)
699
        logger.info("User %s joined %s." %
700
                    (membership.person.log_display, project))
701
        auto_accepted = True
702
    else:
703
        membership_request_notify(project, membership.person)
704
        logger.info("User %s requested to join %s." %
705
                    (membership.person.log_display, project))
706
    return auto_accepted
707

    
708

    
709
def submit_application(kw, request_user=None):
710

    
711
    kw['applicant'] = request_user
712
    resource_policies = kw.pop('resource_policies', None)
713

    
714
    precursor = None
715
    precursor_id = kw.get('precursor_application', None)
716
    if precursor_id is not None:
717
        objs = ProjectApplication.objects
718
        precursor = objs.get_for_update(id=precursor_id)
719
        kw['precursor_application'] = precursor
720

    
721
        if (request_user and
722
            (not precursor.owner == request_user and
723
             not request_user.is_superuser
724
             and not request_user.is_project_admin())):
725
            m = _(astakos_messages.NOT_ALLOWED)
726
            raise PermissionDenied(m)
727

    
728
    owner = kw['owner']
729
    force = request_user.is_project_admin()
730
    ok, limit = qh_add_pending_app(owner, precursor, force)
731
    if not ok:
732
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
733
        raise PermissionDenied(m)
734

    
735
    application = ProjectApplication(**kw)
736

    
737
    if precursor is None:
738
        application.chain = new_chain()
739
    else:
740
        chain = precursor.chain
741
        application.chain = chain
742
        objs = ProjectApplication.objects
743
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
744
        pending = q.select_for_update()
745
        for app in pending:
746
            app.state = ProjectApplication.REPLACED
747
            app.save()
748

    
749
    application.save()
750
    application.resource_policies = resource_policies
751
    logger.info("User %s submitted %s." %
752
                (request_user.log_display, application.log_display))
753
    application_submit_notify(application)
754
    return application
755

    
756

    
757
def cancel_application(application_id, request_user=None):
758
    application = get_application_for_update(application_id)
759
    checkAllowed(application, request_user)
760

    
761
    if not application.can_cancel():
762
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
763
              (application.id, application.state_display()))
764
        raise PermissionDenied(m)
765

    
766
    qh_release_pending_app(application.owner)
767

    
768
    application.cancel()
769
    logger.info("%s has been cancelled." % (application.log_display))
770

    
771

    
772
def dismiss_application(application_id, request_user=None):
773
    application = get_application_for_update(application_id)
774
    checkAllowed(application, request_user)
775

    
776
    if not application.can_dismiss():
777
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
778
              (application.id, application.state_display()))
779
        raise PermissionDenied(m)
780

    
781
    application.dismiss()
782
    logger.info("%s has been dismissed." % (application.log_display))
783

    
784

    
785
def deny_application(application_id, request_user=None, reason=None):
786
    application = get_application_for_update(application_id)
787

    
788
    checkAllowed(application, request_user, admin_only=True)
789

    
790
    if not application.can_deny():
791
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
792
              (application.id, application.state_display()))
793
        raise PermissionDenied(m)
794

    
795
    qh_release_pending_app(application.owner)
796

    
797
    if reason is None:
798
        reason = ""
799
    application.deny(reason)
800
    logger.info("%s has been denied with reason \"%s\"." %
801
                (application.log_display, reason))
802
    application_deny_notify(application)
803

    
804

    
805
def approve_application(app_id, request_user=None):
806

    
807
    try:
808
        objects = ProjectApplication.objects
809
        application = objects.get_for_update(id=app_id)
810
    except ProjectApplication.DoesNotExist:
811
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID % (app_id,))
812
        raise PermissionDenied(m)
813

    
814
    checkAllowed(application, request_user, admin_only=True)
815

    
816
    if not application.can_approve():
817
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
818
              (application.id, application.state_display()))
819
        raise PermissionDenied(m)
820

    
821
    qh_release_pending_app(application.owner)
822
    project = application.approve()
823
    qh_sync_projects([project])
824
    logger.info("%s has been approved." % (application.log_display))
825
    application_approve_notify(application)
826

    
827

    
828
def check_expiration(execute=False):
829
    objects = Project.objects
830
    expired = objects.expired_projects()
831
    if execute:
832
        for project in expired:
833
            terminate(project.id)
834

    
835
    return [project.expiration_info() for project in expired]
836

    
837

    
838
def terminate(project_id, request_user=None):
839
    project = get_project_for_update(project_id)
840
    checkAllowed(project, request_user, admin_only=True)
841
    checkAlive(project)
842

    
843
    project.terminate()
844
    qh_sync_projects([project])
845
    logger.info("%s has been terminated." % (project))
846

    
847
    project_termination_notify(project)
848

    
849

    
850
def suspend(project_id, request_user=None):
851
    project = get_project_by_id(project_id)
852
    checkAllowed(project, request_user, admin_only=True)
853
    checkAlive(project)
854

    
855
    project.suspend()
856
    qh_sync_projects([project])
857
    logger.info("%s has been suspended." % (project))
858

    
859
    project_suspension_notify(project)
860

    
861

    
862
def resume(project_id, request_user=None):
863
    project = get_project_for_update(project_id)
864
    checkAllowed(project, request_user, admin_only=True)
865

    
866
    if not project.is_suspended:
867
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
868
        raise PermissionDenied(m)
869

    
870
    project.resume()
871
    qh_sync_projects([project])
872
    logger.info("%s has been unsuspended." % (project))
873

    
874

    
875
def get_by_chain_or_404(chain_id):
876
    try:
877
        project = Project.objects.get(id=chain_id)
878
        application = project.application
879
        return project, application
880
    except:
881
        application = ProjectApplication.objects.latest_of_chain(chain_id)
882
        if application is None:
883
            raise Http404
884
        else:
885
            return None, application
886

    
887

    
888
def get_user_setting(user_id, key):
889
    try:
890
        setting = UserSetting.objects.get(
891
            user=user_id, setting=key)
892
        return setting.value
893
    except UserSetting.DoesNotExist:
894
        return getattr(settings, key)
895

    
896

    
897
def set_user_setting(user_id, key, value):
898
    try:
899
        setting = UserSetting.objects.get_for_update(
900
            user=user_id, setting=key)
901
    except UserSetting.DoesNotExist:
902
        setting = UserSetting(user_id=user_id, setting=key)
903
    setting.value = value
904
    setting.save()
905

    
906

    
907
def unset_user_setting(user_id, key):
908
    UserSetting.objects.filter(user=user_id, setting=key).delete()
909

    
910

    
911
def _partition_by(f, l):
912
    d = {}
913
    for x in l:
914
        group = f(x)
915
        group_l = d.get(group, [])
916
        group_l.append(x)
917
        d[group] = group_l
918
    return d
919

    
920

    
921
def count_pending_app(users):
922
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
923
                                             owner__in=users)
924
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
925

    
926
    usage = {}
927
    for user in users:
928
        uuid = user.uuid
929
        usage[uuid] = len(apps_d.get(uuid, []))
930
    return usage
931

    
932

    
933
def qh_add_pending_app(user, precursor=None, force=False, dry_run=False):
934
    if precursor is None:
935
        diff = 1
936
    else:
937
        chain = precursor.chain
938
        objs = ProjectApplication.objects
939
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
940
        count = q.count()
941
        diff = 1 - count
942

    
943
    try:
944
        name = "DRYRUN" if dry_run else ""
945
        serial = register_pending_apps(user, diff, force, name=name)
946
    except NoCapacityError as e:
947
        limit = e.data['limit']
948
        return False, limit
949
    else:
950
        accept = not dry_run
951
        resolve_pending_serial(serial, accept=accept)
952
        return True, None
953

    
954

    
955
def qh_release_pending_app(user):
956
    serial = register_pending_apps(user, -1)
957
    resolve_pending_serial(serial)
958

    
959

    
960
def qh_sync_projects(projects):
961

    
962
    memberships = ProjectMembership.objects.filter(project__in=projects)
963
    user_ids = set(m.person_id for m in memberships)
964

    
965
    qh_sync_users(user_ids)