Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 44104cd3

History | View | Annotate | Download (30 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 qh_sync_user, qh_sync_users
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

    
82
import astakos.im.messages as astakos_messages
83

    
84
logger = logging.getLogger(__name__)
85

    
86

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

    
96

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

    
102

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

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

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

    
130

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

    
136

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

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

    
160

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

    
166

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

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

    
189

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

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

    
218

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

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

    
243

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

    
262

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

    
282

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

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

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

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

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

    
329

    
330
class SendMailError(Exception):
331
    pass
332

    
333

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

    
339

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

    
345

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

    
351

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

    
357

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

    
363

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

    
369

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

    
375

    
376
def get_quota(users):
377
    pass
378

    
379

    
380
### PROJECT VIEWS ###
381

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

    
386
POLICIES = [ AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY ]
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
        raise IOError(
393
            _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % project_application_id)
394

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

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

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

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

    
426

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

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

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

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

    
453
def create_membership(project, user):
454
    if isinstance(user, (int, long)):
455
        user = get_user_by_id(user)
456

    
457
    if not user.is_active:
458
        m = _(astakos_messages.ACCOUNT_NOT_ACTIVE)
459
        raise PermissionDenied(m)
460

    
461
    m, created = ProjectMembership.objects.get_or_create(
462
        project=project,
463
        person=user)
464

    
465
    if created:
466
        return m
467
    else:
468
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
469
        raise PermissionDenied(msg)
470

    
471

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

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

    
491
    if request_user and \
492
        (not application.owner == request_user and \
493
            not request_user.is_superuser):
494
        raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
495

    
496
def checkAlive(project):
497
    if not project.is_alive:
498
        raise PermissionDenied(
499
            _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
500

    
501
def accept_membership_checks(project, request_user):
502
    checkAllowed(project, request_user)
503
    checkAlive(project)
504

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

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

    
512
def accept_membership(project_id, user, 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, user)
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)
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

    
528
    return membership
529

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

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

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

    
546
    membership_change_notify(project, membership.person, 'rejected')
547

    
548
    return membership
549

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

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

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

    
565
def remove_membership_checks(project, request_user=None):
566
    checkAllowed(project, request_user)
567
    checkAlive(project)
568

    
569
    leave_policy = project.application.member_leave_policy
570
    if leave_policy == CLOSED_POLICY:
571
        raise PermissionDenied(_(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED))
572

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

    
581
    membership.remove()
582
    qh_sync_user(user)
583
    logger.info("User %s has been removed from %s." %
584
                (membership.person.log_display, project))
585

    
586
    membership_change_notify(project, membership.person, 'removed')
587

    
588
    return membership
589

    
590
def enroll_member(project_id, user, request_user=None):
591
    project = get_project_for_update(project_id)
592
    accept_membership_checks(project, request_user)
593
    membership = create_membership(project, user)
594

    
595
    if not membership.can_accept():
596
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
597
        raise PermissionDenied(m)
598

    
599
    membership.accept()
600
    qh_sync_user(user)
601
    logger.info("User %s has been enrolled in %s." %
602
                (membership.person.log_display, project))
603

    
604
    membership_enroll_notify(project, membership.person)
605

    
606
    return membership
607

    
608
def leave_project_checks(project):
609
    checkAlive(project)
610

    
611
    leave_policy = project.application.member_leave_policy
612
    if leave_policy == CLOSED_POLICY:
613
        raise PermissionDenied(_(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED))
614

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

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

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

    
649
def join_project_checks(project):
650
    checkAlive(project)
651

    
652
    join_policy = project.application.member_join_policy
653
    if join_policy == CLOSED_POLICY:
654
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
655

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

    
665
def join_project(project_id, user_id):
666
    project = get_project_for_update(project_id)
667
    join_project_checks(project)
668
    membership = create_membership(project, user_id)
669

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

    
684
    return auto_accepted
685

    
686
def submit_application(kw, request_user=None):
687

    
688
    kw['applicant'] = request_user
689
    resource_policies = kw.pop('resource_policies', None)
690

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

    
698
        if (request_user and
699
            (not precursor.owner == request_user and
700
             not request_user.is_superuser
701
             and not request_user.is_project_admin())):
702
            m = _(astakos_messages.NOT_ALLOWED)
703
            raise PermissionDenied(m)
704

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

    
711
    application = ProjectApplication(**kw)
712

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

    
725
    application.save()
726
    application.resource_policies = resource_policies
727
    logger.info("User %s submitted %s." %
728
                (request_user.log_display, application.log_display))
729
    application_submit_notify(application)
730
    return application
731

    
732
def cancel_application(application_id, request_user=None):
733
    application = get_application_for_update(application_id)
734
    checkAllowed(application, request_user)
735

    
736
    if not application.can_cancel():
737
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL % (
738
                application.id, application.state_display()))
739
        raise PermissionDenied(m)
740

    
741
    application.cancel()
742
    logger.info("%s has been cancelled." % (application.log_display))
743

    
744
def dismiss_application(application_id, request_user=None):
745
    application = get_application_for_update(application_id)
746
    checkAllowed(application, request_user)
747

    
748
    if not application.can_dismiss():
749
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS % (
750
                application.id, application.state_display()))
751
        raise PermissionDenied(m)
752

    
753
    application.dismiss()
754
    logger.info("%s has been dismissed." % (application.log_display))
755

    
756
def deny_application(application_id, reason=None):
757
    application = get_application_for_update(application_id)
758

    
759
    if not application.can_deny():
760
        m = _(astakos_messages.APPLICATION_CANNOT_DENY % (
761
                application.id, application.state_display()))
762
        raise PermissionDenied(m)
763

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

    
771
def approve_application(app_id):
772

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

    
780
    if not application.can_approve():
781
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE % (
782
                application.id, application.state_display()))
783
        raise PermissionDenied(m)
784

    
785
    project = application.approve()
786
    qh_sync_projects([project])
787
    logger.info("%s has been approved." % (application.log_display))
788
    application_approve_notify(application)
789

    
790
def check_expiration(execute=False):
791
    objects = Project.objects
792
    expired = objects.expired_projects()
793
    if execute:
794
        for project in expired:
795
            terminate(project.id)
796

    
797
    return [project.expiration_info() for project in expired]
798

    
799
def terminate(project_id):
800
    project = get_project_for_update(project_id)
801
    checkAlive(project)
802

    
803
    project.terminate()
804
    qh_sync_projects([project])
805
    logger.info("%s has been terminated." % (project))
806

    
807
    project_termination_notify(project)
808

    
809
def suspend(project_id):
810
    project = get_project_by_id(project_id)
811
    checkAlive(project)
812

    
813
    project.suspend()
814
    qh_sync_projects([project])
815
    logger.info("%s has been suspended." % (project))
816

    
817
    project_suspension_notify(project)
818

    
819
def resume(project_id):
820
    project = get_project_for_update(project_id)
821

    
822
    if not project.is_suspended:
823
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
824
        raise PermissionDenied(m)
825

    
826
    project.resume()
827
    qh_sync_projects([project])
828
    logger.info("%s has been unsuspended." % (project))
829

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

    
842

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

    
851

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

    
861

    
862
def unset_user_setting(user_id, key):
863
    UserSetting.objects.filter(user=user_id, setting=key).delete()
864

    
865

    
866
PENDING_APPLICATION_LIMIT_SETTING = 'PENDING_APPLICATION_LIMIT'
867

    
868
def get_pending_application_limit(user_id):
869
    key = PENDING_APPLICATION_LIMIT_SETTING
870
    return get_user_setting(user_id, key)
871

    
872

    
873
def set_pending_application_limit(user_id, value):
874
    key = PENDING_APPLICATION_LIMIT_SETTING
875
    return set_user_setting(user_id, key, value)
876

    
877

    
878
def unset_pending_application_limit(user_id):
879
    key = PENDING_APPLICATION_LIMIT_SETTING
880
    return unset_user_setting(user_id, key)
881

    
882

    
883
def _reached_pending_application_limit(user_id):
884
    limit = get_pending_application_limit(user_id)
885

    
886
    PENDING = ProjectApplication.PENDING
887
    pending = ProjectApplication.objects.filter(
888
        owner__id=user_id, state=PENDING).count()
889

    
890
    return pending >= limit, limit
891

    
892

    
893
def reached_pending_application_limit(user_id, precursor=None):
894
    reached, limit = _reached_pending_application_limit(user_id)
895

    
896
    if precursor is None:
897
        return reached, limit
898

    
899
    chain = precursor.chain
900
    objs = ProjectApplication.objects
901
    q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
902
    has_pending = q.exists()
903

    
904
    if not has_pending:
905
        return reached, limit
906

    
907
    return False, limit
908

    
909

    
910
def qh_sync_projects(projects):
911

    
912
    memberships = ProjectMembership.objects.filter(project__in=projects)
913
    user_ids = set(m.person_id for m in memberships)
914

    
915
    qh_sync_users(user_ids)