Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 56bbece7

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
    logger.info('%s logged out.', user.log_display)
77

    
78

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

    
97

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

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

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

    
126

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

    
139

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

    
160

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

    
183

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

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

    
204

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

    
218

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

    
236

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

    
244

    
245
### PROJECT FUNCTIONS ###
246

    
247

    
248
class ProjectError(Exception):
249
    pass
250

    
251

    
252
class ProjectNotFound(ProjectError):
253
    pass
254

    
255

    
256
class ProjectForbidden(ProjectError):
257
    pass
258

    
259

    
260
class ProjectBadRequest(ProjectError):
261
    pass
262

    
263

    
264
class ProjectConflict(ProjectError):
265
    pass
266

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

    
271
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
272

    
273

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

    
281

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

    
291

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

    
299

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

    
304

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

    
308

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

    
316

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

    
321

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

    
329

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

    
338

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

    
347

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

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

    
361

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

    
366
    if silent:
367
        return False
368

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

    
372

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

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

    
380

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

    
385

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

    
391

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

    
397

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

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

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

    
411

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

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

    
420

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

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

    
434

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

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

    
444

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

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

    
457

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

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

    
467

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

    
476

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

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

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

    
491

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

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

    
505

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

    
513

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

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

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

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

    
538

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

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

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

    
553

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

    
564

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

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

    
586

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

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

    
595

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

    
598

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

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

    
611

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

    
620

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

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

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

    
648

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

    
657

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

    
668

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

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

    
688
    policies = validate_resource_policies(resources)
689

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

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

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

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

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

    
735

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

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

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

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

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

    
764

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

    
772

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

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

    
783
    qh_release_pending_app(application.owner)
784

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

    
788

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

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

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

    
802

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

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

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

    
814
    qh_release_pending_app(application.owner)
815

    
816
    application.deny(actor=request_user, reason=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 check_conflicting_projects(application):
823
    project = application.chain
824
    new_project_name = application.name
825
    try:
826
        q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
827
        conflicting_project = Project.objects.get(q)
828
        if (conflicting_project != project):
829
            m = (_("cannot approve: project with name '%s' "
830
                   "already exists (id: %s)") %
831
                 (new_project_name, conflicting_project.id))
832
            raise ProjectConflict(m)  # invalid argument
833
    except Project.DoesNotExist:
834
        pass
835

    
836

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

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

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

    
849
    check_conflicting_projects(application)
850

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

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

    
870

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

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

    
880

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

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

    
890
    project_termination_notify(project)
891

    
892

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

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

    
902
    project_suspension_notify(project)
903

    
904

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

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

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

    
918

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

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

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

    
934

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

    
944

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

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

    
957

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

    
968

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

    
974

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

    
984

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