Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (28.7 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.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.BASEURL,
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.BASEURL, reverse('index')), invitation.code)
168
    message = render_to_string(template_name, {
169
                               'invitation': invitation,
170
                               'url': url,
171
                               'baseurl': settings.BASEURL,
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' % invitation
178
    logger.log(settings.LOGGING_LEVEL, msg)
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.BASEURL, reverse('index')),
194
                               'baseurl': settings.BASEURL,
195
                               'site_name': settings.SITENAME,
196
                               'support': settings.CONTACT_EMAIL})
197
    sender = settings.SERVER_EMAIL
198
    send_mail(subject, message, sender, [user.email],
199
              connection=get_connection())
200
    msg = 'Sent greeting %s' % user.log_display
201
    logger.log(settings.LOGGING_LEVEL, msg)
202

    
203

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

    
217

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

    
232

    
233
def invite(inviter, email, realname):
234
    inv = Invitation(inviter=inviter, username=email, realname=realname)
235
    inv.save()
236
    send_invitation(inv)
237
    inviter.invitations = max(0, inviter.invitations - 1)
238
    inviter.save()
239

    
240

    
241
### PROJECT FUNCTIONS ###
242

    
243
AUTO_ACCEPT_POLICY = 1
244
MODERATED_POLICY = 2
245
CLOSED_POLICY = 3
246

    
247
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
248

    
249

    
250
def get_project_by_application_id(project_application_id):
251
    try:
252
        return Project.objects.get(application__id=project_application_id)
253
    except Project.DoesNotExist:
254
        m = (_(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) %
255
             project_application_id)
256
        raise IOError(m)
257

    
258

    
259
def get_related_project_id(application_id):
260
    try:
261
        app = ProjectApplication.objects.get(id=application_id)
262
        chain = app.chain
263
        Project.objects.get(id=chain)
264
        return chain
265
    except (ProjectApplication.DoesNotExist, Project.DoesNotExist):
266
        return None
267

    
268

    
269
def get_chain_of_application_id(application_id):
270
    try:
271
        app = ProjectApplication.objects.get(id=application_id)
272
        chain = app.chain
273
        return chain.chain
274
    except ProjectApplication.DoesNotExist:
275
        return None
276

    
277

    
278
def get_project_by_id(project_id):
279
    try:
280
        return Project.objects.get(id=project_id)
281
    except Project.DoesNotExist:
282
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
283
        raise IOError(m)
284

    
285

    
286
def get_project_by_name(name):
287
    try:
288
        return Project.objects.get(name=name)
289
    except Project.DoesNotExist:
290
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % name
291
        raise IOError(m)
292

    
293

    
294
def get_chain_for_update(chain_id):
295
    try:
296
        return Chain.objects.get_for_update(chain=chain_id)
297
    except Chain.DoesNotExist:
298
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % chain_id
299
        raise IOError(m)
300

    
301

    
302
def get_chain_of_application_for_update(app_id):
303
    app = get_application(app_id)
304
    return Chain.objects.get_for_update(chain=app.chain_id)
305

    
306

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

    
314

    
315
def get_user_by_id(user_id):
316
    try:
317
        return AstakosUser.objects.get(id=user_id)
318
    except AstakosUser.DoesNotExist:
319
        m = _(astakos_messages.UNKNOWN_USER_ID) % user_id
320
        raise IOError(m)
321

    
322

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

    
330

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

    
339

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

    
348

    
349
def checkAllowed(entity, request_user, admin_only=False):
350
    if isinstance(entity, Project):
351
        application = entity.application
352
    elif isinstance(entity, ProjectApplication):
353
        application = entity
354
    else:
355
        m = "%s not a Project nor a ProjectApplication" % (entity,)
356
        raise ValueError(m)
357

    
358
    if not request_user or request_user.is_project_admin():
359
        return
360

    
361
    if not admin_only and application.owner == request_user:
362
        return
363

    
364
    m = _(astakos_messages.NOT_ALLOWED)
365
    raise PermissionDenied(m)
366

    
367

    
368
def checkAlive(project):
369
    if not project.is_alive:
370
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__
371
        raise PermissionDenied(m)
372

    
373

    
374
def accept_membership_checks(project, request_user):
375
    checkAllowed(project, request_user)
376
    checkAlive(project)
377

    
378
    join_policy = project.application.member_join_policy
379
    if join_policy == CLOSED_POLICY:
380
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
381
        raise PermissionDenied(m)
382

    
383
    if project.violates_members_limit(adding=1):
384
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
385
        raise PermissionDenied(m)
386

    
387

    
388
def accept_membership(project_id, memb_id, request_user=None):
389
    get_chain_for_update(project_id)
390

    
391
    membership = get_membership_by_id(project_id, memb_id)
392
    if not membership.can_accept():
393
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
394
        raise PermissionDenied(m)
395

    
396
    project = membership.project
397
    accept_membership_checks(project, request_user)
398
    user = membership.person
399
    membership.accept()
400
    qh_sync_user(user)
401
    logger.info("User %s has been accepted in %s." %
402
                (user.log_display, project))
403

    
404
    membership_change_notify(project, user, 'accepted')
405
    return membership
406

    
407

    
408
def reject_membership_checks(project, request_user):
409
    checkAllowed(project, request_user)
410
    checkAlive(project)
411

    
412

    
413
def reject_membership(project_id, memb_id, request_user=None):
414
    get_chain_for_update(project_id)
415

    
416
    membership = get_membership_by_id(project_id, memb_id)
417
    if not membership.can_reject():
418
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
419
        raise PermissionDenied(m)
420

    
421
    project = membership.project
422
    reject_membership_checks(project, request_user)
423
    user = membership.person
424
    membership.reject()
425
    logger.info("Request of user %s for %s has been rejected." %
426
                (user.log_display, project))
427

    
428
    membership_change_notify(project, user, 'rejected')
429
    return membership
430

    
431

    
432
def cancel_membership_checks(project):
433
    checkAlive(project)
434

    
435

    
436
def cancel_membership(project_id, request_user):
437
    get_chain_for_update(project_id)
438

    
439
    membership = get_membership(project_id, request_user.id)
440
    if not membership.can_cancel():
441
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
442
        raise PermissionDenied(m)
443

    
444
    project = membership.project
445
    cancel_membership_checks(project)
446
    membership.cancel()
447
    logger.info("Request of user %s for %s has been cancelled." %
448
                (membership.person.log_display, project))
449

    
450

    
451
def remove_membership_checks(project, request_user=None):
452
    checkAllowed(project, request_user)
453
    checkAlive(project)
454

    
455
    leave_policy = project.application.member_leave_policy
456
    if leave_policy == CLOSED_POLICY:
457
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
458
        raise PermissionDenied(m)
459

    
460

    
461
def remove_membership(project_id, memb_id, request_user=None):
462
    get_chain_for_update(project_id)
463

    
464
    membership = get_membership_by_id(project_id, memb_id)
465
    if not membership.can_remove():
466
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
467
        raise PermissionDenied(m)
468

    
469
    project = membership.project
470
    remove_membership_checks(project, request_user)
471
    user = membership.person
472
    membership.remove()
473
    qh_sync_user(user)
474
    logger.info("User %s has been removed from %s." %
475
                (user.log_display, project))
476

    
477
    membership_change_notify(project, user, 'removed')
478
    return membership
479

    
480

    
481
def enroll_member(project_id, user, request_user=None):
482
    get_chain_for_update(project_id)
483
    project = get_project_by_id(project_id)
484
    accept_membership_checks(project, request_user)
485

    
486
    membership, created = ProjectMembership.objects.get_or_create(
487
        project=project,
488
        person=user)
489

    
490
    if not membership.can_accept():
491
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
492
        raise PermissionDenied(m)
493

    
494
    membership.accept()
495
    qh_sync_user(user)
496
    logger.info("User %s has been enrolled in %s." %
497
                (membership.person.log_display, project))
498

    
499
    membership_enroll_notify(project, membership.person)
500
    return membership
501

    
502

    
503
def leave_project_checks(project):
504
    checkAlive(project)
505

    
506
    leave_policy = project.application.member_leave_policy
507
    if leave_policy == CLOSED_POLICY:
508
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
509
        raise PermissionDenied(m)
510

    
511

    
512
def can_leave_request(project, user):
513
    leave_policy = project.application.member_leave_policy
514
    if leave_policy == CLOSED_POLICY:
515
        return False
516
    m = user.get_membership(project)
517
    if m is None:
518
        return False
519
    if m.state != ProjectMembership.ACCEPTED:
520
        return False
521
    return True
522

    
523

    
524
def leave_project(project_id, request_user):
525
    get_chain_for_update(project_id)
526

    
527
    membership = get_membership(project_id, request_user.id)
528
    if not membership.can_leave():
529
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
530
        raise PermissionDenied(m)
531

    
532
    project = membership.project
533
    leave_project_checks(project)
534

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

    
550

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

    
554
    join_policy = project.application.member_join_policy
555
    if join_policy == CLOSED_POLICY:
556
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
557
        raise PermissionDenied(m)
558

    
559

    
560
def can_join_request(project, user):
561
    join_policy = project.application.member_join_policy
562
    if join_policy == CLOSED_POLICY:
563
        return False
564
    m = user.get_membership(project)
565
    if m:
566
        return False
567
    return True
568

    
569

    
570
def join_project(project_id, request_user):
571
    get_chain_for_update(project_id)
572
    project = get_project_by_id(project_id)
573
    join_project_checks(project)
574

    
575
    membership, created = ProjectMembership.objects.get_or_create(
576
        project=project,
577
        person=request_user)
578

    
579
    if not created:
580
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
581
        raise PermissionDenied(msg)
582

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

    
598

    
599
def submit_application(owner=None,
600
                       name=None,
601
                       precursor_id=None,
602
                       homepage=None,
603
                       description=None,
604
                       start_date=None,
605
                       end_date=None,
606
                       member_join_policy=None,
607
                       member_leave_policy=None,
608
                       limit_on_members_number=None,
609
                       comments=None,
610
                       resource_policies=None,
611
                       request_user=None):
612

    
613
    precursor = None
614
    if precursor_id is not None:
615
        get_chain_of_application_for_update(precursor_id)
616
        precursor = ProjectApplication.objects.get(id=precursor_id)
617

    
618
        if (request_user and
619
            (not precursor.owner == request_user and
620
             not request_user.is_superuser
621
             and not request_user.is_project_admin())):
622
            m = _(astakos_messages.NOT_ALLOWED)
623
            raise PermissionDenied(m)
624

    
625
    force = request_user.is_project_admin()
626
    ok, limit = qh_add_pending_app(owner, precursor, force)
627
    if not ok:
628
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
629
        raise PermissionDenied(m)
630

    
631
    application = ProjectApplication(
632
        applicant=request_user,
633
        owner=owner,
634
        name=name,
635
        precursor_application_id=precursor_id,
636
        homepage=homepage,
637
        description=description,
638
        start_date=start_date,
639
        end_date=end_date,
640
        member_join_policy=member_join_policy,
641
        member_leave_policy=member_leave_policy,
642
        limit_on_members_number=limit_on_members_number,
643
        comments=comments)
644

    
645
    if precursor is None:
646
        application.chain = new_chain()
647
    else:
648
        chain = precursor.chain
649
        application.chain = chain
650
        objs = ProjectApplication.objects
651
        pending = objs.filter(chain=chain, state=ProjectApplication.PENDING)
652
        for app in pending:
653
            app.state = ProjectApplication.REPLACED
654
            app.save()
655

    
656
    application.save()
657
    if resource_policies is not None:
658
        application.set_resource_policies(resource_policies)
659
    logger.info("User %s submitted %s." %
660
                (request_user.log_display, application.log_display))
661
    application_submit_notify(application)
662
    return application
663

    
664

    
665
def cancel_application(application_id, request_user=None, reason=""):
666
    get_chain_of_application_for_update(application_id)
667
    application = get_application(application_id)
668
    checkAllowed(application, request_user)
669

    
670
    if not application.can_cancel():
671
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
672
              (application.id, application.state_display()))
673
        raise PermissionDenied(m)
674

    
675
    qh_release_pending_app(application.owner)
676

    
677
    application.cancel()
678
    logger.info("%s has been cancelled." % (application.log_display))
679

    
680

    
681
def dismiss_application(application_id, request_user=None, reason=""):
682
    get_chain_of_application_for_update(application_id)
683
    application = get_application(application_id)
684
    checkAllowed(application, request_user)
685

    
686
    if not application.can_dismiss():
687
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
688
              (application.id, application.state_display()))
689
        raise PermissionDenied(m)
690

    
691
    application.dismiss()
692
    logger.info("%s has been dismissed." % (application.log_display))
693

    
694

    
695
def deny_application(application_id, request_user=None, reason=""):
696
    get_chain_of_application_for_update(application_id)
697
    application = get_application(application_id)
698

    
699
    checkAllowed(application, request_user, admin_only=True)
700

    
701
    if not application.can_deny():
702
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
703
              (application.id, application.state_display()))
704
        raise PermissionDenied(m)
705

    
706
    qh_release_pending_app(application.owner)
707

    
708
    application.deny(reason)
709
    logger.info("%s has been denied with reason \"%s\"." %
710
                (application.log_display, reason))
711
    application_deny_notify(application)
712

    
713

    
714
def check_conflicting_projects(application):
715
    try:
716
        project = get_project_by_id(application.chain)
717
    except IOError:
718
        project = None
719

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

    
732
    return project
733

    
734

    
735
def approve_application(app_id, request_user=None, reason=""):
736
    get_chain_of_application_for_update(app_id)
737
    application = get_application(app_id)
738

    
739
    checkAllowed(application, request_user, admin_only=True)
740

    
741
    if not application.can_approve():
742
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
743
              (application.id, application.state_display()))
744
        raise PermissionDenied(m)
745

    
746
    project = check_conflicting_projects(application)
747

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

    
756
    qh_release_pending_app(owner, locked=True)
757
    application.approve(reason)
758
    qh_sync_locked_users(members)
759
    logger.info("%s has been approved." % (application.log_display))
760
    application_approve_notify(application)
761

    
762

    
763
def check_expiration(execute=False):
764
    objects = Project.objects
765
    expired = objects.expired_projects()
766
    if execute:
767
        for project in expired:
768
            terminate(project.id)
769

    
770
    return [project.expiration_info() for project in expired]
771

    
772

    
773
def terminate(project_id, request_user=None):
774
    get_chain_for_update(project_id)
775
    project = get_project_by_id(project_id)
776
    checkAllowed(project, request_user, admin_only=True)
777
    checkAlive(project)
778

    
779
    project.terminate()
780
    qh_sync_project(project)
781
    logger.info("%s has been terminated." % (project))
782

    
783
    project_termination_notify(project)
784

    
785

    
786
def suspend(project_id, request_user=None):
787
    get_chain_for_update(project_id)
788
    project = get_project_by_id(project_id)
789
    checkAllowed(project, request_user, admin_only=True)
790
    checkAlive(project)
791

    
792
    project.suspend()
793
    qh_sync_project(project)
794
    logger.info("%s has been suspended." % (project))
795

    
796
    project_suspension_notify(project)
797

    
798

    
799
def resume(project_id, request_user=None):
800
    get_chain_for_update(project_id)
801
    project = get_project_by_id(project_id)
802
    checkAllowed(project, request_user, admin_only=True)
803

    
804
    if not project.is_suspended:
805
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
806
        raise PermissionDenied(m)
807

    
808
    project.resume()
809
    qh_sync_project(project)
810
    logger.info("%s has been unsuspended." % (project))
811

    
812

    
813
def get_by_chain_or_404(chain_id):
814
    try:
815
        project = Project.objects.get(id=chain_id)
816
        application = project.application
817
        return project, application
818
    except:
819
        application = ProjectApplication.objects.latest_of_chain(chain_id)
820
        if application is None:
821
            raise Http404
822
        else:
823
            return None, application
824

    
825

    
826
def _partition_by(f, l):
827
    d = {}
828
    for x in l:
829
        group = f(x)
830
        group_l = d.get(group, [])
831
        group_l.append(x)
832
        d[group] = group_l
833
    return d
834

    
835

    
836
def count_pending_app(users):
837
    users = list(users)
838
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
839
                                             owner__in=users)
840
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
841

    
842
    usage = {}
843
    for user in users:
844
        uuid = user.uuid
845
        usage[uuid] = len(apps_d.get(uuid, []))
846
    return usage
847

    
848

    
849
def get_pending_app_diff(user, precursor):
850
    if precursor is None:
851
        diff = 1
852
    else:
853
        chain = precursor.chain
854
        objs = ProjectApplication.objects
855
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
856
        count = q.count()
857
        diff = 1 - count
858
    return diff
859

    
860

    
861
def qh_add_pending_app(user, precursor=None, force=False):
862
    user = AstakosUser.forupdate.get_for_update(id=user.id)
863
    diff = get_pending_app_diff(user, precursor)
864
    return register_pending_apps(user, diff, force)
865

    
866

    
867
def check_pending_app_quota(user, precursor=None):
868
    diff = get_pending_app_diff(user, precursor)
869
    quota = get_pending_app_quota(user)
870
    limit = quota['limit']
871
    usage = quota['usage']
872
    if usage + diff > limit:
873
        return False, limit
874
    return True, None
875

    
876

    
877
def qh_release_pending_app(user, locked=False):
878
    if not locked:
879
        user = AstakosUser.forupdate.get_for_update(id=user.id)
880
    register_pending_apps(user, -1)