Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 5dcf6618

History | View | Annotate | Download (31.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
### PROJECT FUNCTIONS ###
375

    
376
AUTO_ACCEPT_POLICY = 1
377
MODERATED_POLICY = 2
378
CLOSED_POLICY = 3
379

    
380
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
381

    
382

    
383
def get_project_by_application_id(project_application_id):
384
    try:
385
        return Project.objects.get(application__id=project_application_id)
386
    except Project.DoesNotExist:
387
        m = (_(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) %
388
             project_application_id)
389
        raise IOError(m)
390

    
391

    
392
def get_related_project_id(application_id):
393
    try:
394
        app = ProjectApplication.objects.get(id=application_id)
395
        chain = app.chain
396
        Project.objects.get(id=chain)
397
        return chain
398
    except (ProjectApplication.DoesNotExist, Project.DoesNotExist):
399
        return None
400

    
401

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

    
410

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

    
418

    
419
def get_project_by_name(name):
420
    try:
421
        return Project.objects.get(name=name)
422
    except Project.DoesNotExist:
423
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % name
424
        raise IOError(m)
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
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
432
        raise IOError(m)
433

    
434

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

    
442

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

    
450

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

    
458

    
459
def get_membership_for_update(project_id, user_id):
460
    try:
461
        objs = ProjectMembership.objects
462
        return objs.get_for_update(project__id=project_id,
463
                                   person__id=user_id)
464
    except ProjectMembership.DoesNotExist:
465
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
466
        raise IOError(m)
467

    
468

    
469
def get_membership_for_update_by_id(project_id, memb_id):
470
    try:
471
        objs = ProjectMembership.objects
472
        return objs.get_for_update(project__id=project_id,
473
                                   id=memb_id)
474
    except ProjectMembership.DoesNotExist:
475
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
476
        raise IOError(m)
477

    
478

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

    
488
    if not request_user or request_user.is_project_admin():
489
        return
490

    
491
    if not admin_only and application.owner == request_user:
492
        return
493

    
494
    m = _(astakos_messages.NOT_ALLOWED)
495
    raise PermissionDenied(m)
496

    
497

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

    
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
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
511
        raise PermissionDenied(m)
512

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

    
517

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

    
522
    membership = get_membership_for_update_by_id(project_id, memb_id)
523
    if not membership.can_accept():
524
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
525
        raise PermissionDenied(m)
526

    
527
    user = membership.person
528
    membership.accept()
529
    qh_sync_user(user.id)
530
    logger.info("User %s has been accepted in %s." %
531
                (user.log_display, project))
532

    
533
    membership_change_notify(project, user, 'accepted')
534
    return membership
535

    
536

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

    
541

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

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

    
555
    membership_change_notify(project, user, 'rejected')
556
    return membership
557

    
558

    
559
def cancel_membership_checks(project):
560
    checkAlive(project)
561

    
562

    
563
def cancel_membership(project_id, request_user):
564
    project = get_project_for_update(project_id)
565
    cancel_membership_checks(project)
566
    membership = get_membership_for_update(project_id, request_user.id)
567
    if not membership.can_cancel():
568
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
569
        raise PermissionDenied(m)
570

    
571
    membership.cancel()
572
    logger.info("Request of user %s for %s has been cancelled." %
573
                (membership.person.log_display, project))
574

    
575

    
576
def remove_membership_checks(project, request_user=None):
577
    checkAllowed(project, request_user)
578
    checkAlive(project)
579

    
580
    leave_policy = project.application.member_leave_policy
581
    if leave_policy == CLOSED_POLICY:
582
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
583
        raise PermissionDenied(m)
584

    
585

    
586
def remove_membership(project_id, memb_id, request_user=None):
587
    project = get_project_for_update(project_id)
588
    remove_membership_checks(project, request_user)
589
    membership = get_membership_for_update_by_id(project_id, memb_id)
590
    if not membership.can_remove():
591
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
592
        raise PermissionDenied(m)
593

    
594
    user = membership.person
595
    membership.remove()
596
    qh_sync_user(user.id)
597
    logger.info("User %s has been removed from %s." %
598
                (user.log_display, project))
599

    
600
    membership_change_notify(project, user, 'removed')
601
    return membership
602

    
603

    
604
def enroll_member(project_id, user, request_user=None):
605
    project = get_project_for_update(project_id)
606
    accept_membership_checks(project, request_user)
607

    
608
    membership, created = ProjectMembership.objects.get_or_create(
609
        project=project,
610
        person=user)
611

    
612
    if not membership.can_accept():
613
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
614
        raise PermissionDenied(m)
615

    
616
    membership.accept()
617
    qh_sync_user(user.id)
618
    logger.info("User %s has been enrolled in %s." %
619
                (membership.person.log_display, project))
620

    
621
    membership_enroll_notify(project, membership.person)
622
    return membership
623

    
624

    
625
def leave_project_checks(project):
626
    checkAlive(project)
627

    
628
    leave_policy = project.application.member_leave_policy
629
    if leave_policy == CLOSED_POLICY:
630
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
631
        raise PermissionDenied(m)
632

    
633

    
634
def can_leave_request(project, user):
635
    leave_policy = project.application.member_leave_policy
636
    if leave_policy == CLOSED_POLICY:
637
        return False
638
    m = user.get_membership(project)
639
    if m is None:
640
        return False
641
    if m.state != ProjectMembership.ACCEPTED:
642
        return False
643
    return True
644

    
645

    
646
def leave_project(project_id, request_user):
647
    project = get_project_for_update(project_id)
648
    leave_project_checks(project)
649
    membership = get_membership_for_update(project_id, request_user.id)
650
    if not membership.can_leave():
651
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
652
        raise PermissionDenied(m)
653

    
654
    auto_accepted = False
655
    leave_policy = project.application.member_leave_policy
656
    if leave_policy == AUTO_ACCEPT_POLICY:
657
        membership.remove()
658
        qh_sync_user(request_user.id)
659
        logger.info("User %s has left %s." %
660
                    (membership.person.log_display, project))
661
        auto_accepted = True
662
    else:
663
        membership.leave_request()
664
        logger.info("User %s requested to leave %s." %
665
                    (membership.person.log_display, project))
666
        membership_leave_request_notify(project, membership.person)
667
    return auto_accepted
668

    
669

    
670
def join_project_checks(project):
671
    checkAlive(project)
672

    
673
    join_policy = project.application.member_join_policy
674
    if join_policy == CLOSED_POLICY:
675
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
676
        raise PermissionDenied(m)
677

    
678

    
679
def can_join_request(project, user):
680
    join_policy = project.application.member_join_policy
681
    if join_policy == CLOSED_POLICY:
682
        return False
683
    m = user.get_membership(project)
684
    if m:
685
        return False
686
    return True
687

    
688

    
689
def join_project(project_id, request_user):
690
    project = get_project_for_update(project_id)
691
    join_project_checks(project)
692

    
693
    membership, created = ProjectMembership.objects.get_or_create(
694
        project=project,
695
        person=request_user)
696

    
697
    if not created:
698
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
699
        raise PermissionDenied(msg)
700

    
701
    auto_accepted = False
702
    join_policy = project.application.member_join_policy
703
    if (join_policy == AUTO_ACCEPT_POLICY and (
704
            not project.violates_members_limit(adding=1))):
705
        membership.accept()
706
        qh_sync_user(request_user.id)
707
        logger.info("User %s joined %s." %
708
                    (membership.person.log_display, project))
709
        auto_accepted = True
710
    else:
711
        membership_request_notify(project, membership.person)
712
        logger.info("User %s requested to join %s." %
713
                    (membership.person.log_display, project))
714
    return auto_accepted
715

    
716

    
717
def submit_application(owner=None,
718
                       name=None,
719
                       precursor_id=None,
720
                       homepage=None,
721
                       description=None,
722
                       start_date=None,
723
                       end_date=None,
724
                       member_join_policy=None,
725
                       member_leave_policy=None,
726
                       limit_on_members_number=None,
727
                       comments=None,
728
                       resource_policies=None,
729
                       request_user=None):
730

    
731
    precursor = None
732
    if precursor_id is not None:
733
        objs = ProjectApplication.objects
734
        precursor = objs.get_for_update(id=precursor_id)
735

    
736
        if (request_user and
737
            (not precursor.owner == request_user and
738
             not request_user.is_superuser
739
             and not request_user.is_project_admin())):
740
            m = _(astakos_messages.NOT_ALLOWED)
741
            raise PermissionDenied(m)
742

    
743
    force = request_user.is_project_admin()
744
    ok, limit = qh_add_pending_app(owner, precursor, force)
745
    if not ok:
746
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
747
        raise PermissionDenied(m)
748

    
749
    application = ProjectApplication(
750
        applicant=request_user,
751
        owner=owner,
752
        name=name,
753
        precursor_application_id=precursor_id,
754
        homepage=homepage,
755
        description=description,
756
        start_date=start_date,
757
        end_date=end_date,
758
        member_join_policy=member_join_policy,
759
        member_leave_policy=member_leave_policy,
760
        limit_on_members_number=limit_on_members_number,
761
        comments=comments)
762

    
763
    if precursor is None:
764
        application.chain = new_chain()
765
    else:
766
        chain = precursor.chain
767
        application.chain = chain
768
        objs = ProjectApplication.objects
769
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
770
        pending = q.select_for_update()
771
        for app in pending:
772
            app.state = ProjectApplication.REPLACED
773
            app.save()
774

    
775
    application.save()
776
    application.set_resource_policies(resource_policies)
777
    logger.info("User %s submitted %s." %
778
                (request_user.log_display, application.log_display))
779
    application_submit_notify(application)
780
    return application
781

    
782

    
783
def cancel_application(application_id, request_user=None, reason=""):
784
    application = get_application_for_update(application_id)
785
    checkAllowed(application, request_user)
786

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

    
792
    qh_release_pending_app(application.owner)
793

    
794
    application.cancel()
795
    logger.info("%s has been cancelled." % (application.log_display))
796

    
797

    
798
def dismiss_application(application_id, request_user=None, reason=""):
799
    application = get_application_for_update(application_id)
800
    checkAllowed(application, request_user)
801

    
802
    if not application.can_dismiss():
803
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
804
              (application.id, application.state_display()))
805
        raise PermissionDenied(m)
806

    
807
    application.dismiss()
808
    logger.info("%s has been dismissed." % (application.log_display))
809

    
810

    
811
def deny_application(application_id, request_user=None, reason=""):
812
    application = get_application_for_update(application_id)
813

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

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

    
821
    qh_release_pending_app(application.owner)
822

    
823
    application.deny(reason)
824
    logger.info("%s has been denied with reason \"%s\"." %
825
                (application.log_display, reason))
826
    application_deny_notify(application)
827

    
828

    
829
def approve_application(app_id, request_user=None, reason=""):
830

    
831
    try:
832
        objects = ProjectApplication.objects
833
        application = objects.get_for_update(id=app_id)
834
    except ProjectApplication.DoesNotExist:
835
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID % (app_id,))
836
        raise PermissionDenied(m)
837

    
838
    checkAllowed(application, request_user, admin_only=True)
839

    
840
    if not application.can_approve():
841
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
842
              (application.id, application.state_display()))
843
        raise PermissionDenied(m)
844

    
845
    qh_release_pending_app(application.owner)
846
    project = application.approve(reason)
847
    qh_sync_projects([project])
848
    logger.info("%s has been approved." % (application.log_display))
849
    application_approve_notify(application)
850

    
851

    
852
def check_expiration(execute=False):
853
    objects = Project.objects
854
    expired = objects.expired_projects()
855
    if execute:
856
        for project in expired:
857
            terminate(project.id)
858

    
859
    return [project.expiration_info() for project in expired]
860

    
861

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

    
867
    project.terminate()
868
    qh_sync_projects([project])
869
    logger.info("%s has been terminated." % (project))
870

    
871
    project_termination_notify(project)
872

    
873

    
874
def suspend(project_id, request_user=None):
875
    project = get_project_by_id(project_id)
876
    checkAllowed(project, request_user, admin_only=True)
877
    checkAlive(project)
878

    
879
    project.suspend()
880
    qh_sync_projects([project])
881
    logger.info("%s has been suspended." % (project))
882

    
883
    project_suspension_notify(project)
884

    
885

    
886
def resume(project_id, request_user=None):
887
    project = get_project_for_update(project_id)
888
    checkAllowed(project, request_user, admin_only=True)
889

    
890
    if not project.is_suspended:
891
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
892
        raise PermissionDenied(m)
893

    
894
    project.resume()
895
    qh_sync_projects([project])
896
    logger.info("%s has been unsuspended." % (project))
897

    
898

    
899
def get_by_chain_or_404(chain_id):
900
    try:
901
        project = Project.objects.get(id=chain_id)
902
        application = project.application
903
        return project, application
904
    except:
905
        application = ProjectApplication.objects.latest_of_chain(chain_id)
906
        if application is None:
907
            raise Http404
908
        else:
909
            return None, application
910

    
911

    
912
def get_user_setting(user_id, key):
913
    try:
914
        setting = UserSetting.objects.get(
915
            user=user_id, setting=key)
916
        return setting.value
917
    except UserSetting.DoesNotExist:
918
        return getattr(settings, key)
919

    
920

    
921
def set_user_setting(user_id, key, value):
922
    try:
923
        setting = UserSetting.objects.get_for_update(
924
            user=user_id, setting=key)
925
    except UserSetting.DoesNotExist:
926
        setting = UserSetting(user_id=user_id, setting=key)
927
    setting.value = value
928
    setting.save()
929

    
930

    
931
def unset_user_setting(user_id, key):
932
    UserSetting.objects.filter(user=user_id, setting=key).delete()
933

    
934

    
935
def _partition_by(f, l):
936
    d = {}
937
    for x in l:
938
        group = f(x)
939
        group_l = d.get(group, [])
940
        group_l.append(x)
941
        d[group] = group_l
942
    return d
943

    
944

    
945
def count_pending_app(users):
946
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
947
                                             owner__in=users)
948
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
949

    
950
    usage = {}
951
    for user in users:
952
        uuid = user.uuid
953
        usage[uuid] = len(apps_d.get(uuid, []))
954
    return usage
955

    
956

    
957
def qh_add_pending_app(user, precursor=None, force=False, dry_run=False):
958
    if precursor is None:
959
        diff = 1
960
    else:
961
        chain = precursor.chain
962
        objs = ProjectApplication.objects
963
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
964
        count = q.count()
965
        diff = 1 - count
966

    
967
    return register_pending_apps(user, diff, force, dry_run)
968

    
969

    
970
def qh_release_pending_app(user):
971
    register_pending_apps(user, -1)
972

    
973

    
974
def qh_sync_projects(projects):
975

    
976
    memberships = ProjectMembership.objects.filter(project__in=projects)
977
    user_ids = set(m.person_id for m in memberships)
978

    
979
    qh_sync_users(user_ids)