Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 5764728a

History | View | Annotate | Download (33.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

    
36
from django.utils.translation import ugettext as _
37
from django.core.mail import send_mail, get_connection
38
from django.core.urlresolvers import reverse
39
from django.contrib.auth import login as auth_login, logout as auth_logout
40
from django.db.models import Q
41

    
42
from synnefo_branding.utils import render_to_string
43

    
44
from synnefo.lib import join_urls
45
from astakos.im.models import AstakosUser, Invitation, ProjectMembership, \
46
    ProjectApplication, Project, new_chain, Resource, ProjectLock
47
from astakos.im.quotas import qh_sync_user, get_pending_app_quota, \
48
    register_pending_apps, qh_sync_project, qh_sync_locked_users, \
49
    get_users_for_update, members_to_sync
50
from astakos.im.project_notif import membership_change_notify, \
51
    membership_enroll_notify, membership_request_notify, \
52
    membership_leave_request_notify, application_submit_notify, \
53
    application_approve_notify, application_deny_notify, \
54
    project_termination_notify, project_suspension_notify, \
55
    project_unsuspension_notify, project_reinstatement_notify
56
from astakos.im import settings
57

    
58
import astakos.im.messages as astakos_messages
59

    
60
logger = logging.getLogger(__name__)
61

    
62

    
63
def login(request, user):
64
    auth_login(request, user)
65
    from astakos.im.models import SessionCatalog
66
    SessionCatalog(
67
        session_key=request.session.session_key,
68
        user=user
69
    ).save()
70
    logger.info('%s logged in.', user.log_display)
71

    
72

    
73
def logout(request, *args, **kwargs):
74
    user = request.user
75
    auth_logout(request, *args, **kwargs)
76
    user.delete_online_access_tokens()
77
    logger.info('%s logged out.', user.log_display)
78

    
79

    
80
def send_verification(user, template_name='im/activation_email.txt'):
81
    """
82
    Send email to user to verify his/her email and activate his/her account.
83
    """
84
    url = join_urls(settings.BASE_HOST,
85
                    user.get_activation_url(nxt=reverse('index')))
86
    message = render_to_string(template_name, {
87
                               'user': user,
88
                               'url': url,
89
                               'baseurl': settings.BASE_URL,
90
                               'site_name': settings.SITENAME,
91
                               'support': settings.CONTACT_EMAIL})
92
    sender = settings.SERVER_EMAIL
93
    send_mail(_(astakos_messages.VERIFICATION_EMAIL_SUBJECT), message, sender,
94
              [user.email],
95
              connection=get_connection())
96
    logger.info("Sent user verirfication email: %s", user.log_display)
97

    
98

    
99
def _send_admin_notification(template_name,
100
                             context=None,
101
                             user=None,
102
                             msg="",
103
                             subject='alpha2 testing notification',):
104
    """
105
    Send notification email to settings.HELPDESK + settings.MANAGERS +
106
    settings.ADMINS.
107
    """
108
    if context is None:
109
        context = {}
110
    if not 'user' in context:
111
        context['user'] = user
112

    
113
    message = render_to_string(template_name, context)
114
    sender = settings.SERVER_EMAIL
115
    recipient_list = [e[1] for e in settings.HELPDESK +
116
                      settings.MANAGERS + settings.ADMINS]
117
    send_mail(subject, message, sender, recipient_list,
118
              connection=get_connection())
119
    if user:
120
        msg = 'Sent admin notification (%s) for user %s' % (msg,
121
                                                            user.log_display)
122
    else:
123
        msg = 'Sent admin notification (%s)' % msg
124

    
125
    logger.log(settings.LOGGING_LEVEL, msg)
126

    
127

    
128
def send_account_pending_moderation_notification(
129
        user,
130
        template_name='im/account_pending_moderation_notification.txt'):
131
    """
132
    Notify admins that a new user has verified his email address and moderation
133
    step is required to activate his account.
134
    """
135
    subject = (_(astakos_messages.ACCOUNT_CREATION_SUBJECT) %
136
               {'user': user.email})
137
    return _send_admin_notification(template_name, {}, subject=subject,
138
                                    user=user, msg="account creation")
139

    
140

    
141
def send_account_activated_notification(
142
        user,
143
        template_name='im/account_activated_notification.txt'):
144
    """
145
    Send email to settings.HELPDESK + settings.MANAGERES + settings.ADMINS
146
    lists to notify that a new account has been accepted and activated.
147
    """
148
    message = render_to_string(
149
        template_name,
150
        {'user': user}
151
    )
152
    sender = settings.SERVER_EMAIL
153
    recipient_list = [e[1] for e in settings.HELPDESK +
154
                      settings.MANAGERS + settings.ADMINS]
155
    send_mail(_(astakos_messages.HELPDESK_NOTIFICATION_EMAIL_SUBJECT) %
156
              {'user': user.email},
157
              message, sender, recipient_list, connection=get_connection())
158
    msg = 'Sent helpdesk admin notification for %s'
159
    logger.log(settings.LOGGING_LEVEL, msg, user.email)
160

    
161

    
162
def send_invitation(invitation, template_name='im/invitation.txt'):
163
    """
164
    Send invitation email.
165
    """
166
    subject = _(astakos_messages.INVITATION_EMAIL_SUBJECT)
167
    url = '%s?code=%d' % (join_urls(settings.BASE_HOST,
168
                                    reverse('index')), invitation.code)
169
    message = render_to_string(template_name, {
170
                               'invitation': invitation,
171
                               'url': url,
172
                               'baseurl': settings.BASE_URL,
173
                               'site_name': settings.SITENAME,
174
                               'support': settings.CONTACT_EMAIL})
175
    sender = settings.SERVER_EMAIL
176
    send_mail(subject, message, sender, [invitation.username],
177
              connection=get_connection())
178
    msg = 'Sent invitation %s'
179
    logger.log(settings.LOGGING_LEVEL, msg, invitation)
180
    inviter_invitations = invitation.inviter.invitations
181
    invitation.inviter.invitations = max(0, inviter_invitations - 1)
182
    invitation.inviter.save()
183

    
184

    
185
def send_greeting(user, email_template_name='im/welcome_email.txt'):
186
    """
187
    Send welcome email to an accepted/activated user.
188

189
    Raises SMTPException, socket.error
190
    """
191
    subject = _(astakos_messages.GREETING_EMAIL_SUBJECT)
192
    message = render_to_string(email_template_name, {
193
                               'user': user,
194
                               'url': join_urls(settings.BASE_HOST,
195
                                                reverse('index')),
196
                               'baseurl': settings.BASE_URL,
197
                               'site_name': settings.SITENAME,
198
                               'support': settings.CONTACT_EMAIL})
199
    sender = settings.SERVER_EMAIL
200
    send_mail(subject, message, sender, [user.email],
201
              connection=get_connection())
202
    msg = 'Sent greeting %s'
203
    logger.log(settings.LOGGING_LEVEL, msg, user.log_display)
204

    
205

    
206
def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'):
207
    subject = _(astakos_messages.FEEDBACK_EMAIL_SUBJECT)
208
    from_email = settings.SERVER_EMAIL
209
    recipient_list = [e[1] for e in settings.HELPDESK]
210
    content = render_to_string(email_template_name, {
211
        'message': msg,
212
        'data': data,
213
        'user': user})
214
    send_mail(subject, content, from_email, recipient_list,
215
              connection=get_connection())
216
    msg = 'Sent feedback from %s'
217
    logger.log(settings.LOGGING_LEVEL, msg, user.log_display)
218

    
219

    
220
def send_change_email(ec, request,
221
                      email_template_name=
222
                      'registration/email_change_email.txt'):
223
    url = ec.get_url()
224
    url = request.build_absolute_uri(url)
225
    c = {'url': url,
226
         'site_name': settings.SITENAME,
227
         'support': settings.CONTACT_EMAIL,
228
         'ec': ec}
229
    message = render_to_string(email_template_name, c)
230
    from_email = settings.SERVER_EMAIL
231
    send_mail(_(astakos_messages.EMAIL_CHANGE_EMAIL_SUBJECT), message,
232
              from_email,
233
              [ec.new_email_address], connection=get_connection())
234
    msg = 'Sent change email for %s'
235
    logger.log(settings.LOGGING_LEVEL, msg, ec.user.log_display)
236

    
237

    
238
def invite(inviter, email, realname):
239
    inv = Invitation(inviter=inviter, username=email, realname=realname)
240
    inv.save()
241
    send_invitation(inv)
242
    inviter.invitations = max(0, inviter.invitations - 1)
243
    inviter.save()
244

    
245

    
246
### PROJECT FUNCTIONS ###
247

    
248

    
249
class ProjectError(Exception):
250
    pass
251

    
252

    
253
class ProjectNotFound(ProjectError):
254
    pass
255

    
256

    
257
class ProjectForbidden(ProjectError):
258
    pass
259

    
260

    
261
class ProjectBadRequest(ProjectError):
262
    pass
263

    
264

    
265
class ProjectConflict(ProjectError):
266
    pass
267

    
268
AUTO_ACCEPT_POLICY = 1
269
MODERATED_POLICY = 2
270
CLOSED_POLICY = 3
271

    
272
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
273

    
274

    
275
def get_related_project_id(application_id):
276
    try:
277
        app = ProjectApplication.objects.get(id=application_id)
278
        return app.chain_id
279
    except ProjectApplication.DoesNotExist:
280
        return None
281

    
282

    
283
def get_project_by_id(project_id):
284
    try:
285
        return Project.objects.select_related(
286
            "application", "application__owner",
287
            "application__applicant").get(id=project_id)
288
    except Project.DoesNotExist:
289
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
290
        raise ProjectNotFound(m)
291

    
292

    
293
def get_project_for_update(project_id):
294
    try:
295
        return Project.objects.select_for_update().get(id=project_id)
296
    except Project.DoesNotExist:
297
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
298
        raise ProjectNotFound(m)
299

    
300

    
301
def get_project_of_application_for_update(app_id):
302
    app = get_application(app_id)
303
    return get_project_for_update(app.chain_id)
304

    
305

    
306
def get_project_lock():
307
    ProjectLock.objects.select_for_update().get(pk=1)
308

    
309

    
310
def get_application(application_id):
311
    try:
312
        return ProjectApplication.objects.get(id=application_id)
313
    except ProjectApplication.DoesNotExist:
314
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % application_id
315
        raise ProjectNotFound(m)
316

    
317

    
318
def get_project_of_membership_for_update(memb_id):
319
    m = get_membership_by_id(memb_id)
320
    return get_project_for_update(m.project_id)
321

    
322

    
323
def get_user_by_uuid(uuid):
324
    try:
325
        return AstakosUser.objects.get(uuid=uuid)
326
    except AstakosUser.DoesNotExist:
327
        m = _(astakos_messages.UNKNOWN_USER_ID) % uuid
328
        raise ProjectNotFound(m)
329

    
330

    
331
def get_membership(project_id, user_id):
332
    try:
333
        objs = ProjectMembership.objects.select_related('project', 'person')
334
        return objs.get(project__id=project_id, person__id=user_id)
335
    except ProjectMembership.DoesNotExist:
336
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
337
        raise ProjectNotFound(m)
338

    
339

    
340
def get_membership_by_id(memb_id):
341
    try:
342
        objs = ProjectMembership.objects.select_related('project', 'person')
343
        return objs.get(id=memb_id)
344
    except ProjectMembership.DoesNotExist:
345
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
346
        raise ProjectNotFound(m)
347

    
348

    
349
ALLOWED_CHECKS = [
350
    (lambda u, a: not u or u.is_project_admin()),
351
    (lambda u, a: a.owner == u),
352
    (lambda u, a: a.applicant == u),
353
    (lambda u, a: a.chain.overall_state() == Project.O_ACTIVE
354
     or bool(a.chain.projectmembership_set.any_accepted().filter(person=u))),
355
]
356

    
357
ADMIN_LEVEL = 0
358
OWNER_LEVEL = 1
359
APPLICANT_LEVEL = 2
360
ANY_LEVEL = 3
361

    
362

    
363
def _check_yield(b, silent=False):
364
    if b:
365
        return True
366

    
367
    if silent:
368
        return False
369

    
370
    m = _(astakos_messages.NOT_ALLOWED)
371
    raise ProjectForbidden(m)
372

    
373

    
374
def membership_check_allowed(membership, request_user,
375
                             level=OWNER_LEVEL, silent=False):
376
    r = project_check_allowed(
377
        membership.project, request_user, level, silent=True)
378

    
379
    return _check_yield(r or membership.person == request_user, silent)
380

    
381

    
382
def project_check_allowed(project, request_user,
383
                          level=OWNER_LEVEL, silent=False):
384
    return app_check_allowed(project.application, request_user, level, silent)
385

    
386

    
387
def app_check_allowed(application, request_user,
388
                      level=OWNER_LEVEL, silent=False):
389
    checks = (f(request_user, application) for f in ALLOWED_CHECKS[:level+1])
390
    return _check_yield(any(checks), silent)
391

    
392

    
393
def checkAlive(project):
394
    if not project.is_alive:
395
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.id
396
        raise ProjectConflict(m)
397

    
398

    
399
def accept_membership_project_checks(project, request_user):
400
    project_check_allowed(project, request_user)
401
    checkAlive(project)
402

    
403
    join_policy = project.application.member_join_policy
404
    if join_policy == CLOSED_POLICY:
405
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
406
        raise ProjectConflict(m)
407

    
408
    if project.violates_members_limit(adding=1):
409
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
410
        raise ProjectConflict(m)
411

    
412

    
413
def accept_membership_checks(membership, request_user):
414
    if not membership.check_action("accept"):
415
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
416
        raise ProjectConflict(m)
417

    
418
    project = membership.project
419
    accept_membership_project_checks(project, request_user)
420

    
421

    
422
def accept_membership(memb_id, request_user=None, reason=None):
423
    project = get_project_of_membership_for_update(memb_id)
424
    membership = get_membership_by_id(memb_id)
425
    accept_membership_checks(membership, request_user)
426
    user = membership.person
427
    membership.perform_action("accept", actor=request_user, reason=reason)
428
    qh_sync_user(user)
429
    logger.info("User %s has been accepted in %s." %
430
                (user.log_display, project))
431

    
432
    membership_change_notify(project, user, 'accepted')
433
    return membership
434

    
435

    
436
def reject_membership_checks(membership, request_user):
437
    if not membership.check_action("reject"):
438
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
439
        raise ProjectConflict(m)
440

    
441
    project = membership.project
442
    project_check_allowed(project, request_user)
443
    checkAlive(project)
444

    
445

    
446
def reject_membership(memb_id, request_user=None, reason=None):
447
    project = get_project_of_membership_for_update(memb_id)
448
    membership = get_membership_by_id(memb_id)
449
    reject_membership_checks(membership, request_user)
450
    user = membership.person
451
    membership.perform_action("reject", actor=request_user, reason=reason)
452
    logger.info("Request of user %s for %s has been rejected." %
453
                (user.log_display, project))
454

    
455
    membership_change_notify(project, user, 'rejected')
456
    return membership
457

    
458

    
459
def cancel_membership_checks(membership, request_user):
460
    if not membership.check_action("cancel"):
461
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
462
        raise ProjectConflict(m)
463

    
464
    membership_check_allowed(membership, request_user, level=ADMIN_LEVEL)
465
    project = membership.project
466
    checkAlive(project)
467

    
468

    
469
def cancel_membership(memb_id, request_user, reason=None):
470
    project = get_project_of_membership_for_update(memb_id)
471
    membership = get_membership_by_id(memb_id)
472
    cancel_membership_checks(membership, request_user)
473
    membership.perform_action("cancel", actor=request_user, reason=reason)
474
    logger.info("Request of user %s for %s has been cancelled." %
475
                (membership.person.log_display, project))
476

    
477

    
478
def remove_membership_checks(membership, request_user=None):
479
    if not membership.check_action("remove"):
480
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
481
        raise ProjectConflict(m)
482

    
483
    project = membership.project
484
    project_check_allowed(project, request_user)
485
    checkAlive(project)
486

    
487
    leave_policy = project.application.member_leave_policy
488
    if leave_policy == CLOSED_POLICY:
489
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
490
        raise ProjectConflict(m)
491

    
492

    
493
def remove_membership(memb_id, request_user=None, reason=None):
494
    project = get_project_of_membership_for_update(memb_id)
495
    membership = get_membership_by_id(memb_id)
496
    remove_membership_checks(membership, request_user)
497
    user = membership.person
498
    membership.perform_action("remove", actor=request_user, reason=reason)
499
    qh_sync_user(user)
500
    logger.info("User %s has been removed from %s." %
501
                (user.log_display, project))
502

    
503
    membership_change_notify(project, user, 'removed')
504
    return membership
505

    
506

    
507
def enroll_member_by_email(project_id, email, request_user=None, reason=None):
508
    try:
509
        user = AstakosUser.objects.accepted().get(email=email)
510
        return enroll_member(project_id, user, request_user, reason=reason)
511
    except AstakosUser.DoesNotExist:
512
        raise ProjectConflict(astakos_messages.UNKNOWN_USERS % email)
513

    
514

    
515
def enroll_member(project_id, user, request_user=None, reason=None):
516
    try:
517
        project = get_project_for_update(project_id)
518
    except ProjectNotFound as e:
519
        raise ProjectConflict(e.message)
520
    accept_membership_project_checks(project, request_user)
521

    
522
    try:
523
        membership = get_membership(project_id, user.id)
524
        if not membership.check_action("enroll"):
525
            m = _(astakos_messages.MEMBERSHIP_ACCEPTED)
526
            raise ProjectConflict(m)
527
        membership.perform_action("enroll", actor=request_user, reason=reason)
528
    except ProjectNotFound:
529
        membership = new_membership(project, user, actor=request_user,
530
                                    enroll=True)
531

    
532
    qh_sync_user(user)
533
    logger.info("User %s has been enrolled in %s." %
534
                (membership.person.log_display, project))
535

    
536
    membership_enroll_notify(project, membership.person)
537
    return membership
538

    
539

    
540
def leave_project_checks(membership, request_user):
541
    if not membership.check_action("leave"):
542
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
543
        raise ProjectConflict(m)
544

    
545
    membership_check_allowed(membership, request_user, level=ADMIN_LEVEL)
546
    project = membership.project
547
    checkAlive(project)
548

    
549
    leave_policy = project.application.member_leave_policy
550
    if leave_policy == CLOSED_POLICY:
551
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
552
        raise ProjectConflict(m)
553

    
554

    
555
def can_leave_request(project, user):
556
    m = user.get_membership(project)
557
    if m is None:
558
        return False
559
    try:
560
        leave_project_checks(m, user)
561
    except ProjectError:
562
        return False
563
    return True
564

    
565

    
566
def leave_project(memb_id, request_user, reason=None):
567
    project = get_project_of_membership_for_update(memb_id)
568
    membership = get_membership_by_id(memb_id)
569
    leave_project_checks(membership, request_user)
570

    
571
    auto_accepted = False
572
    leave_policy = project.application.member_leave_policy
573
    if leave_policy == AUTO_ACCEPT_POLICY:
574
        membership.perform_action("remove", actor=request_user, reason=reason)
575
        qh_sync_user(request_user)
576
        logger.info("User %s has left %s." %
577
                    (request_user.log_display, project))
578
        auto_accepted = True
579
    else:
580
        membership.perform_action("leave_request", actor=request_user,
581
                                  reason=reason)
582
        logger.info("User %s requested to leave %s." %
583
                    (request_user.log_display, project))
584
        membership_leave_request_notify(project, membership.person)
585
    return auto_accepted
586

    
587

    
588
def join_project_checks(project):
589
    checkAlive(project)
590

    
591
    join_policy = project.application.member_join_policy
592
    if join_policy == CLOSED_POLICY:
593
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
594
        raise ProjectConflict(m)
595

    
596

    
597
Nothing = type('Nothing', (), {})
598

    
599

    
600
def can_join_request(project, user, membership=Nothing):
601
    try:
602
        join_project_checks(project)
603
    except ProjectError:
604
        return False
605

    
606
    m = (membership if membership is not Nothing
607
         else user.get_membership(project))
608
    if not m:
609
        return True
610
    return m.check_action("join")
611

    
612

    
613
def new_membership(project, user, actor=None, reason=None, enroll=False):
614
    state = (ProjectMembership.ACCEPTED if enroll
615
             else ProjectMembership.REQUESTED)
616
    m = ProjectMembership.objects.create(
617
        project=project, person=user, state=state)
618
    m._log_create(None, state, actor=actor, reason=reason)
619
    return m
620

    
621

    
622
def join_project(project_id, request_user, reason=None):
623
    project = get_project_for_update(project_id)
624
    join_project_checks(project)
625

    
626
    try:
627
        membership = get_membership(project.id, request_user.id)
628
        if not membership.check_action("join"):
629
            msg = _(astakos_messages.MEMBERSHIP_ASSOCIATED)
630
            raise ProjectConflict(msg)
631
        membership.perform_action("join", actor=request_user, reason=reason)
632
    except ProjectNotFound:
633
        membership = new_membership(project, request_user, actor=request_user,
634
                                    reason=reason)
635

    
636
    join_policy = project.application.member_join_policy
637
    if (join_policy == AUTO_ACCEPT_POLICY and (
638
            not project.violates_members_limit(adding=1))):
639
        membership.perform_action("accept", actor=request_user, reason=reason)
640
        qh_sync_user(request_user)
641
        logger.info("User %s joined %s." %
642
                    (request_user.log_display, project))
643
    else:
644
        membership_request_notify(project, membership.person)
645
        logger.info("User %s requested to join %s." %
646
                    (request_user.log_display, project))
647
    return membership
648

    
649

    
650
MEMBERSHIP_ACTION_CHECKS = {
651
    "leave":  leave_project_checks,
652
    "cancel": cancel_membership_checks,
653
    "accept": accept_membership_checks,
654
    "reject": reject_membership_checks,
655
    "remove": remove_membership_checks,
656
}
657

    
658

    
659
def membership_allowed_actions(membership, request_user):
660
    allowed = []
661
    for action, check in MEMBERSHIP_ACTION_CHECKS.iteritems():
662
        try:
663
            check(membership, request_user)
664
            allowed.append(action)
665
        except ProjectError:
666
            pass
667
    return allowed
668

    
669

    
670
def submit_application(owner=None,
671
                       name=None,
672
                       project_id=None,
673
                       homepage=None,
674
                       description=None,
675
                       start_date=None,
676
                       end_date=None,
677
                       member_join_policy=None,
678
                       member_leave_policy=None,
679
                       limit_on_members_number=None,
680
                       comments=None,
681
                       resources=None,
682
                       request_user=None):
683

    
684
    project = None
685
    if project_id is not None:
686
        project = get_project_for_update(project_id)
687
        project_check_allowed(project, request_user, level=APPLICANT_LEVEL)
688

    
689
    policies = validate_resource_policies(resources)
690

    
691
    force = request_user.is_project_admin()
692
    ok, limit = qh_add_pending_app(owner, project, force)
693
    if not ok:
694
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
695
        raise ProjectConflict(m)
696

    
697
    application = ProjectApplication(
698
        applicant=request_user,
699
        owner=owner,
700
        name=name,
701
        homepage=homepage,
702
        description=description,
703
        start_date=start_date,
704
        end_date=end_date,
705
        member_join_policy=member_join_policy,
706
        member_leave_policy=member_leave_policy,
707
        limit_on_members_number=limit_on_members_number,
708
        comments=comments)
709

    
710
    if project is None:
711
        chain = new_chain()
712
        application.chain_id = chain.chain
713
        application.save()
714
        Project.objects.create(id=chain.chain, application=application)
715
    else:
716
        application.chain = project
717
        application.save()
718
        if project.application.state != ProjectApplication.APPROVED:
719
            project.application = application
720
            project.save()
721

    
722
        pending = ProjectApplication.objects.filter(
723
            chain=project,
724
            state=ProjectApplication.PENDING).exclude(id=application.id)
725
        for app in pending:
726
            app.state = ProjectApplication.REPLACED
727
            app.save()
728

    
729
    if policies is not None:
730
        set_resource_policies(application, policies)
731
    logger.info("User %s submitted %s." %
732
                (request_user.log_display, application.log_display))
733
    application_submit_notify(application)
734
    return application
735

    
736

    
737
def validate_resource_policies(policies):
738
    if not isinstance(policies, dict):
739
        raise ProjectBadRequest("Malformed resource policies")
740

    
741
    resource_names = policies.keys()
742
    resources = Resource.objects.filter(name__in=resource_names,
743
                                        api_visible=True)
744
    resource_d = {}
745
    for resource in resources:
746
        resource_d[resource.name] = resource
747

    
748
    found = resource_d.keys()
749
    nonex = [name for name in resource_names if name not in found]
750
    if nonex:
751
        raise ProjectBadRequest("Malformed resource policies")
752

    
753
    pols = []
754
    for resource_name, specs in policies.iteritems():
755
        p_capacity = specs.get("project_capacity")
756
        m_capacity = specs.get("member_capacity")
757

    
758
        if p_capacity is not None and not isinstance(p_capacity, (int, long)):
759
            raise ProjectBadRequest("Malformed resource policies")
760
        if not isinstance(m_capacity, (int, long)):
761
            raise ProjectBadRequest("Malformed resource policies")
762
        pols.append((resource_d[resource_name], m_capacity, p_capacity))
763
    return pols
764

    
765

    
766
def set_resource_policies(application, policies):
767
    for resource, m_capacity, p_capacity in policies:
768
        g = application.projectresourcegrant_set
769
        g.create(resource=resource,
770
                 member_capacity=m_capacity,
771
                 project_capacity=p_capacity)
772

    
773

    
774
def cancel_application(application_id, request_user=None, reason=""):
775
    get_project_of_application_for_update(application_id)
776
    application = get_application(application_id)
777
    app_check_allowed(application, request_user, level=APPLICANT_LEVEL)
778

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

    
784
    qh_release_pending_app(application.owner)
785

    
786
    application.cancel(actor=request_user, reason=reason)
787
    logger.info("%s has been cancelled." % (application.log_display))
788

    
789

    
790
def dismiss_application(application_id, request_user=None, reason=""):
791
    get_project_of_application_for_update(application_id)
792
    application = get_application(application_id)
793
    app_check_allowed(application, request_user, level=APPLICANT_LEVEL)
794

    
795
    if not application.can_dismiss():
796
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
797
              (application.id, application.state_display()))
798
        raise ProjectConflict(m)
799

    
800
    application.dismiss(actor=request_user, reason=reason)
801
    logger.info("%s has been dismissed." % (application.log_display))
802

    
803

    
804
def deny_application(application_id, request_user=None, reason=""):
805
    get_project_of_application_for_update(application_id)
806
    application = get_application(application_id)
807

    
808
    app_check_allowed(application, request_user, level=ADMIN_LEVEL)
809

    
810
    if not application.can_deny():
811
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
812
              (application.id, application.state_display()))
813
        raise ProjectConflict(m)
814

    
815
    qh_release_pending_app(application.owner)
816

    
817
    application.deny(actor=request_user, reason=reason)
818
    logger.info("%s has been denied with reason \"%s\"." %
819
                (application.log_display, reason))
820
    application_deny_notify(application)
821

    
822

    
823
def check_conflicting_projects(application):
824
    project = application.chain
825
    new_project_name = application.name
826
    try:
827
        q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
828
        conflicting_project = Project.objects.get(q)
829
        if (conflicting_project != project):
830
            m = (_("cannot approve: project with name '%s' "
831
                   "already exists (id: %s)") %
832
                 (new_project_name, conflicting_project.id))
833
            raise ProjectConflict(m)  # invalid argument
834
    except Project.DoesNotExist:
835
        pass
836

    
837

    
838
def approve_application(app_id, request_user=None, reason=""):
839
    get_project_lock()
840
    project = get_project_of_application_for_update(app_id)
841
    application = get_application(app_id)
842

    
843
    app_check_allowed(application, request_user, level=ADMIN_LEVEL)
844

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

    
850
    check_conflicting_projects(application)
851

    
852
    # Pre-lock members and owner together in order to impose an ordering
853
    # on locking users
854
    members = members_to_sync(project)
855
    uids_to_sync = [member.id for member in members]
856
    owner = application.owner
857
    uids_to_sync.append(owner.id)
858
    get_users_for_update(uids_to_sync)
859

    
860
    qh_release_pending_app(owner, locked=True)
861
    application.approve(actor=request_user, reason=reason)
862
    project.application = application
863
    project.name = application.name
864
    project.save()
865
    if project.is_deactivated():
866
        project.resume(actor=request_user, reason="APPROVE")
867
    qh_sync_locked_users(members)
868
    logger.info("%s has been approved." % (application.log_display))
869
    application_approve_notify(application)
870

    
871

    
872
def check_expiration(execute=False):
873
    objects = Project.objects
874
    expired = objects.expired_projects()
875
    if execute:
876
        for project in expired:
877
            terminate(project.pk)
878

    
879
    return [project.expiration_info() for project in expired]
880

    
881

    
882
def terminate(project_id, request_user=None, reason=None):
883
    project = get_project_for_update(project_id)
884
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
885
    checkAlive(project)
886

    
887
    project.terminate(actor=request_user, reason=reason)
888
    qh_sync_project(project)
889
    logger.info("%s has been terminated." % (project))
890

    
891
    project_termination_notify(project)
892

    
893

    
894
def suspend(project_id, request_user=None, reason=None):
895
    project = get_project_for_update(project_id)
896
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
897
    checkAlive(project)
898

    
899
    project.suspend(actor=request_user, reason=reason)
900
    qh_sync_project(project)
901
    logger.info("%s has been suspended." % (project))
902

    
903
    project_suspension_notify(project)
904

    
905

    
906
def unsuspend(project_id, request_user=None, reason=None):
907
    project = get_project_for_update(project_id)
908
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
909

    
910
    if not project.is_suspended:
911
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.id
912
        raise ProjectConflict(m)
913

    
914
    project.resume(actor=request_user, reason=reason)
915
    qh_sync_project(project)
916
    logger.info("%s has been unsuspended." % (project))
917
    project_unsuspension_notify(project)
918

    
919

    
920
def reinstate(project_id, request_user=None, reason=None):
921
    get_project_lock()
922
    project = get_project_for_update(project_id)
923
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
924

    
925
    if not project.is_terminated:
926
        m = _(astakos_messages.NOT_TERMINATED_PROJECT) % project.id
927
        raise ProjectConflict(m)
928

    
929
    check_conflicting_projects(project.application)
930
    project.resume(actor=request_user, reason=reason)
931
    qh_sync_project(project)
932
    logger.info("%s has been reinstated" % (project))
933
    project_reinstatement_notify(project)
934

    
935

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

    
945

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

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

    
958

    
959
def get_pending_app_diff(user, project):
960
    if project is None:
961
        diff = 1
962
    else:
963
        objs = ProjectApplication.objects
964
        q = objs.filter(chain=project, state=ProjectApplication.PENDING)
965
        count = q.count()
966
        diff = 1 - count
967
    return diff
968

    
969

    
970
def qh_add_pending_app(user, project=None, force=False):
971
    user = AstakosUser.objects.select_for_update().get(id=user.id)
972
    diff = get_pending_app_diff(user, project)
973
    return register_pending_apps(user, diff, force)
974

    
975

    
976
def check_pending_app_quota(user, project=None):
977
    diff = get_pending_app_diff(user, project)
978
    quota = get_pending_app_quota(user)
979
    limit = quota['limit']
980
    usage = quota['usage']
981
    if usage + diff > limit:
982
        return False, limit
983
    return True, None
984

    
985

    
986
def qh_release_pending_app(user, locked=False):
987
    if not locked:
988
        user = AstakosUser.objects.select_for_update().get(id=user.id)
989
    register_pending_apps(user, -1)