Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 3805be31

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.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
    url = ec.get_url()
223
    url = request.build_absolute_uri(url)
224
    c = {'url': url, 'site_name': settings.SITENAME, 'support': settings.CONTACT_EMAIL,
225
         'ec': ec}
226
    message = render_to_string(email_template_name, c)
227
    from_email = settings.SERVER_EMAIL
228
    send_mail(_(astakos_messages.EMAIL_CHANGE_EMAIL_SUBJECT), message,
229
              from_email,
230
              [ec.new_email_address], connection=get_connection())
231
    msg = 'Sent change email for %s' % ec.user.log_display
232
    logger.log(settings.LOGGING_LEVEL, msg)
233

    
234

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

    
242

    
243
### PROJECT FUNCTIONS ###
244

    
245
AUTO_ACCEPT_POLICY = 1
246
MODERATED_POLICY = 2
247
CLOSED_POLICY = 3
248

    
249
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
250

    
251

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

    
260

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

    
270

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

    
279

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

    
287

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

    
295

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

    
303

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

    
308

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

    
316

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

    
324

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

    
332

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

    
341

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

    
350

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

    
360
    if not request_user or request_user.is_project_admin():
361
        return
362

    
363
    if not admin_only and application.owner == request_user:
364
        return
365

    
366
    m = _(astakos_messages.NOT_ALLOWED)
367
    raise PermissionDenied(m)
368

    
369

    
370
def checkAlive(project):
371
    if not project.is_alive:
372
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.id
373
        raise PermissionDenied(m)
374

    
375

    
376
def accept_membership_checks(project, request_user):
377
    checkAllowed(project, request_user)
378
    checkAlive(project)
379

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

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

    
389

    
390
def accept_membership(project_id, memb_id, request_user=None):
391
    get_chain_for_update(project_id)
392

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

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

    
406
    membership_change_notify(project, user, 'accepted')
407
    return membership
408

    
409

    
410
def reject_membership_checks(project, request_user):
411
    checkAllowed(project, request_user)
412
    checkAlive(project)
413

    
414

    
415
def reject_membership(project_id, memb_id, request_user=None):
416
    get_chain_for_update(project_id)
417

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

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

    
430
    membership_change_notify(project, user, 'rejected')
431
    return membership
432

    
433

    
434
def cancel_membership_checks(project):
435
    checkAlive(project)
436

    
437

    
438
def cancel_membership(project_id, request_user):
439
    get_chain_for_update(project_id)
440

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

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

    
452

    
453
def remove_membership_checks(project, request_user=None):
454
    checkAllowed(project, request_user)
455
    checkAlive(project)
456

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

    
462

    
463
def remove_membership(project_id, memb_id, request_user=None):
464
    get_chain_for_update(project_id)
465

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

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

    
479
    membership_change_notify(project, user, 'removed')
480
    return membership
481

    
482

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

    
488
    membership, created = ProjectMembership.objects.get_or_create(
489
        project=project,
490
        person=user)
491

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

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

    
501
    membership_enroll_notify(project, membership.person)
502
    return membership
503

    
504

    
505
def leave_project_checks(project):
506
    checkAlive(project)
507

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

    
513

    
514
def can_leave_request(project, user):
515
    try:
516
        leave_project_checks(project)
517
    except PermissionDenied:
518
        return False
519
    m = user.get_membership(project)
520
    if m is None:
521
        return False
522
    return m.can_leave()
523

    
524

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

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

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

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

    
551

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

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

    
560

    
561
def can_join_request(project, user):
562
    try:
563
        join_project_checks(project)
564
    except PermissionDenied:
565
        return False
566

    
567
    m = user.get_membership(project)
568
    return not(m)
569

    
570

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

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

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

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

    
599

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

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

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

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

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

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

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

    
665

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

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

    
676
    qh_release_pending_app(application.owner)
677

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

    
681

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

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

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

    
695

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

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

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

    
707
    qh_release_pending_app(application.owner)
708

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

    
714

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

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

    
733
    return project
734

    
735

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

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

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

    
747
    project = check_conflicting_projects(application)
748

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

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

    
763

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

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

    
773

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

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

    
784
    project_termination_notify(project)
785

    
786

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

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

    
797
    project_suspension_notify(project)
798

    
799

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

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

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

    
813

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

    
826

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

    
836

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

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

    
849

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

    
861

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

    
867

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

    
877

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