Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (30.4 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)
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

    
82
logger = logging.getLogger(__name__)
83

    
84

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

    
94

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

    
100

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

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

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

    
128

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

    
134

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

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

    
158

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

    
164

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

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

    
187

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

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

    
216

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

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

    
241

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

    
260

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

    
280

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

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

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

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

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

    
327

    
328
class SendMailError(Exception):
329
    pass
330

    
331

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

    
337

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

    
343

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

    
349

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

    
355

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

    
361

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

    
367

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

    
373

    
374
def get_quota(users):
375
    pass
376

    
377

    
378
### PROJECT FUNCTIONS ###
379

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

    
384
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
385

    
386

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

    
395

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

    
405

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

    
414

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

    
422

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

    
430

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

    
438

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

    
446

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

    
454

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

    
462

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

    
472

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

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

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

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

    
491

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

    
497

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

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

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

    
511

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

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

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

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

    
529

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

    
534

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

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

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

    
550

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

    
554

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

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

    
567

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

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

    
577

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

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

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

    
594

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

    
599
    membership, created = ProjectMembership.objects.get_or_create(
600
        project=project,
601
        person=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_user(user.id)
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
    return membership
614

    
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
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
622
        raise PermissionDenied(m)
623

    
624

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

    
636

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

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

    
660

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

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

    
669

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

    
679

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

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

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

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

    
707

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

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

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

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

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

    
734
    application = ProjectApplication(**kw)
735

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

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

    
755

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

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

    
765
    qh_release_pending_app(application.owner)
766

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

    
770

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

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

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

    
783

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

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

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

    
794
    qh_release_pending_app(application.owner)
795

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

    
803

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

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

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

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

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

    
826

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

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

    
836

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

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

    
846
    project_termination_notify(project)
847

    
848

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

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

    
858
    project_suspension_notify(project)
859

    
860

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

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

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

    
873

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

    
886

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

    
895

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

    
905

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

    
909

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

    
919

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

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

    
931

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

    
942
    return register_pending_apps(user, diff, force, dry_run)
943

    
944

    
945
def qh_release_pending_app(user):
946
    register_pending_apps(user, -1)
947

    
948

    
949
def qh_sync_projects(projects):
950

    
951
    memberships = ProjectMembership.objects.filter(project__in=projects)
952
    user_ids = set(m.person_id for m in memberships)
953

    
954
    qh_sync_users(user_ids)