Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 7ce770be

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
def get_quota(users):
375
    pass
376

    
377

    
378
### PROJECT FUNCTIONS ###
379

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

    
384
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
385

    
386

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

    
395

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

    
405

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

    
414

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

    
422

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

    
430

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

    
438

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

    
446

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

    
454

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

    
462

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

    
472

    
473
def get_membership_for_update_by_id(project_id, memb_id):
474
    try:
475
        objs = ProjectMembership.objects
476
        return objs.get_for_update(project__id=project_id,
477
                                   person__id=memb_id)
478
    except ProjectMembership.DoesNotExist:
479
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
480
        raise IOError(m)
481

    
482

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

    
492
    if not request_user or request_user.is_project_admin():
493
        return
494

    
495
    if not admin_only and application.owner == request_user:
496
        return
497

    
498
    m = _(astakos_messages.NOT_ALLOWED)
499
    raise PermissionDenied(m)
500

    
501

    
502
def checkAlive(project):
503
    if not project.is_alive:
504
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__
505
        raise PermissionDenied(m)
506

    
507

    
508
def accept_membership_checks(project, request_user):
509
    checkAllowed(project, request_user)
510
    checkAlive(project)
511

    
512
    join_policy = project.application.member_join_policy
513
    if join_policy == CLOSED_POLICY:
514
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
515
        raise PermissionDenied(m)
516

    
517
    if project.violates_members_limit(adding=1):
518
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
519
        raise PermissionDenied(m)
520

    
521

    
522
def accept_membership(project_id, memb_id, request_user=None):
523
    project = get_project_for_update(project_id)
524
    accept_membership_checks(project, request_user)
525

    
526
    membership = get_membership_for_update_by_id(project_id, memb_id)
527
    if not membership.can_accept():
528
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
529
        raise PermissionDenied(m)
530

    
531
    user = membership.person
532
    membership.accept()
533
    qh_sync_user(user.id)
534
    logger.info("User %s has been accepted in %s." %
535
                (user.log_display, project))
536

    
537
    membership_change_notify(project, user, 'accepted')
538
    return membership
539

    
540

    
541
def reject_membership_checks(project, request_user):
542
    checkAllowed(project, request_user)
543
    checkAlive(project)
544

    
545

    
546
def reject_membership(project_id, memb_id, request_user=None):
547
    project = get_project_for_update(project_id)
548
    reject_membership_checks(project, request_user)
549
    membership = get_membership_for_update_by_id(project_id, memb_id)
550
    if not membership.can_reject():
551
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
552
        raise PermissionDenied(m)
553

    
554
    user = membership.person
555
    membership.reject()
556
    logger.info("Request of user %s for %s has been rejected." %
557
                (user.log_display, project))
558

    
559
    membership_change_notify(project, user, 'rejected')
560
    return membership
561

    
562

    
563
def cancel_membership_checks(project):
564
    checkAlive(project)
565

    
566

    
567
def cancel_membership(project_id, request_user):
568
    project = get_project_for_update(project_id)
569
    cancel_membership_checks(project)
570
    membership = get_membership_for_update(project_id, request_user.id)
571
    if not membership.can_cancel():
572
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
573
        raise PermissionDenied(m)
574

    
575
    membership.cancel()
576
    logger.info("Request of user %s for %s has been cancelled." %
577
                (membership.person.log_display, project))
578

    
579

    
580
def remove_membership_checks(project, request_user=None):
581
    checkAllowed(project, request_user)
582
    checkAlive(project)
583

    
584
    leave_policy = project.application.member_leave_policy
585
    if leave_policy == CLOSED_POLICY:
586
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
587
        raise PermissionDenied(m)
588

    
589

    
590
def remove_membership(project_id, memb_id, request_user=None):
591
    project = get_project_for_update(project_id)
592
    remove_membership_checks(project, request_user)
593
    membership = get_membership_for_update_by_id(project_id, memb_id)
594
    if not membership.can_remove():
595
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
596
        raise PermissionDenied(m)
597

    
598
    user = membership.person
599
    membership.remove()
600
    qh_sync_user(user.id)
601
    logger.info("User %s has been removed from %s." %
602
                (user.log_display, project))
603

    
604
    membership_change_notify(project, user, 'removed')
605
    return membership
606

    
607

    
608
def enroll_member(project_id, user, request_user=None):
609
    project = get_project_for_update(project_id)
610
    accept_membership_checks(project, request_user)
611

    
612
    membership, created = ProjectMembership.objects.get_or_create(
613
        project=project,
614
        person=user)
615

    
616
    if not membership.can_accept():
617
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
618
        raise PermissionDenied(m)
619

    
620
    membership.accept()
621
    qh_sync_user(user.id)
622
    logger.info("User %s has been enrolled in %s." %
623
                (membership.person.log_display, project))
624

    
625
    membership_enroll_notify(project, membership.person)
626
    return membership
627

    
628

    
629
def leave_project_checks(project):
630
    checkAlive(project)
631

    
632
    leave_policy = project.application.member_leave_policy
633
    if leave_policy == CLOSED_POLICY:
634
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
635
        raise PermissionDenied(m)
636

    
637

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

    
649

    
650
def leave_project(project_id, request_user):
651
    project = get_project_for_update(project_id)
652
    leave_project_checks(project)
653
    membership = get_membership_for_update(project_id, request_user.id)
654
    if not membership.can_leave():
655
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
656
        raise PermissionDenied(m)
657

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

    
673

    
674
def join_project_checks(project):
675
    checkAlive(project)
676

    
677
    join_policy = project.application.member_join_policy
678
    if join_policy == CLOSED_POLICY:
679
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
680
        raise PermissionDenied(m)
681

    
682

    
683
def can_join_request(project, user):
684
    join_policy = project.application.member_join_policy
685
    if join_policy == CLOSED_POLICY:
686
        return False
687
    m = user.get_membership(project)
688
    if m:
689
        return False
690
    return True
691

    
692

    
693
def join_project(project_id, request_user):
694
    project = get_project_for_update(project_id)
695
    join_project_checks(project)
696

    
697
    membership, created = ProjectMembership.objects.get_or_create(
698
        project=project,
699
        person=request_user)
700

    
701
    if not created:
702
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
703
        raise PermissionDenied(msg)
704

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

    
720

    
721
def submit_application(owner=None,
722
                       name=None,
723
                       precursor_id=None,
724
                       homepage=None,
725
                       description=None,
726
                       start_date=None,
727
                       end_date=None,
728
                       member_join_policy=None,
729
                       member_leave_policy=None,
730
                       limit_on_members_number=None,
731
                       comments=None,
732
                       resource_policies=None,
733
                       request_user=None):
734

    
735
    precursor = None
736
    if precursor_id is not None:
737
        objs = ProjectApplication.objects
738
        precursor = objs.get_for_update(id=precursor_id)
739

    
740
        if (request_user and
741
            (not precursor.owner == request_user and
742
             not request_user.is_superuser
743
             and not request_user.is_project_admin())):
744
            m = _(astakos_messages.NOT_ALLOWED)
745
            raise PermissionDenied(m)
746

    
747
    force = request_user.is_project_admin()
748
    ok, limit = qh_add_pending_app(owner, precursor, force)
749
    if not ok:
750
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
751
        raise PermissionDenied(m)
752

    
753
    application = ProjectApplication(
754
        applicant=request_user,
755
        owner=owner,
756
        name=name,
757
        precursor_application_id=precursor_id,
758
        homepage=homepage,
759
        description=description,
760
        start_date=start_date,
761
        end_date=end_date,
762
        member_join_policy=member_join_policy,
763
        member_leave_policy=member_leave_policy,
764
        limit_on_members_number=limit_on_members_number,
765
        comments=comments)
766

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

    
779
    application.save()
780
    application.set_resource_policies(resource_policies)
781
    logger.info("User %s submitted %s." %
782
                (request_user.log_display, application.log_display))
783
    application_submit_notify(application)
784
    return application
785

    
786

    
787
def cancel_application(application_id, request_user=None, reason=""):
788
    application = get_application_for_update(application_id)
789
    checkAllowed(application, request_user)
790

    
791
    if not application.can_cancel():
792
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
793
              (application.id, application.state_display()))
794
        raise PermissionDenied(m)
795

    
796
    qh_release_pending_app(application.owner)
797

    
798
    application.cancel()
799
    logger.info("%s has been cancelled." % (application.log_display))
800

    
801

    
802
def dismiss_application(application_id, request_user=None, reason=""):
803
    application = get_application_for_update(application_id)
804
    checkAllowed(application, request_user)
805

    
806
    if not application.can_dismiss():
807
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
808
              (application.id, application.state_display()))
809
        raise PermissionDenied(m)
810

    
811
    application.dismiss()
812
    logger.info("%s has been dismissed." % (application.log_display))
813

    
814

    
815
def deny_application(application_id, request_user=None, reason=""):
816
    application = get_application_for_update(application_id)
817

    
818
    checkAllowed(application, request_user, admin_only=True)
819

    
820
    if not application.can_deny():
821
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
822
              (application.id, application.state_display()))
823
        raise PermissionDenied(m)
824

    
825
    qh_release_pending_app(application.owner)
826

    
827
    application.deny(reason)
828
    logger.info("%s has been denied with reason \"%s\"." %
829
                (application.log_display, reason))
830
    application_deny_notify(application)
831

    
832

    
833
def approve_application(app_id, request_user=None, reason=""):
834

    
835
    try:
836
        objects = ProjectApplication.objects
837
        application = objects.get_for_update(id=app_id)
838
    except ProjectApplication.DoesNotExist:
839
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID % (app_id,))
840
        raise PermissionDenied(m)
841

    
842
    checkAllowed(application, request_user, admin_only=True)
843

    
844
    if not application.can_approve():
845
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
846
              (application.id, application.state_display()))
847
        raise PermissionDenied(m)
848

    
849
    qh_release_pending_app(application.owner)
850
    project = application.approve(reason)
851
    qh_sync_projects([project])
852
    logger.info("%s has been approved." % (application.log_display))
853
    application_approve_notify(application)
854

    
855

    
856
def check_expiration(execute=False):
857
    objects = Project.objects
858
    expired = objects.expired_projects()
859
    if execute:
860
        for project in expired:
861
            terminate(project.id)
862

    
863
    return [project.expiration_info() for project in expired]
864

    
865

    
866
def terminate(project_id, request_user=None):
867
    project = get_project_for_update(project_id)
868
    checkAllowed(project, request_user, admin_only=True)
869
    checkAlive(project)
870

    
871
    project.terminate()
872
    qh_sync_projects([project])
873
    logger.info("%s has been terminated." % (project))
874

    
875
    project_termination_notify(project)
876

    
877

    
878
def suspend(project_id, request_user=None):
879
    project = get_project_by_id(project_id)
880
    checkAllowed(project, request_user, admin_only=True)
881
    checkAlive(project)
882

    
883
    project.suspend()
884
    qh_sync_projects([project])
885
    logger.info("%s has been suspended." % (project))
886

    
887
    project_suspension_notify(project)
888

    
889

    
890
def resume(project_id, request_user=None):
891
    project = get_project_for_update(project_id)
892
    checkAllowed(project, request_user, admin_only=True)
893

    
894
    if not project.is_suspended:
895
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
896
        raise PermissionDenied(m)
897

    
898
    project.resume()
899
    qh_sync_projects([project])
900
    logger.info("%s has been unsuspended." % (project))
901

    
902

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

    
915

    
916
def get_user_setting(user_id, key):
917
    try:
918
        setting = UserSetting.objects.get(
919
            user=user_id, setting=key)
920
        return setting.value
921
    except UserSetting.DoesNotExist:
922
        return getattr(settings, key)
923

    
924

    
925
def set_user_setting(user_id, key, value):
926
    try:
927
        setting = UserSetting.objects.get_for_update(
928
            user=user_id, setting=key)
929
    except UserSetting.DoesNotExist:
930
        setting = UserSetting(user_id=user_id, setting=key)
931
    setting.value = value
932
    setting.save()
933

    
934

    
935
def unset_user_setting(user_id, key):
936
    UserSetting.objects.filter(user=user_id, setting=key).delete()
937

    
938

    
939
def _partition_by(f, l):
940
    d = {}
941
    for x in l:
942
        group = f(x)
943
        group_l = d.get(group, [])
944
        group_l.append(x)
945
        d[group] = group_l
946
    return d
947

    
948

    
949
def count_pending_app(users):
950
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
951
                                             owner__in=users)
952
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
953

    
954
    usage = {}
955
    for user in users:
956
        uuid = user.uuid
957
        usage[uuid] = len(apps_d.get(uuid, []))
958
    return usage
959

    
960

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

    
971
    return register_pending_apps(user, diff, force, dry_run)
972

    
973

    
974
def qh_release_pending_app(user):
975
    register_pending_apps(user, -1)
976

    
977

    
978
def qh_sync_projects(projects):
979

    
980
    memberships = ProjectMembership.objects.filter(project__in=projects)
981
    user_ids = set(m.person_id for m in memberships)
982

    
983
    qh_sync_users(user_ids)