Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ b2369828

History | View | Annotate | Download (30.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
from datetime import datetime
36

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

    
44
from synnefo_branding.utils import render_to_string
45

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

    
59
import astakos.im.messages as astakos_messages
60

    
61
logger = logging.getLogger(__name__)
62

    
63

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

    
73

    
74
def logout(request, *args, **kwargs):
75
    user = request.user
76
    auth_logout(request, *args, **kwargs)
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' % user.email
159
    logger.log(settings.LOGGING_LEVEL, msg)
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' % invitation
179
    logger.log(settings.LOGGING_LEVEL, msg)
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' % user.log_display
203
    logger.log(settings.LOGGING_LEVEL, msg)
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' % user.log_display
217
    logger.log(settings.LOGGING_LEVEL, msg)
218

    
219

    
220
def send_change_email(
221
    ec, request, email_template_name='registration/email_change_email.txt'
222
):
223
    url = ec.get_url()
224
    url = request.build_absolute_uri(url)
225
    c = {'url': url, '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' % ec.user.log_display
234
    logger.log(settings.LOGGING_LEVEL, msg)
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
AUTO_ACCEPT_POLICY = 1
248
MODERATED_POLICY = 2
249
CLOSED_POLICY = 3
250

    
251
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
252

    
253

    
254
def get_related_project_id(application_id):
255
    try:
256
        app = ProjectApplication.objects.get(id=application_id)
257
        return app.chain_id
258
    except ProjectApplication.DoesNotExist:
259
        return None
260

    
261

    
262
def get_project_by_id(project_id):
263
    try:
264
        return Project.objects.get(id=project_id)
265
    except Project.DoesNotExist:
266
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
267
        raise IOError(m)
268

    
269

    
270
def get_project_for_update(project_id):
271
    try:
272
        return Project.objects.get_for_update(id=project_id)
273
    except Project.DoesNotExist:
274
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
275
        raise IOError(m)
276

    
277

    
278
def get_project_of_application_for_update(app_id):
279
    app = get_application(app_id)
280
    return get_project_for_update(app.chain_id)
281

    
282

    
283
def get_application(application_id):
284
    try:
285
        return ProjectApplication.objects.get(id=application_id)
286
    except ProjectApplication.DoesNotExist:
287
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % application_id
288
        raise IOError(m)
289

    
290

    
291
def get_project_of_membership_for_update(memb_id):
292
    m = get_membership_by_id(memb_id)
293
    return get_project_for_update(m.project_id)
294

    
295

    
296
def get_user_by_id(user_id):
297
    try:
298
        return AstakosUser.objects.get(id=user_id)
299
    except AstakosUser.DoesNotExist:
300
        m = _(astakos_messages.UNKNOWN_USER_ID) % user_id
301
        raise IOError(m)
302

    
303

    
304
def get_user_by_uuid(uuid):
305
    try:
306
        return AstakosUser.objects.get(uuid=uuid)
307
    except AstakosUser.DoesNotExist:
308
        m = _(astakos_messages.UNKNOWN_USER_ID) % uuid
309
        raise IOError(m)
310

    
311

    
312
def get_membership(project_id, user_id):
313
    try:
314
        objs = ProjectMembership.objects.select_related('project', 'person')
315
        return objs.get(project__id=project_id, person__id=user_id)
316
    except ProjectMembership.DoesNotExist:
317
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
318
        raise IOError(m)
319

    
320

    
321
def get_membership_by_id(memb_id):
322
    try:
323
        objs = ProjectMembership.objects.select_related('project', 'person')
324
        return objs.get(id=memb_id)
325
    except ProjectMembership.DoesNotExist:
326
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
327
        raise IOError(m)
328

    
329

    
330
def checkAllowed(entity, request_user, admin_only=False):
331
    if isinstance(entity, Project):
332
        application = entity.application
333
    elif isinstance(entity, ProjectApplication):
334
        application = entity
335
    else:
336
        m = "%s not a Project nor a ProjectApplication" % (entity,)
337
        raise ValueError(m)
338

    
339
    if not request_user or request_user.is_project_admin():
340
        return
341

    
342
    if not admin_only and application.owner == request_user:
343
        return
344

    
345
    m = _(astakos_messages.NOT_ALLOWED)
346
    raise PermissionDenied(m)
347

    
348

    
349
def checkAlive(project):
350
    if not project.is_alive:
351
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.id
352
        raise PermissionDenied(m)
353

    
354

    
355
def accept_membership_project_checks(project, request_user):
356
    checkAllowed(project, request_user)
357
    checkAlive(project)
358

    
359
    join_policy = project.application.member_join_policy
360
    if join_policy == CLOSED_POLICY:
361
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
362
        raise PermissionDenied(m)
363

    
364
    if project.violates_members_limit(adding=1):
365
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
366
        raise PermissionDenied(m)
367

    
368

    
369
def accept_membership_checks(membership, request_user):
370
    if not membership.can_accept():
371
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
372
        raise PermissionDenied(m)
373

    
374
    project = membership.project
375
    accept_membership_project_checks(project, request_user)
376

    
377

    
378
def accept_membership(memb_id, request_user=None):
379
    project = get_project_of_membership_for_update(memb_id)
380
    membership = get_membership_by_id(memb_id)
381
    accept_membership_checks(membership, request_user)
382
    user = membership.person
383
    membership.accept()
384
    qh_sync_user(user)
385
    logger.info("User %s has been accepted in %s." %
386
                (user.log_display, project))
387

    
388
    membership_change_notify(project, user, 'accepted')
389
    return membership
390

    
391

    
392
def reject_membership_checks(membership, request_user):
393
    if not membership.can_reject():
394
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
395
        raise PermissionDenied(m)
396

    
397
    project = membership.project
398
    checkAllowed(project, request_user)
399
    checkAlive(project)
400

    
401

    
402
def reject_membership(memb_id, request_user=None):
403
    project = get_project_of_membership_for_update(memb_id)
404
    membership = get_membership_by_id(memb_id)
405
    reject_membership_checks(membership, request_user)
406
    user = membership.person
407
    membership.reject()
408
    logger.info("Request of user %s for %s has been rejected." %
409
                (user.log_display, project))
410

    
411
    membership_change_notify(project, user, 'rejected')
412
    return membership
413

    
414

    
415
def cancel_membership_checks(membership, request_user):
416
    if not membership.can_cancel():
417
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
418
        raise PermissionDenied(m)
419

    
420
    if membership.person != request_user:
421
        raise PermissionDenied()
422

    
423
    project = membership.project
424
    checkAlive(project)
425

    
426

    
427
def cancel_membership(memb_id, request_user):
428
    project = get_project_of_membership_for_update(memb_id)
429
    membership = get_membership_by_id(memb_id)
430
    cancel_membership_checks(membership, request_user)
431
    membership.cancel()
432
    logger.info("Request of user %s for %s has been cancelled." %
433
                (membership.person.log_display, project))
434

    
435

    
436
def remove_membership_checks(membership, request_user=None):
437
    if not membership.can_remove():
438
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
439
        raise PermissionDenied(m)
440

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

    
445
    leave_policy = project.application.member_leave_policy
446
    if leave_policy == CLOSED_POLICY:
447
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
448
        raise PermissionDenied(m)
449

    
450

    
451
def remove_membership(memb_id, request_user=None):
452
    project = get_project_of_membership_for_update(memb_id)
453
    membership = get_membership_by_id(memb_id)
454
    remove_membership_checks(membership, request_user)
455
    user = membership.person
456
    membership.remove()
457
    qh_sync_user(user)
458
    logger.info("User %s has been removed from %s." %
459
                (user.log_display, project))
460

    
461
    membership_change_notify(project, user, 'removed')
462
    return membership
463

    
464

    
465
def enroll_member(project_id, user, request_user=None):
466
    project = get_project_for_update(project_id)
467
    accept_membership_project_checks(project, request_user)
468

    
469
    try:
470
        membership = get_membership(project_id, user.id)
471
        if not membership.can_enroll():
472
            m = _(astakos_messages.MEMBERSHIP_ACCEPTED)
473
            raise PermissionDenied(m)
474
        membership.join()
475
    except IOError:
476
        membership = new_membership(project, user)
477

    
478
    membership.accept()
479
    qh_sync_user(user)
480
    logger.info("User %s has been enrolled in %s." %
481
                (membership.person.log_display, project))
482

    
483
    membership_enroll_notify(project, membership.person)
484
    return membership
485

    
486

    
487
def leave_project_checks(membership, request_user):
488
    if not membership.can_leave():
489
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
490
        raise PermissionDenied(m)
491

    
492
    if membership.person != request_user:
493
        raise PermissionDenied()
494

    
495
    project = membership.project
496
    checkAlive(project)
497

    
498
    leave_policy = project.application.member_leave_policy
499
    if leave_policy == CLOSED_POLICY:
500
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
501
        raise PermissionDenied(m)
502

    
503

    
504
def can_leave_request(project, user):
505
    m = user.get_membership(project)
506
    if m is None:
507
        return False
508
    try:
509
        leave_project_checks(m, user)
510
    except PermissionDenied:
511
        return False
512
    return True
513

    
514

    
515
def leave_project(memb_id, request_user):
516
    project = get_project_of_membership_for_update(memb_id)
517
    membership = get_membership_by_id(memb_id)
518
    leave_project_checks(membership, request_user)
519

    
520
    auto_accepted = False
521
    leave_policy = project.application.member_leave_policy
522
    if leave_policy == AUTO_ACCEPT_POLICY:
523
        membership.remove()
524
        qh_sync_user(request_user)
525
        logger.info("User %s has left %s." %
526
                    (request_user.log_display, project))
527
        auto_accepted = True
528
    else:
529
        membership.leave_request()
530
        logger.info("User %s requested to leave %s." %
531
                    (request_user.log_display, project))
532
        membership_leave_request_notify(project, membership.person)
533
    return auto_accepted
534

    
535

    
536
def join_project_checks(project):
537
    checkAlive(project)
538

    
539
    join_policy = project.application.member_join_policy
540
    if join_policy == CLOSED_POLICY:
541
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
542
        raise PermissionDenied(m)
543

    
544

    
545
def can_join_request(project, user):
546
    try:
547
        join_project_checks(project)
548
    except PermissionDenied:
549
        return False
550

    
551
    m = user.get_membership(project)
552
    if not m:
553
        return True
554
    return m.can_join()
555

    
556

    
557
def new_membership(project, user):
558
    m = ProjectMembership.objects.create(project=project, person=user)
559
    m._log_create(None, ProjectMembership.REQUESTED)
560
    return m
561

    
562

    
563
def join_project(project_id, request_user):
564
    project = get_project_for_update(project_id)
565
    join_project_checks(project)
566

    
567
    try:
568
        membership = get_membership(project.id, request_user.id)
569
        if not membership.can_join():
570
            msg = _(astakos_messages.MEMBERSHIP_ASSOCIATED)
571
            raise PermissionDenied(msg)
572
        membership.join()
573
    except IOError:
574
        membership = new_membership(project, request_user)
575

    
576
    auto_accepted = False
577
    join_policy = project.application.member_join_policy
578
    if (join_policy == AUTO_ACCEPT_POLICY and (
579
            not project.violates_members_limit(adding=1))):
580
        membership.accept()
581
        qh_sync_user(request_user)
582
        logger.info("User %s joined %s." %
583
                    (request_user.log_display, project))
584
        auto_accepted = True
585
    else:
586
        membership_request_notify(project, membership.person)
587
        logger.info("User %s requested to join %s." %
588
                    (request_user.log_display, project))
589
    return auto_accepted
590

    
591

    
592
MEMBERSHIP_ACTION_CHECKS = {
593
    "leave":  leave_project_checks,
594
    "cancel": cancel_membership_checks,
595
    "accept": accept_membership_checks,
596
    "reject": reject_membership_checks,
597
    "remove": remove_membership_checks,
598
}
599

    
600

    
601
def membership_allowed_actions(membership, request_user):
602
    allowed = []
603
    for action, check in MEMBERSHIP_ACTION_CHECKS.iteritems():
604
        try:
605
            check(membership, request_user)
606
            allowed.append(action)
607
        except PermissionDenied:
608
            pass
609
    return allowed
610

    
611

    
612
def submit_application(owner=None,
613
                       name=None,
614
                       project_id=None,
615
                       homepage=None,
616
                       description=None,
617
                       start_date=None,
618
                       end_date=None,
619
                       member_join_policy=None,
620
                       member_leave_policy=None,
621
                       limit_on_members_number=None,
622
                       comments=None,
623
                       resources=None,
624
                       request_user=None):
625

    
626
    project = None
627
    if project_id is not None:
628
        project = get_project_for_update(project_id)
629

    
630
        if (request_user and
631
            (not project.application.owner == request_user and
632
             not request_user.is_superuser
633
             and not request_user.is_project_admin())):
634
            m = _(astakos_messages.NOT_ALLOWED)
635
            raise PermissionDenied(m)
636

    
637
    policies = validate_resource_policies(resources)
638

    
639
    force = request_user.is_project_admin()
640
    ok, limit = qh_add_pending_app(owner, project, force)
641
    if not ok:
642
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
643
        raise PermissionDenied(m)
644

    
645
    application = ProjectApplication(
646
        applicant=request_user,
647
        owner=owner,
648
        name=name,
649
        homepage=homepage,
650
        description=description,
651
        start_date=start_date,
652
        end_date=end_date,
653
        member_join_policy=member_join_policy,
654
        member_leave_policy=member_leave_policy,
655
        limit_on_members_number=limit_on_members_number,
656
        comments=comments)
657

    
658
    if project is None:
659
        chain = new_chain()
660
        application.chain_id = chain.chain
661
        application.save()
662
        Project.objects.create(id=chain.chain, application=application)
663
    else:
664
        application.chain = project
665
        application.save()
666
        if project.application.state != ProjectApplication.APPROVED:
667
            project.application = application
668
            project.save()
669

    
670
        pending = ProjectApplication.objects.filter(
671
            chain=project,
672
            state=ProjectApplication.PENDING).exclude(id=application.id)
673
        for app in pending:
674
            app.state = ProjectApplication.REPLACED
675
            app.save()
676

    
677
    if policies is not None:
678
        set_resource_policies(application, policies)
679
    logger.info("User %s submitted %s." %
680
                (request_user.log_display, application.log_display))
681
    application_submit_notify(application)
682
    return application
683

    
684

    
685
def validate_resource_policies(policies):
686
    if not isinstance(policies, dict):
687
        raise ProjectBadRequest("Malformed resource policies")
688

    
689
    resource_names = policies.keys()
690
    resources = Resource.objects.filter(name__in=resource_names)
691
    resource_d = {}
692
    for resource in resources:
693
        resource_d[resource.name] = resource
694

    
695
    found = resource_d.keys()
696
    nonex = [name for name in resource_names if name not in found]
697
    if nonex:
698
        raise ValueError("Malformed resource policies")
699

    
700
    pols = []
701
    for resource_name, specs in policies.iteritems():
702
        p_capacity = specs.get("project_capacity")
703
        m_capacity = specs.get("member_capacity")
704

    
705
        if p_capacity is not None and not isinstance(p_capacity, (int, long)):
706
            raise ValueError("Malformed resource policies")
707
        if not isinstance(m_capacity, (int, long)):
708
            raise ValueError("Malformed resource policies")
709
        pols.append((resource_d[resource_name], m_capacity, p_capacity))
710
    return pols
711

    
712

    
713
def set_resource_policies(application, policies):
714
    for resource, m_capacity, p_capacity in policies:
715
        g = application.projectresourcegrant_set
716
        g.create(resource=resource,
717
                 member_capacity=m_capacity,
718
                 project_capacity=p_capacity)
719

    
720

    
721
def cancel_application(application_id, request_user=None, reason=""):
722
    get_project_of_application_for_update(application_id)
723
    application = get_application(application_id)
724
    checkAllowed(application, request_user)
725

    
726
    if not application.can_cancel():
727
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
728
              (application.id, application.state_display()))
729
        raise PermissionDenied(m)
730

    
731
    qh_release_pending_app(application.owner)
732

    
733
    application.cancel()
734
    logger.info("%s has been cancelled." % (application.log_display))
735

    
736

    
737
def dismiss_application(application_id, request_user=None, reason=""):
738
    get_project_of_application_for_update(application_id)
739
    application = get_application(application_id)
740
    checkAllowed(application, request_user)
741

    
742
    if not application.can_dismiss():
743
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
744
              (application.id, application.state_display()))
745
        raise PermissionDenied(m)
746

    
747
    application.dismiss()
748
    logger.info("%s has been dismissed." % (application.log_display))
749

    
750

    
751
def deny_application(application_id, request_user=None, reason=""):
752
    get_project_of_application_for_update(application_id)
753
    application = get_application(application_id)
754

    
755
    checkAllowed(application, request_user, admin_only=True)
756

    
757
    if not application.can_deny():
758
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
759
              (application.id, application.state_display()))
760
        raise PermissionDenied(m)
761

    
762
    qh_release_pending_app(application.owner)
763

    
764
    application.deny(reason)
765
    logger.info("%s has been denied with reason \"%s\"." %
766
                (application.log_display, reason))
767
    application_deny_notify(application)
768

    
769

    
770
def check_conflicting_projects(application):
771
    project = application.chain
772
    new_project_name = application.name
773
    try:
774
        q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
775
        conflicting_project = Project.objects.get(q)
776
        if (conflicting_project != project):
777
            m = (_("cannot approve: project with name '%s' "
778
                   "already exists (id: %s)") %
779
                 (new_project_name, conflicting_project.id))
780
            raise PermissionDenied(m)  # invalid argument
781
    except Project.DoesNotExist:
782
        pass
783

    
784

    
785
def approve_application(app_id, request_user=None, reason=""):
786
    project = get_project_of_application_for_update(app_id)
787
    application = get_application(app_id)
788

    
789
    checkAllowed(application, request_user, admin_only=True)
790

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

    
796
    check_conflicting_projects(application)
797

    
798
    # Pre-lock members and owner together in order to impose an ordering
799
    # on locking users
800
    members = members_to_sync(project)
801
    uids_to_sync = [member.id for member in members]
802
    owner = application.owner
803
    uids_to_sync.append(owner.id)
804
    get_users_for_update(uids_to_sync)
805

    
806
    qh_release_pending_app(owner, locked=True)
807
    application.approve(reason)
808
    project.application = application
809
    project.name = application.name
810
    project.save()
811
    if project.is_deactivated():
812
        project.resume()
813
    qh_sync_locked_users(members)
814
    logger.info("%s has been approved." % (application.log_display))
815
    application_approve_notify(application)
816

    
817

    
818
def check_expiration(execute=False):
819
    objects = Project.objects
820
    expired = objects.expired_projects()
821
    if execute:
822
        for project in expired:
823
            terminate(project.pk)
824

    
825
    return [project.expiration_info() for project in expired]
826

    
827

    
828
def terminate(project_id, request_user=None):
829
    project = get_project_for_update(project_id)
830
    checkAllowed(project, request_user, admin_only=True)
831
    checkAlive(project)
832

    
833
    project.terminate()
834
    qh_sync_project(project)
835
    logger.info("%s has been terminated." % (project))
836

    
837
    project_termination_notify(project)
838

    
839

    
840
def suspend(project_id, request_user=None):
841
    project = get_project_for_update(project_id)
842
    checkAllowed(project, request_user, admin_only=True)
843
    checkAlive(project)
844

    
845
    project.suspend()
846
    qh_sync_project(project)
847
    logger.info("%s has been suspended." % (project))
848

    
849
    project_suspension_notify(project)
850

    
851

    
852
def resume(project_id, request_user=None):
853
    project = get_project_for_update(project_id)
854
    checkAllowed(project, request_user, admin_only=True)
855

    
856
    if not project.is_suspended:
857
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.id
858
        raise PermissionDenied(m)
859

    
860
    project.resume()
861
    qh_sync_project(project)
862
    logger.info("%s has been unsuspended." % (project))
863

    
864

    
865
def _partition_by(f, l):
866
    d = {}
867
    for x in l:
868
        group = f(x)
869
        group_l = d.get(group, [])
870
        group_l.append(x)
871
        d[group] = group_l
872
    return d
873

    
874

    
875
def count_pending_app(users):
876
    users = list(users)
877
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
878
                                             owner__in=users)
879
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
880

    
881
    usage = {}
882
    for user in users:
883
        uuid = user.uuid
884
        usage[uuid] = len(apps_d.get(uuid, []))
885
    return usage
886

    
887

    
888
def get_pending_app_diff(user, project):
889
    if project is None:
890
        diff = 1
891
    else:
892
        objs = ProjectApplication.objects
893
        q = objs.filter(chain=project, state=ProjectApplication.PENDING)
894
        count = q.count()
895
        diff = 1 - count
896
    return diff
897

    
898

    
899
def qh_add_pending_app(user, project=None, force=False):
900
    user = AstakosUser.forupdate.get_for_update(id=user.id)
901
    diff = get_pending_app_diff(user, project)
902
    return register_pending_apps(user, diff, force)
903

    
904

    
905
def check_pending_app_quota(user, project=None):
906
    diff = get_pending_app_diff(user, project)
907
    quota = get_pending_app_quota(user)
908
    limit = quota['limit']
909
    usage = quota['usage']
910
    if usage + diff > limit:
911
        return False, limit
912
    return True, None
913

    
914

    
915
def qh_release_pending_app(user, locked=False):
916
    if not locked:
917
        user = AstakosUser.forupdate.get_for_update(id=user.id)
918
    register_pending_apps(user, -1)