Statistics
| Branch: | Tag: | Revision:

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

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'
159
    logger.log(settings.LOGGING_LEVEL, msg, user.email)
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'
179
    logger.log(settings.LOGGING_LEVEL, msg, invitation)
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'
203
    logger.log(settings.LOGGING_LEVEL, msg, user.log_display)
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'
217
    logger.log(settings.LOGGING_LEVEL, msg, user.log_display)
218

    
219

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

    
235

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

    
243

    
244
### PROJECT FUNCTIONS ###
245

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

    
250
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
251

    
252

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

    
261

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

    
271

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

    
280

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

    
288

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

    
296

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

    
304

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

    
309

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

    
317

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

    
325

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

    
333

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

    
342

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

    
351

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

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

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

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

    
370

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

    
376

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

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

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

    
390

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

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

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

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

    
410

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

    
415

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

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

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

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

    
434

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

    
438

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

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

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

    
453

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

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

    
463

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

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

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

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

    
483

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

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

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

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

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

    
505

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

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

    
514

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

    
525

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

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

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

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

    
552

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

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

    
561

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

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

    
571

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

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

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

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

    
600

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

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

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

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

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

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

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

    
666

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

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

    
677
    qh_release_pending_app(application.owner)
678

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

    
682

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

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

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

    
696

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

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

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

    
708
    qh_release_pending_app(application.owner)
709

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

    
715

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

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

    
734
    return project
735

    
736

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

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

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

    
748
    project = check_conflicting_projects(application)
749

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

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

    
764

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

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

    
774

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

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

    
785
    project_termination_notify(project)
786

    
787

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

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

    
798
    project_suspension_notify(project)
799

    
800

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

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

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

    
814

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

    
827

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

    
837

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

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

    
850

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

    
862

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

    
868

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

    
878

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