Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 8998f09a

History | View | Annotate | Download (28.6 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.core.exceptions import PermissionDenied
41
from django.db.models import Q
42
from django.http import Http404
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, Chain, new_chain
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.BASEURL, user.get_activation_url(nxt=reverse('index')))
85
    message = render_to_string(template_name, {
86
                               'user': user,
87
                               'url': url,
88
                               'baseurl': settings.BASEURL,
89
                               'site_name': settings.SITENAME,
90
                               'support': settings.CONTACT_EMAIL})
91
    sender = settings.SERVER_EMAIL
92
    send_mail(_(settings.VERIFICATION_EMAIL_SUBJECT), message, sender, [user.email],
93
              connection=get_connection())
94
    logger.info("Sent user verirfication email: %s", user.log_display)
95

    
96

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

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

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

    
125

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

    
137

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

    
157

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

    
179

    
180
def send_greeting(user, email_template_name='im/welcome_email.txt'):
181
    """
182
    Send welcome email to an accepted/activated user.
183

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

    
199

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

    
213

    
214
def send_change_email(
215
    ec, request, email_template_name='registration/email_change_email.txt'):
216
    url = ec.get_url()
217
    url = request.build_absolute_uri(url)
218
    c = {'url': url, 'site_name': settings.SITENAME, 'support': settings.CONTACT_EMAIL,
219
         'ec': ec}
220
    message = render_to_string(email_template_name, c)
221
    from_email = settings.SERVER_EMAIL
222
    send_mail(_(settings.EMAIL_CHANGE_EMAIL_SUBJECT), message, from_email,
223
              [ec.new_email_address], connection=get_connection())
224
    msg = 'Sent change email for %s' % ec.user.log_display
225
    logger.log(settings.LOGGING_LEVEL, msg)
226

    
227

    
228
def invite(inviter, email, realname):
229
    inv = Invitation(inviter=inviter, username=email, realname=realname)
230
    inv.save()
231
    send_invitation(inv)
232
    inviter.invitations = max(0, inviter.invitations - 1)
233
    inviter.save()
234

    
235

    
236
### PROJECT FUNCTIONS ###
237

    
238
AUTO_ACCEPT_POLICY = 1
239
MODERATED_POLICY = 2
240
CLOSED_POLICY = 3
241

    
242
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
243

    
244

    
245
def get_project_by_application_id(project_application_id):
246
    try:
247
        return Project.objects.get(application__id=project_application_id)
248
    except Project.DoesNotExist:
249
        m = (_(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) %
250
             project_application_id)
251
        raise IOError(m)
252

    
253

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

    
263

    
264
def get_chain_of_application_id(application_id):
265
    try:
266
        app = ProjectApplication.objects.get(id=application_id)
267
        chain = app.chain
268
        return chain.chain
269
    except ProjectApplication.DoesNotExist:
270
        return None
271

    
272

    
273
def get_project_by_id(project_id):
274
    try:
275
        return Project.objects.get(id=project_id)
276
    except Project.DoesNotExist:
277
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
278
        raise IOError(m)
279

    
280

    
281
def get_project_by_name(name):
282
    try:
283
        return Project.objects.get(name=name)
284
    except Project.DoesNotExist:
285
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % name
286
        raise IOError(m)
287

    
288

    
289
def get_chain_for_update(chain_id):
290
    try:
291
        return Chain.objects.get_for_update(chain=chain_id)
292
    except Chain.DoesNotExist:
293
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % chain_id
294
        raise IOError(m)
295

    
296

    
297
def get_chain_of_application_for_update(app_id):
298
    app = get_application(app_id)
299
    return Chain.objects.get_for_update(chain=app.chain_id)
300

    
301

    
302
def get_application(application_id):
303
    try:
304
        return ProjectApplication.objects.get(id=application_id)
305
    except ProjectApplication.DoesNotExist:
306
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % application_id
307
        raise IOError(m)
308

    
309

    
310
def get_user_by_id(user_id):
311
    try:
312
        return AstakosUser.objects.get(id=user_id)
313
    except AstakosUser.DoesNotExist:
314
        m = _(astakos_messages.UNKNOWN_USER_ID) % user_id
315
        raise IOError(m)
316

    
317

    
318
def get_user_by_uuid(uuid):
319
    try:
320
        return AstakosUser.objects.get(uuid=uuid)
321
    except AstakosUser.DoesNotExist:
322
        m = _(astakos_messages.UNKNOWN_USER_ID) % uuid
323
        raise IOError(m)
324

    
325

    
326
def get_membership(project_id, user_id):
327
    try:
328
        objs = ProjectMembership.objects.select_related('project', 'person')
329
        return objs.get(project__id=project_id, person__id=user_id)
330
    except ProjectMembership.DoesNotExist:
331
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
332
        raise IOError(m)
333

    
334

    
335
def get_membership_by_id(project_id, memb_id):
336
    try:
337
        objs = ProjectMembership.objects.select_related('project', 'person')
338
        return objs.get(project__id=project_id, id=memb_id)
339
    except ProjectMembership.DoesNotExist:
340
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
341
        raise IOError(m)
342

    
343

    
344
def checkAllowed(entity, request_user, admin_only=False):
345
    if isinstance(entity, Project):
346
        application = entity.application
347
    elif isinstance(entity, ProjectApplication):
348
        application = entity
349
    else:
350
        m = "%s not a Project nor a ProjectApplication" % (entity,)
351
        raise ValueError(m)
352

    
353
    if not request_user or request_user.is_project_admin():
354
        return
355

    
356
    if not admin_only and application.owner == request_user:
357
        return
358

    
359
    m = _(astakos_messages.NOT_ALLOWED)
360
    raise PermissionDenied(m)
361

    
362

    
363
def checkAlive(project):
364
    if not project.is_alive:
365
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__
366
        raise PermissionDenied(m)
367

    
368

    
369
def accept_membership_checks(project, request_user):
370
    checkAllowed(project, request_user)
371
    checkAlive(project)
372

    
373
    join_policy = project.application.member_join_policy
374
    if join_policy == CLOSED_POLICY:
375
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
376
        raise PermissionDenied(m)
377

    
378
    if project.violates_members_limit(adding=1):
379
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
380
        raise PermissionDenied(m)
381

    
382

    
383
def accept_membership(project_id, memb_id, request_user=None):
384
    get_chain_for_update(project_id)
385

    
386
    membership = get_membership_by_id(project_id, memb_id)
387
    if not membership.can_accept():
388
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
389
        raise PermissionDenied(m)
390

    
391
    project = membership.project
392
    accept_membership_checks(project, request_user)
393
    user = membership.person
394
    membership.accept()
395
    qh_sync_user(user)
396
    logger.info("User %s has been accepted in %s." %
397
                (user.log_display, project))
398

    
399
    membership_change_notify(project, user, 'accepted')
400
    return membership
401

    
402

    
403
def reject_membership_checks(project, request_user):
404
    checkAllowed(project, request_user)
405
    checkAlive(project)
406

    
407

    
408
def reject_membership(project_id, memb_id, request_user=None):
409
    get_chain_for_update(project_id)
410

    
411
    membership = get_membership_by_id(project_id, memb_id)
412
    if not membership.can_reject():
413
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
414
        raise PermissionDenied(m)
415

    
416
    project = membership.project
417
    reject_membership_checks(project, request_user)
418
    user = membership.person
419
    membership.reject()
420
    logger.info("Request of user %s for %s has been rejected." %
421
                (user.log_display, project))
422

    
423
    membership_change_notify(project, user, 'rejected')
424
    return membership
425

    
426

    
427
def cancel_membership_checks(project):
428
    checkAlive(project)
429

    
430

    
431
def cancel_membership(project_id, request_user):
432
    get_chain_for_update(project_id)
433

    
434
    membership = get_membership(project_id, request_user.id)
435
    if not membership.can_cancel():
436
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
437
        raise PermissionDenied(m)
438

    
439
    project = membership.project
440
    cancel_membership_checks(project)
441
    membership.cancel()
442
    logger.info("Request of user %s for %s has been cancelled." %
443
                (membership.person.log_display, project))
444

    
445

    
446
def remove_membership_checks(project, request_user=None):
447
    checkAllowed(project, request_user)
448
    checkAlive(project)
449

    
450
    leave_policy = project.application.member_leave_policy
451
    if leave_policy == CLOSED_POLICY:
452
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
453
        raise PermissionDenied(m)
454

    
455

    
456
def remove_membership(project_id, memb_id, request_user=None):
457
    get_chain_for_update(project_id)
458

    
459
    membership = get_membership_by_id(project_id, memb_id)
460
    if not membership.can_remove():
461
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
462
        raise PermissionDenied(m)
463

    
464
    project = membership.project
465
    remove_membership_checks(project, request_user)
466
    user = membership.person
467
    membership.remove()
468
    qh_sync_user(user)
469
    logger.info("User %s has been removed from %s." %
470
                (user.log_display, project))
471

    
472
    membership_change_notify(project, user, 'removed')
473
    return membership
474

    
475

    
476
def enroll_member(project_id, user, request_user=None):
477
    get_chain_for_update(project_id)
478
    project = get_project_by_id(project_id)
479
    accept_membership_checks(project, request_user)
480

    
481
    membership, created = ProjectMembership.objects.get_or_create(
482
        project=project,
483
        person=user)
484

    
485
    if not membership.can_accept():
486
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
487
        raise PermissionDenied(m)
488

    
489
    membership.accept()
490
    qh_sync_user(user)
491
    logger.info("User %s has been enrolled in %s." %
492
                (membership.person.log_display, project))
493

    
494
    membership_enroll_notify(project, membership.person)
495
    return membership
496

    
497

    
498
def leave_project_checks(project):
499
    checkAlive(project)
500

    
501
    leave_policy = project.application.member_leave_policy
502
    if leave_policy == CLOSED_POLICY:
503
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
504
        raise PermissionDenied(m)
505

    
506

    
507
def can_leave_request(project, user):
508
    leave_policy = project.application.member_leave_policy
509
    if leave_policy == CLOSED_POLICY:
510
        return False
511
    m = user.get_membership(project)
512
    if m is None:
513
        return False
514
    if m.state != ProjectMembership.ACCEPTED:
515
        return False
516
    return True
517

    
518

    
519
def leave_project(project_id, request_user):
520
    get_chain_for_update(project_id)
521

    
522
    membership = get_membership(project_id, request_user.id)
523
    if not membership.can_leave():
524
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
525
        raise PermissionDenied(m)
526

    
527
    project = membership.project
528
    leave_project_checks(project)
529

    
530
    auto_accepted = False
531
    leave_policy = project.application.member_leave_policy
532
    if leave_policy == AUTO_ACCEPT_POLICY:
533
        membership.remove()
534
        qh_sync_user(request_user)
535
        logger.info("User %s has left %s." %
536
                    (request_user.log_display, project))
537
        auto_accepted = True
538
    else:
539
        membership.leave_request()
540
        logger.info("User %s requested to leave %s." %
541
                    (request_user.log_display, project))
542
        membership_leave_request_notify(project, membership.person)
543
    return auto_accepted
544

    
545

    
546
def join_project_checks(project):
547
    checkAlive(project)
548

    
549
    join_policy = project.application.member_join_policy
550
    if join_policy == CLOSED_POLICY:
551
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
552
        raise PermissionDenied(m)
553

    
554

    
555
def can_join_request(project, user):
556
    join_policy = project.application.member_join_policy
557
    if join_policy == CLOSED_POLICY:
558
        return False
559
    m = user.get_membership(project)
560
    if m:
561
        return False
562
    return True
563

    
564

    
565
def join_project(project_id, request_user):
566
    get_chain_for_update(project_id)
567
    project = get_project_by_id(project_id)
568
    join_project_checks(project)
569

    
570
    membership, created = ProjectMembership.objects.get_or_create(
571
        project=project,
572
        person=request_user)
573

    
574
    if not created:
575
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
576
        raise PermissionDenied(msg)
577

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

    
593

    
594
def submit_application(owner=None,
595
                       name=None,
596
                       precursor_id=None,
597
                       homepage=None,
598
                       description=None,
599
                       start_date=None,
600
                       end_date=None,
601
                       member_join_policy=None,
602
                       member_leave_policy=None,
603
                       limit_on_members_number=None,
604
                       comments=None,
605
                       resource_policies=None,
606
                       request_user=None):
607

    
608
    precursor = None
609
    if precursor_id is not None:
610
        get_chain_of_application_for_update(precursor_id)
611
        precursor = ProjectApplication.objects.get(id=precursor_id)
612

    
613
        if (request_user and
614
            (not precursor.owner == request_user and
615
             not request_user.is_superuser
616
             and not request_user.is_project_admin())):
617
            m = _(astakos_messages.NOT_ALLOWED)
618
            raise PermissionDenied(m)
619

    
620
    force = request_user.is_project_admin()
621
    ok, limit = qh_add_pending_app(owner, precursor, force)
622
    if not ok:
623
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
624
        raise PermissionDenied(m)
625

    
626
    application = ProjectApplication(
627
        applicant=request_user,
628
        owner=owner,
629
        name=name,
630
        precursor_application_id=precursor_id,
631
        homepage=homepage,
632
        description=description,
633
        start_date=start_date,
634
        end_date=end_date,
635
        member_join_policy=member_join_policy,
636
        member_leave_policy=member_leave_policy,
637
        limit_on_members_number=limit_on_members_number,
638
        comments=comments)
639

    
640
    if precursor is None:
641
        application.chain = new_chain()
642
    else:
643
        chain = precursor.chain
644
        application.chain = chain
645
        objs = ProjectApplication.objects
646
        pending = objs.filter(chain=chain, state=ProjectApplication.PENDING)
647
        for app in pending:
648
            app.state = ProjectApplication.REPLACED
649
            app.save()
650

    
651
    application.save()
652
    if resource_policies is not None:
653
        application.set_resource_policies(resource_policies)
654
    logger.info("User %s submitted %s." %
655
                (request_user.log_display, application.log_display))
656
    application_submit_notify(application)
657
    return application
658

    
659

    
660
def cancel_application(application_id, request_user=None, reason=""):
661
    get_chain_of_application_for_update(application_id)
662
    application = get_application(application_id)
663
    checkAllowed(application, request_user)
664

    
665
    if not application.can_cancel():
666
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
667
              (application.id, application.state_display()))
668
        raise PermissionDenied(m)
669

    
670
    qh_release_pending_app(application.owner)
671

    
672
    application.cancel()
673
    logger.info("%s has been cancelled." % (application.log_display))
674

    
675

    
676
def dismiss_application(application_id, request_user=None, reason=""):
677
    get_chain_of_application_for_update(application_id)
678
    application = get_application(application_id)
679
    checkAllowed(application, request_user)
680

    
681
    if not application.can_dismiss():
682
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
683
              (application.id, application.state_display()))
684
        raise PermissionDenied(m)
685

    
686
    application.dismiss()
687
    logger.info("%s has been dismissed." % (application.log_display))
688

    
689

    
690
def deny_application(application_id, request_user=None, reason=""):
691
    get_chain_of_application_for_update(application_id)
692
    application = get_application(application_id)
693

    
694
    checkAllowed(application, request_user, admin_only=True)
695

    
696
    if not application.can_deny():
697
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
698
              (application.id, application.state_display()))
699
        raise PermissionDenied(m)
700

    
701
    qh_release_pending_app(application.owner)
702

    
703
    application.deny(reason)
704
    logger.info("%s has been denied with reason \"%s\"." %
705
                (application.log_display, reason))
706
    application_deny_notify(application)
707

    
708

    
709
def check_conflicting_projects(application):
710
    try:
711
        project = get_project_by_id(application.chain)
712
    except IOError:
713
        project = None
714

    
715
    new_project_name = application.name
716
    try:
717
        q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
718
        conflicting_project = Project.objects.get(q)
719
        if (conflicting_project != project):
720
            m = (_("cannot approve: project with name '%s' "
721
                   "already exists (id: %s)") % (
722
                    new_project_name, conflicting_project.id))
723
            raise PermissionDenied(m) # invalid argument
724
    except Project.DoesNotExist:
725
        pass
726

    
727
    return project
728

    
729

    
730
def approve_application(app_id, request_user=None, reason=""):
731
    get_chain_of_application_for_update(app_id)
732
    application = get_application(app_id)
733

    
734
    checkAllowed(application, request_user, admin_only=True)
735

    
736
    if not application.can_approve():
737
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
738
              (application.id, application.state_display()))
739
        raise PermissionDenied(m)
740

    
741
    project = check_conflicting_projects(application)
742

    
743
    # Pre-lock members and owner together in order to impose an ordering
744
    # on locking users
745
    members = members_to_sync(project) if project is not None else []
746
    uids_to_sync = [member.id for member in members]
747
    owner = application.owner
748
    uids_to_sync.append(owner.id)
749
    get_users_for_update(uids_to_sync)
750

    
751
    qh_release_pending_app(owner, locked=True)
752
    application.approve(reason)
753
    qh_sync_locked_users(members)
754
    logger.info("%s has been approved." % (application.log_display))
755
    application_approve_notify(application)
756

    
757

    
758
def check_expiration(execute=False):
759
    objects = Project.objects
760
    expired = objects.expired_projects()
761
    if execute:
762
        for project in expired:
763
            terminate(project.id)
764

    
765
    return [project.expiration_info() for project in expired]
766

    
767

    
768
def terminate(project_id, request_user=None):
769
    get_chain_for_update(project_id)
770
    project = get_project_by_id(project_id)
771
    checkAllowed(project, request_user, admin_only=True)
772
    checkAlive(project)
773

    
774
    project.terminate()
775
    qh_sync_project(project)
776
    logger.info("%s has been terminated." % (project))
777

    
778
    project_termination_notify(project)
779

    
780

    
781
def suspend(project_id, request_user=None):
782
    get_chain_for_update(project_id)
783
    project = get_project_by_id(project_id)
784
    checkAllowed(project, request_user, admin_only=True)
785
    checkAlive(project)
786

    
787
    project.suspend()
788
    qh_sync_project(project)
789
    logger.info("%s has been suspended." % (project))
790

    
791
    project_suspension_notify(project)
792

    
793

    
794
def resume(project_id, request_user=None):
795
    get_chain_for_update(project_id)
796
    project = get_project_by_id(project_id)
797
    checkAllowed(project, request_user, admin_only=True)
798

    
799
    if not project.is_suspended:
800
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
801
        raise PermissionDenied(m)
802

    
803
    project.resume()
804
    qh_sync_project(project)
805
    logger.info("%s has been unsuspended." % (project))
806

    
807

    
808
def get_by_chain_or_404(chain_id):
809
    try:
810
        project = Project.objects.get(id=chain_id)
811
        application = project.application
812
        return project, application
813
    except:
814
        application = ProjectApplication.objects.latest_of_chain(chain_id)
815
        if application is None:
816
            raise Http404
817
        else:
818
            return None, application
819

    
820

    
821
def _partition_by(f, l):
822
    d = {}
823
    for x in l:
824
        group = f(x)
825
        group_l = d.get(group, [])
826
        group_l.append(x)
827
        d[group] = group_l
828
    return d
829

    
830

    
831
def count_pending_app(users):
832
    users = list(users)
833
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
834
                                             owner__in=users)
835
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
836

    
837
    usage = {}
838
    for user in users:
839
        uuid = user.uuid
840
        usage[uuid] = len(apps_d.get(uuid, []))
841
    return usage
842

    
843

    
844
def get_pending_app_diff(user, precursor):
845
    if precursor is None:
846
        diff = 1
847
    else:
848
        chain = precursor.chain
849
        objs = ProjectApplication.objects
850
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
851
        count = q.count()
852
        diff = 1 - count
853
    return diff
854

    
855

    
856
def qh_add_pending_app(user, precursor=None, force=False):
857
    user = AstakosUser.forupdate.get_for_update(id=user.id)
858
    diff = get_pending_app_diff(user, precursor)
859
    return register_pending_apps(user, diff, force)
860

    
861

    
862
def check_pending_app_quota(user, precursor=None):
863
    diff = get_pending_app_diff(user, precursor)
864
    quota = get_pending_app_quota(user)
865
    limit = quota['limit']
866
    usage = quota['usage']
867
    if usage + diff > limit:
868
        return False, limit
869
    return True, None
870

    
871

    
872
def qh_release_pending_app(user, locked=False):
873
    if not locked:
874
        user = AstakosUser.forupdate.get_for_update(id=user.id)
875
    register_pending_apps(user, -1)