Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 6da04174

History | View | Annotate | Download (31.1 kB)

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

    
34
import logging
35
import socket
36

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

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

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

    
82
logger = logging.getLogger(__name__)
83

    
84

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

    
94

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

    
100

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

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

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

    
128

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

    
134

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

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

    
158

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

    
164

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

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

    
187

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

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

    
216

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

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

    
241

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

    
260

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

    
280

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

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

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

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

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

    
327

    
328
class SendMailError(Exception):
329
    pass
330

    
331

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

    
337

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

    
343

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

    
349

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

    
355

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

    
361

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

    
367

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

    
373

    
374
def get_quota(users):
375
    pass
376

    
377

    
378
### PROJECT FUNCTIONS ###
379

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

    
384
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
385

    
386

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

    
395

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

    
405

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

    
414

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

    
422

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

    
430

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

    
438

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

    
446

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

    
454

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

    
462

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

    
472

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

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

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

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

    
491

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

    
497

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

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

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

    
511

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

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

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

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

    
529

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

    
534

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

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

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

    
550

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

    
554

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

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

    
567

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

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

    
577

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

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

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

    
594

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

    
599
    membership, created = ProjectMembership.objects.get_or_create(
600
        project=project,
601
        person=user)
602

    
603
    if not membership.can_accept():
604
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
605
        raise PermissionDenied(m)
606

    
607
    membership.accept()
608
    qh_sync_user(user.id)
609
    logger.info("User %s has been enrolled in %s." %
610
                (membership.person.log_display, project))
611

    
612
    membership_enroll_notify(project, membership.person)
613
    return membership
614

    
615

    
616
def leave_project_checks(project):
617
    checkAlive(project)
618

    
619
    leave_policy = project.application.member_leave_policy
620
    if leave_policy == CLOSED_POLICY:
621
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
622
        raise PermissionDenied(m)
623

    
624

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

    
636

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

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

    
660

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

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

    
669

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

    
679

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

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

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

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

    
707

    
708
def submit_application(owner=None,
709
                       name=None,
710
                       precursor_id=None,
711
                       homepage=None,
712
                       description=None,
713
                       start_date=None,
714
                       end_date=None,
715
                       member_join_policy=None,
716
                       member_leave_policy=None,
717
                       limit_on_members_number=None,
718
                       comments=None,
719
                       resource_policies=None,
720
                       request_user=None):
721

    
722
    precursor = None
723
    if precursor_id is not None:
724
        objs = ProjectApplication.objects
725
        precursor = objs.get_for_update(id=precursor_id)
726

    
727
        if (request_user and
728
            (not precursor.owner == request_user and
729
             not request_user.is_superuser
730
             and not request_user.is_project_admin())):
731
            m = _(astakos_messages.NOT_ALLOWED)
732
            raise PermissionDenied(m)
733

    
734
    force = request_user.is_project_admin()
735
    ok, limit = qh_add_pending_app(owner, precursor, force)
736
    if not ok:
737
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
738
        raise PermissionDenied(m)
739

    
740
    application = ProjectApplication(
741
        applicant=request_user,
742
        owner=owner,
743
        name=name,
744
        precursor_application_id=precursor_id,
745
        homepage=homepage,
746
        description=description,
747
        start_date=start_date,
748
        end_date=end_date,
749
        member_join_policy=member_join_policy,
750
        member_leave_policy=member_leave_policy,
751
        limit_on_members_number=limit_on_members_number,
752
        comments=comments)
753

    
754
    if precursor is None:
755
        application.chain = new_chain()
756
    else:
757
        chain = precursor.chain
758
        application.chain = chain
759
        objs = ProjectApplication.objects
760
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
761
        pending = q.select_for_update()
762
        for app in pending:
763
            app.state = ProjectApplication.REPLACED
764
            app.save()
765

    
766
    application.save()
767
    application.set_resource_policies(resource_policies)
768
    logger.info("User %s submitted %s." %
769
                (request_user.log_display, application.log_display))
770
    application_submit_notify(application)
771
    return application
772

    
773

    
774
def cancel_application(application_id, request_user=None):
775
    application = get_application_for_update(application_id)
776
    checkAllowed(application, request_user)
777

    
778
    if not application.can_cancel():
779
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
780
              (application.id, application.state_display()))
781
        raise PermissionDenied(m)
782

    
783
    qh_release_pending_app(application.owner)
784

    
785
    application.cancel()
786
    logger.info("%s has been cancelled." % (application.log_display))
787

    
788

    
789
def dismiss_application(application_id, request_user=None):
790
    application = get_application_for_update(application_id)
791
    checkAllowed(application, request_user)
792

    
793
    if not application.can_dismiss():
794
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
795
              (application.id, application.state_display()))
796
        raise PermissionDenied(m)
797

    
798
    application.dismiss()
799
    logger.info("%s has been dismissed." % (application.log_display))
800

    
801

    
802
def deny_application(application_id, request_user=None, reason=None):
803
    application = get_application_for_update(application_id)
804

    
805
    checkAllowed(application, request_user, admin_only=True)
806

    
807
    if not application.can_deny():
808
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
809
              (application.id, application.state_display()))
810
        raise PermissionDenied(m)
811

    
812
    qh_release_pending_app(application.owner)
813

    
814
    if reason is None:
815
        reason = ""
816
    application.deny(reason)
817
    logger.info("%s has been denied with reason \"%s\"." %
818
                (application.log_display, reason))
819
    application_deny_notify(application)
820

    
821

    
822
def approve_application(app_id, request_user=None):
823

    
824
    try:
825
        objects = ProjectApplication.objects
826
        application = objects.get_for_update(id=app_id)
827
    except ProjectApplication.DoesNotExist:
828
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID % (app_id,))
829
        raise PermissionDenied(m)
830

    
831
    checkAllowed(application, request_user, admin_only=True)
832

    
833
    if not application.can_approve():
834
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
835
              (application.id, application.state_display()))
836
        raise PermissionDenied(m)
837

    
838
    qh_release_pending_app(application.owner)
839
    project = application.approve()
840
    qh_sync_projects([project])
841
    logger.info("%s has been approved." % (application.log_display))
842
    application_approve_notify(application)
843

    
844

    
845
def check_expiration(execute=False):
846
    objects = Project.objects
847
    expired = objects.expired_projects()
848
    if execute:
849
        for project in expired:
850
            terminate(project.id)
851

    
852
    return [project.expiration_info() for project in expired]
853

    
854

    
855
def terminate(project_id, request_user=None):
856
    project = get_project_for_update(project_id)
857
    checkAllowed(project, request_user, admin_only=True)
858
    checkAlive(project)
859

    
860
    project.terminate()
861
    qh_sync_projects([project])
862
    logger.info("%s has been terminated." % (project))
863

    
864
    project_termination_notify(project)
865

    
866

    
867
def suspend(project_id, request_user=None):
868
    project = get_project_by_id(project_id)
869
    checkAllowed(project, request_user, admin_only=True)
870
    checkAlive(project)
871

    
872
    project.suspend()
873
    qh_sync_projects([project])
874
    logger.info("%s has been suspended." % (project))
875

    
876
    project_suspension_notify(project)
877

    
878

    
879
def resume(project_id, request_user=None):
880
    project = get_project_for_update(project_id)
881
    checkAllowed(project, request_user, admin_only=True)
882

    
883
    if not project.is_suspended:
884
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
885
        raise PermissionDenied(m)
886

    
887
    project.resume()
888
    qh_sync_projects([project])
889
    logger.info("%s has been unsuspended." % (project))
890

    
891

    
892
def get_by_chain_or_404(chain_id):
893
    try:
894
        project = Project.objects.get(id=chain_id)
895
        application = project.application
896
        return project, application
897
    except:
898
        application = ProjectApplication.objects.latest_of_chain(chain_id)
899
        if application is None:
900
            raise Http404
901
        else:
902
            return None, application
903

    
904

    
905
def get_user_setting(user_id, key):
906
    try:
907
        setting = UserSetting.objects.get(
908
            user=user_id, setting=key)
909
        return setting.value
910
    except UserSetting.DoesNotExist:
911
        return getattr(settings, key)
912

    
913

    
914
def set_user_setting(user_id, key, value):
915
    try:
916
        setting = UserSetting.objects.get_for_update(
917
            user=user_id, setting=key)
918
    except UserSetting.DoesNotExist:
919
        setting = UserSetting(user_id=user_id, setting=key)
920
    setting.value = value
921
    setting.save()
922

    
923

    
924
def unset_user_setting(user_id, key):
925
    UserSetting.objects.filter(user=user_id, setting=key).delete()
926

    
927

    
928
def _partition_by(f, l):
929
    d = {}
930
    for x in l:
931
        group = f(x)
932
        group_l = d.get(group, [])
933
        group_l.append(x)
934
        d[group] = group_l
935
    return d
936

    
937

    
938
def count_pending_app(users):
939
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
940
                                             owner__in=users)
941
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
942

    
943
    usage = {}
944
    for user in users:
945
        uuid = user.uuid
946
        usage[uuid] = len(apps_d.get(uuid, []))
947
    return usage
948

    
949

    
950
def qh_add_pending_app(user, precursor=None, force=False, dry_run=False):
951
    if precursor is None:
952
        diff = 1
953
    else:
954
        chain = precursor.chain
955
        objs = ProjectApplication.objects
956
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
957
        count = q.count()
958
        diff = 1 - count
959

    
960
    return register_pending_apps(user, diff, force, dry_run)
961

    
962

    
963
def qh_release_pending_app(user):
964
    register_pending_apps(user, -1)
965

    
966

    
967
def qh_sync_projects(projects):
968

    
969
    memberships = ProjectMembership.objects.filter(project__in=projects)
970
    user_ids = set(m.person_id for m in memberships)
971

    
972
    qh_sync_users(user_ids)