Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (30.3 kB)

1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import socket
36

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

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

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

    
84
import astakos.im.messages as astakos_messages
85

    
86
logger = logging.getLogger(__name__)
87

    
88

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

    
98

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

    
104

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

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

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

    
132

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

    
138

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

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

    
162

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

    
168

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

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

    
191

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

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

    
220

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

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

    
245

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

    
264

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

    
284

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

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

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

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

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

    
331

    
332
class SendMailError(Exception):
333
    pass
334

    
335

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

    
341

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

    
347

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

    
353

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

    
359

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

    
365

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

    
371

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

    
377

    
378
def get_quota(users):
379
    resources = get_resource_names()
380
    return qh_get_quota(users, resources)
381

    
382

    
383
### PROJECT VIEWS ###
384

    
385
AUTO_ACCEPT_POLICY = 1
386
MODERATED_POLICY   = 2
387
CLOSED_POLICY      = 3
388

    
389
POLICIES = [ AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY ]
390

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

    
398
def get_related_project_id(application_id):
399
    try:
400
        app = ProjectApplication.objects.get(id=application_id)
401
        chain = app.chain
402
        project = Project.objects.get(id=chain)
403
        return chain
404
    except:
405
        return None
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:
413
        return None
414

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

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

    
429

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

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

    
444
def get_user_by_id(user_id):
445
    try:
446
        return AstakosUser.objects.get(id=user_id)
447
    except AstakosUser.DoesNotExist:
448
        raise IOError(_(astakos_messages.UNKNOWN_USER_ID) % user_id)
449

    
450
def get_user_by_uuid(uuid):
451
    try:
452
        return AstakosUser.objects.get(uuid=uuid)
453
    except AstakosUser.DoesNotExist:
454
        raise IOError(_(astakos_messages.UNKNOWN_USER_ID) % uuid)
455

    
456
def create_membership(project, user):
457
    if isinstance(user, (int, long)):
458
        user = get_user_by_id(user)
459

    
460
    if not user.is_active:
461
        m = _(astakos_messages.ACCOUNT_NOT_ACTIVE)
462
        raise PermissionDenied(m)
463

    
464
    m, created = ProjectMembership.objects.get_or_create(
465
        project=project,
466
        person=user)
467

    
468
    if created:
469
        return m
470
    else:
471
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
472
        raise PermissionDenied(msg)
473

    
474

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

    
485
def checkAllowed(entity, request_user):
486
    if isinstance(entity, Project):
487
        application = entity.application
488
    elif isinstance(entity, ProjectApplication):
489
        application = entity
490
    else:
491
        m = "%s not a Project nor a ProjectApplication" % (entity,)
492
        raise ValueError(m)
493

    
494
    if request_user and \
495
        (not application.owner == request_user and \
496
            not request_user.is_superuser):
497
        raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
498

    
499
def checkAlive(project):
500
    if not project.is_alive:
501
        raise PermissionDenied(
502
            _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
503

    
504
def accept_membership_checks(project, request_user):
505
    checkAllowed(project, request_user)
506
    checkAlive(project)
507

    
508
    join_policy = project.application.member_join_policy
509
    if join_policy == CLOSED_POLICY:
510
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
511

    
512
    if project.violates_members_limit(adding=1):
513
        raise PermissionDenied(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
514

    
515
def accept_membership(project_id, user, request_user=None):
516
    project = get_project_for_update(project_id)
517
    accept_membership_checks(project, request_user)
518

    
519
    membership = get_membership_for_update(project, user)
520
    if not membership.can_accept():
521
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
522
        raise PermissionDenied(m)
523

    
524
    membership.accept()
525
    qh_sync([user])
526
    logger.info("User %s has been accepted in %s." %
527
                (membership.person.log_display, project))
528

    
529
    membership_change_notify(project, membership.person, 'accepted')
530

    
531
    return membership
532

    
533
def reject_membership_checks(project, request_user):
534
    checkAllowed(project, request_user)
535
    checkAlive(project)
536

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

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

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

    
551
    return membership
552

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

    
556
def cancel_membership(project_id, user_id):
557
    project = get_project_for_update(project_id)
558
    cancel_membership_checks(project)
559
    membership = get_membership_for_update(project, 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
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
        raise PermissionDenied(_(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED))
575

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

    
584
    membership.remove()
585
    qh_sync([user])
586
    logger.info("User %s has been removed from %s." %
587
                (membership.person.log_display, project))
588

    
589
    membership_change_notify(project, membership.person, 'removed')
590

    
591
    return membership
592

    
593
def enroll_member(project_id, user, request_user=None):
594
    project = get_project_for_update(project_id)
595
    accept_membership_checks(project, request_user)
596
    membership = create_membership(project, user)
597

    
598
    if not membership.can_accept():
599
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
600
        raise PermissionDenied(m)
601

    
602
    membership.accept()
603
    qh_sync([user])
604
    logger.info("User %s has been enrolled in %s." %
605
                (membership.person.log_display, project))
606

    
607
    membership_enroll_notify(project, membership.person)
608

    
609
    return membership
610

    
611
def leave_project_checks(project):
612
    checkAlive(project)
613

    
614
    leave_policy = project.application.member_leave_policy
615
    if leave_policy == CLOSED_POLICY:
616
        raise PermissionDenied(_(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED))
617

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

    
629
def leave_project(project_id, user_id):
630
    project = get_project_for_update(project_id)
631
    leave_project_checks(project)
632
    membership = get_membership_for_update(project, user_id)
633
    if not membership.can_leave():
634
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
635
        raise PermissionDenied(m)
636

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

    
652
def join_project_checks(project):
653
    checkAlive(project)
654

    
655
    join_policy = project.application.member_join_policy
656
    if join_policy == CLOSED_POLICY:
657
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
658

    
659
def can_join_request(project, user):
660
    join_policy = project.application.member_join_policy
661
    if join_policy == CLOSED_POLICY:
662
        return False
663
    m = user.get_membership(project)
664
    if m:
665
        return False
666
    return True
667

    
668
def join_project(project_id, user_id):
669
    project = get_project_for_update(project_id)
670
    join_project_checks(project)
671
    membership = create_membership(project, user_id)
672

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

    
687
    return auto_accepted
688

    
689
def submit_application(kw, request_user=None):
690

    
691
    kw['applicant'] = request_user
692
    resource_policies = kw.pop('resource_policies', None)
693

    
694
    precursor = None
695
    precursor_id = kw.get('precursor_application', None)
696
    if precursor_id is not None:
697
        objs = ProjectApplication.objects
698
        precursor = objs.get_for_update(id=precursor_id)
699
        kw['precursor_application'] = precursor
700

    
701
        if (request_user and
702
            (not precursor.owner == request_user and
703
             not request_user.is_superuser
704
             and not request_user.is_project_admin())):
705
            m = _(astakos_messages.NOT_ALLOWED)
706
            raise PermissionDenied(m)
707

    
708
    owner = kw['owner']
709
    reached, limit = reached_pending_application_limit(owner.id, precursor)
710
    if not request_user.is_project_admin() and reached:
711
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
712
        raise PermissionDenied(m)
713

    
714
    application = ProjectApplication(**kw)
715

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

    
728
    application.save()
729
    application.resource_policies = resource_policies
730
    logger.info("User %s submitted %s." %
731
                (request_user.log_display, application.log_display))
732
    application_submit_notify(application)
733
    return application
734

    
735
def cancel_application(application_id, request_user=None):
736
    application = get_application_for_update(application_id)
737
    checkAllowed(application, request_user)
738

    
739
    if not application.can_cancel():
740
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL % (
741
                application.id, application.state_display()))
742
        raise PermissionDenied(m)
743

    
744
    application.cancel()
745
    logger.info("%s has been cancelled." % (application.log_display))
746

    
747
def dismiss_application(application_id, request_user=None):
748
    application = get_application_for_update(application_id)
749
    checkAllowed(application, request_user)
750

    
751
    if not application.can_dismiss():
752
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS % (
753
                application.id, application.state_display()))
754
        raise PermissionDenied(m)
755

    
756
    application.dismiss()
757
    logger.info("%s has been dismissed." % (application.log_display))
758

    
759
def deny_application(application_id, reason=None):
760
    application = get_application_for_update(application_id)
761

    
762
    if not application.can_deny():
763
        m = _(astakos_messages.APPLICATION_CANNOT_DENY % (
764
                application.id, application.state_display()))
765
        raise PermissionDenied(m)
766

    
767
    if reason is None:
768
        reason = ""
769
    application.deny(reason)
770
    logger.info("%s has been denied with reason \"%s\"." %
771
                (application.log_display, reason))
772
    application_deny_notify(application)
773

    
774
def approve_application(app_id):
775

    
776
    try:
777
        objects = ProjectApplication.objects
778
        application = objects.get_for_update(id=app_id)
779
    except ProjectApplication.DoesNotExist:
780
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID % (app_id,))
781
        raise PermissionDenied(m)
782

    
783
    if not application.can_approve():
784
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE % (
785
                application.id, application.state_display()))
786
        raise PermissionDenied(m)
787

    
788
    project = application.approve()
789
    qh_sync_projects([project])
790
    logger.info("%s has been approved." % (application.log_display))
791
    application_approve_notify(application)
792

    
793
def check_expiration(execute=False):
794
    objects = Project.objects
795
    expired = objects.expired_projects()
796
    if execute:
797
        for project in expired:
798
            terminate(project.id)
799

    
800
    return [project.expiration_info() for project in expired]
801

    
802
def terminate(project_id):
803
    project = get_project_for_update(project_id)
804
    checkAlive(project)
805

    
806
    project.terminate()
807
    qh_sync_projects([project])
808
    logger.info("%s has been terminated." % (project))
809

    
810
    project_termination_notify(project)
811

    
812
def suspend(project_id):
813
    project = get_project_by_id(project_id)
814
    checkAlive(project)
815

    
816
    project.suspend()
817
    qh_sync_projects([project])
818
    logger.info("%s has been suspended." % (project))
819

    
820
    project_suspension_notify(project)
821

    
822
def resume(project_id):
823
    project = get_project_for_update(project_id)
824

    
825
    if not project.is_suspended:
826
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
827
        raise PermissionDenied(m)
828

    
829
    project.resume()
830
    qh_sync_projects([project])
831
    logger.info("%s has been unsuspended." % (project))
832

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

    
845

    
846
def get_user_setting(user_id, key):
847
    try:
848
        setting = UserSetting.objects.get(
849
            user=user_id, setting=key)
850
        return setting.value
851
    except UserSetting.DoesNotExist:
852
        return getattr(astakos_settings, key)
853

    
854

    
855
def set_user_setting(user_id, key, value):
856
    try:
857
        setting = UserSetting.objects.get_for_update(
858
            user=user_id, setting=key)
859
    except UserSetting.DoesNotExist:
860
        setting = UserSetting(user_id=user_id, setting=key)
861
    setting.value = value
862
    setting.save()
863

    
864

    
865
def unset_user_setting(user_id, key):
866
    UserSetting.objects.filter(user=user_id, setting=key).delete()
867

    
868

    
869
PENDING_APPLICATION_LIMIT_SETTING = 'PENDING_APPLICATION_LIMIT'
870

    
871
def get_pending_application_limit(user_id):
872
    key = PENDING_APPLICATION_LIMIT_SETTING
873
    return get_user_setting(user_id, key)
874

    
875

    
876
def set_pending_application_limit(user_id, value):
877
    key = PENDING_APPLICATION_LIMIT_SETTING
878
    return set_user_setting(user_id, key, value)
879

    
880

    
881
def unset_pending_application_limit(user_id):
882
    key = PENDING_APPLICATION_LIMIT_SETTING
883
    return unset_user_setting(user_id, key)
884

    
885

    
886
def _reached_pending_application_limit(user_id):
887
    limit = get_pending_application_limit(user_id)
888

    
889
    PENDING = ProjectApplication.PENDING
890
    pending = ProjectApplication.objects.filter(
891
        owner__id=user_id, state=PENDING).count()
892

    
893
    return pending >= limit, limit
894

    
895

    
896
def reached_pending_application_limit(user_id, precursor=None):
897
    reached, limit = _reached_pending_application_limit(user_id)
898

    
899
    if precursor is None:
900
        return reached, limit
901

    
902
    chain = precursor.chain
903
    objs = ProjectApplication.objects
904
    q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
905
    has_pending = q.exists()
906

    
907
    if not has_pending:
908
        return reached, limit
909

    
910
    return False, limit
911

    
912

    
913
def qh_sync_projects(projects):
914

    
915
    memberships = ProjectMembership.objects.filter(project__in=projects)
916
    user_ids = set(m.person_id for m in memberships)
917

    
918
    qh_sync(user_ids)
919

    
920

    
921
def qh_sync(user_ids):
922
    users = AstakosUser.forupdate.filter(id__in=user_ids).select_for_update()
923
    astakos_quotas = users_quotas(list(users))
924
    set_user_quota(astakos_quotas)