Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 67cf14bf

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 (
40
    login as auth_login,
41
    logout as auth_logout)
42
from django.core.exceptions import PermissionDenied
43
from django.db.models import Q
44
from django.http import Http404
45

    
46
from synnefo_branding.utils import render_to_string
47

    
48
from synnefo.lib import join_urls
49
from astakos.im.settings import (
50
    CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL,
51
    VERIFICATION_EMAIL_SUBJECT, ACCOUNT_CREATION_SUBJECT,
52
    HELPDESK_NOTIFICATION_EMAIL_SUBJECT,
53
    INVITATION_EMAIL_SUBJECT, GREETING_EMAIL_SUBJECT, FEEDBACK_EMAIL_SUBJECT,
54
    EMAIL_CHANGE_EMAIL_SUBJECT,
55
    )
56
from astakos.im.models import (
57
    AstakosUser, Invitation, ProjectMembership, ProjectApplication, Project,
58
    Chain, new_chain)
59
from astakos.im.quotas import (qh_sync_user, get_pending_app_quota,
60
                               register_pending_apps, qh_sync_project,
61
                               qh_sync_locked_users, get_users_for_update,
62
                               members_to_sync)
63
from astakos.im.project_notif import (
64
    membership_change_notify, membership_enroll_notify,
65
    membership_request_notify, membership_leave_request_notify,
66
    application_submit_notify, application_approve_notify,
67
    application_deny_notify,
68
    project_termination_notify, project_suspension_notify)
69
from astakos.im import settings
70
import astakos.im.messages as astakos_messages
71

    
72
logger = logging.getLogger(__name__)
73

    
74

    
75
def login(request, user):
76
    auth_login(request, user)
77
    from astakos.im.models import SessionCatalog
78
    SessionCatalog(
79
        session_key=request.session.session_key,
80
        user=user
81
    ).save()
82
    logger.info('%s logged in.', user.log_display)
83

    
84

    
85
def logout(request, *args, **kwargs):
86
    user = request.user
87
    auth_logout(request, *args, **kwargs)
88
    logger.info('%s logged out.', user.log_display)
89

    
90

    
91
def send_verification(user, template_name='im/activation_email.txt'):
92
    """
93
    Send email to user to verify his/her email and activate his/her account.
94
    """
95
    url = join_urls(BASEURL, user.get_activation_url(nxt=reverse('index')))
96
    message = render_to_string(template_name, {
97
                               'user': user,
98
                               'url': url,
99
                               'baseurl': BASEURL,
100
                               'site_name': SITENAME,
101
                               'support': CONTACT_EMAIL})
102
    sender = settings.SERVER_EMAIL
103
    send_mail(_(VERIFICATION_EMAIL_SUBJECT), message, sender, [user.email],
104
              connection=get_connection())
105
    logger.info("Sent user verirfication email: %s", user.log_display)
106

    
107

    
108
def _send_admin_notification(template_name,
109
                             context=None,
110
                             user=None,
111
                             msg="",
112
                             subject='alpha2 testing notification',):
113
    """
114
    Send notification email to settings.HELPDESK + settings.MANAGERS +
115
    settings.ADMINS.
116
    """
117
    if context is None:
118
        context = {}
119
    if not 'user' in context:
120
        context['user'] = user
121

    
122
    message = render_to_string(template_name, context)
123
    sender = settings.SERVER_EMAIL
124
    recipient_list = [e[1] for e in settings.HELPDESK +
125
                      settings.MANAGERS + settings.ADMINS]
126
    send_mail(subject, message, sender, recipient_list,
127
              connection=get_connection())
128
    if user:
129
        msg = 'Sent admin notification (%s) for user %s' % (msg,
130
                                                            user.log_display)
131
    else:
132
        msg = 'Sent admin notification (%s)' % msg
133

    
134
    logger.log(LOGGING_LEVEL, msg)
135

    
136

    
137
def send_account_pending_moderation_notification(
138
        user,
139
        template_name='im/account_pending_moderation_notification.txt'):
140
    """
141
    Notify admins that a new user has verified his email address and moderation
142
    step is required to activate his account.
143
    """
144
    subject = _(ACCOUNT_CREATION_SUBJECT) % {'user': user.email}
145
    return _send_admin_notification(template_name, {}, subject=subject,
146
                                    user=user, msg="account creation")
147

    
148

    
149
def send_account_activated_notification(
150
        user,
151
        template_name='im/account_activated_notification.txt'):
152
    """
153
    Send email to settings.HELPDESK + settings.MANAGERES + settings.ADMINS
154
    lists to notify that a new account has been accepted and activated.
155
    """
156
    message = render_to_string(
157
        template_name,
158
        {'user': user}
159
    )
160
    sender = settings.SERVER_EMAIL
161
    recipient_list = [e[1] for e in settings.HELPDESK +
162
                      settings.MANAGERS + settings.ADMINS]
163
    send_mail(_(HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % {'user': user.email},
164
              message, sender, recipient_list, connection=get_connection())
165
    msg = 'Sent helpdesk admin notification for %s' % user.email
166
    logger.log(LOGGING_LEVEL, msg)
167

    
168

    
169
def send_invitation(invitation, template_name='im/invitation.txt'):
170
    """
171
    Send invitation email.
172
    """
173
    subject = _(INVITATION_EMAIL_SUBJECT)
174
    url = '%s?code=%d' % (join_urls(BASEURL, reverse('index')), invitation.code)
175
    message = render_to_string(template_name, {
176
                               'invitation': invitation,
177
                               'url': url,
178
                               'baseurl': BASEURL,
179
                               'site_name': SITENAME,
180
                               'support': CONTACT_EMAIL})
181
    sender = settings.SERVER_EMAIL
182
    send_mail(subject, message, sender, [invitation.username],
183
              connection=get_connection())
184
    msg = 'Sent invitation %s' % invitation
185
    logger.log(LOGGING_LEVEL, msg)
186
    inviter_invitations = invitation.inviter.invitations
187
    invitation.inviter.invitations = max(0, inviter_invitations - 1)
188
    invitation.inviter.save()
189

    
190

    
191
def send_greeting(user, email_template_name='im/welcome_email.txt'):
192
    """
193
    Send welcome email to an accepted/activated user.
194

195
    Raises SMTPException, socket.error
196
    """
197
    subject = _(GREETING_EMAIL_SUBJECT)
198
    message = render_to_string(email_template_name, {
199
                               'user': user,
200
                               'url': join_urls(BASEURL, reverse('index')),
201
                               'baseurl': BASEURL,
202
                               'site_name': SITENAME,
203
                               'support': CONTACT_EMAIL})
204
    sender = settings.SERVER_EMAIL
205
    send_mail(subject, message, sender, [user.email],
206
              connection=get_connection())
207
    msg = 'Sent greeting %s' % user.log_display
208
    logger.log(LOGGING_LEVEL, msg)
209

    
210

    
211
def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'):
212
    subject = _(FEEDBACK_EMAIL_SUBJECT)
213
    from_email = settings.SERVER_EMAIL
214
    recipient_list = [e[1] for e in settings.HELPDESK]
215
    content = render_to_string(email_template_name, {
216
        'message': msg,
217
        'data': data,
218
        'user': user})
219
    send_mail(subject, content, from_email, recipient_list,
220
              connection=get_connection())
221
    msg = 'Sent feedback from %s' % user.log_display
222
    logger.log(LOGGING_LEVEL, msg)
223

    
224

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

    
238

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

    
246

    
247
### PROJECT FUNCTIONS ###
248

    
249
AUTO_ACCEPT_POLICY = 1
250
MODERATED_POLICY = 2
251
CLOSED_POLICY = 3
252

    
253
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
254

    
255

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

    
264

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

    
274

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

    
283

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

    
291

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

    
299

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

    
307

    
308
def get_chain_of_application_for_update(app_id):
309
    app = get_application(app_id)
310
    return Chain.objects.get_for_update(chain=app.chain_id)
311

    
312

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

    
320

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

    
328

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

    
336

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

    
345

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

    
354

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

    
364
    if not request_user or request_user.is_project_admin():
365
        return
366

    
367
    if not admin_only and application.owner == request_user:
368
        return
369

    
370
    m = _(astakos_messages.NOT_ALLOWED)
371
    raise PermissionDenied(m)
372

    
373

    
374
def checkAlive(project):
375
    if not project.is_alive:
376
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__
377
        raise PermissionDenied(m)
378

    
379

    
380
def accept_membership_checks(project, request_user):
381
    checkAllowed(project, request_user)
382
    checkAlive(project)
383

    
384
    join_policy = project.application.member_join_policy
385
    if join_policy == CLOSED_POLICY:
386
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
387
        raise PermissionDenied(m)
388

    
389
    if project.violates_members_limit(adding=1):
390
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
391
        raise PermissionDenied(m)
392

    
393

    
394
def accept_membership(project_id, memb_id, request_user=None):
395
    get_chain_for_update(project_id)
396

    
397
    membership = get_membership_by_id(project_id, memb_id)
398
    if not membership.can_accept():
399
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
400
        raise PermissionDenied(m)
401

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

    
410
    membership_change_notify(project, user, 'accepted')
411
    return membership
412

    
413

    
414
def reject_membership_checks(project, request_user):
415
    checkAllowed(project, request_user)
416
    checkAlive(project)
417

    
418

    
419
def reject_membership(project_id, memb_id, request_user=None):
420
    get_chain_for_update(project_id)
421

    
422
    membership = get_membership_by_id(project_id, memb_id)
423
    if not membership.can_reject():
424
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
425
        raise PermissionDenied(m)
426

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

    
434
    membership_change_notify(project, user, 'rejected')
435
    return membership
436

    
437

    
438
def cancel_membership_checks(project):
439
    checkAlive(project)
440

    
441

    
442
def cancel_membership(project_id, request_user):
443
    get_chain_for_update(project_id)
444

    
445
    membership = get_membership(project_id, request_user.id)
446
    if not membership.can_cancel():
447
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
448
        raise PermissionDenied(m)
449

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

    
456

    
457
def remove_membership_checks(project, request_user=None):
458
    checkAllowed(project, request_user)
459
    checkAlive(project)
460

    
461
    leave_policy = project.application.member_leave_policy
462
    if leave_policy == CLOSED_POLICY:
463
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
464
        raise PermissionDenied(m)
465

    
466

    
467
def remove_membership(project_id, memb_id, request_user=None):
468
    get_chain_for_update(project_id)
469

    
470
    membership = get_membership_by_id(project_id, memb_id)
471
    if not membership.can_remove():
472
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
473
        raise PermissionDenied(m)
474

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

    
483
    membership_change_notify(project, user, 'removed')
484
    return membership
485

    
486

    
487
def enroll_member(project_id, user, request_user=None):
488
    get_chain_for_update(project_id)
489
    project = get_project_by_id(project_id)
490
    accept_membership_checks(project, request_user)
491

    
492
    membership, created = ProjectMembership.objects.get_or_create(
493
        project=project,
494
        person=user)
495

    
496
    if not membership.can_accept():
497
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
498
        raise PermissionDenied(m)
499

    
500
    membership.accept()
501
    qh_sync_user(user)
502
    logger.info("User %s has been enrolled in %s." %
503
                (membership.person.log_display, project))
504

    
505
    membership_enroll_notify(project, membership.person)
506
    return membership
507

    
508

    
509
def leave_project_checks(project):
510
    checkAlive(project)
511

    
512
    leave_policy = project.application.member_leave_policy
513
    if leave_policy == CLOSED_POLICY:
514
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
515
        raise PermissionDenied(m)
516

    
517

    
518
def can_leave_request(project, user):
519
    leave_policy = project.application.member_leave_policy
520
    if leave_policy == CLOSED_POLICY:
521
        return False
522
    m = user.get_membership(project)
523
    if m is None:
524
        return False
525
    if m.state != ProjectMembership.ACCEPTED:
526
        return False
527
    return True
528

    
529

    
530
def leave_project(project_id, request_user):
531
    get_chain_for_update(project_id)
532

    
533
    membership = get_membership(project_id, request_user.id)
534
    if not membership.can_leave():
535
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
536
        raise PermissionDenied(m)
537

    
538
    project = membership.project
539
    leave_project_checks(project)
540

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

    
556

    
557
def join_project_checks(project):
558
    checkAlive(project)
559

    
560
    join_policy = project.application.member_join_policy
561
    if join_policy == CLOSED_POLICY:
562
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
563
        raise PermissionDenied(m)
564

    
565

    
566
def can_join_request(project, user):
567
    join_policy = project.application.member_join_policy
568
    if join_policy == CLOSED_POLICY:
569
        return False
570
    m = user.get_membership(project)
571
    if m:
572
        return False
573
    return True
574

    
575

    
576
def join_project(project_id, request_user):
577
    get_chain_for_update(project_id)
578
    project = get_project_by_id(project_id)
579
    join_project_checks(project)
580

    
581
    membership, created = ProjectMembership.objects.get_or_create(
582
        project=project,
583
        person=request_user)
584

    
585
    if not created:
586
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
587
        raise PermissionDenied(msg)
588

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

    
604

    
605
def submit_application(owner=None,
606
                       name=None,
607
                       precursor_id=None,
608
                       homepage=None,
609
                       description=None,
610
                       start_date=None,
611
                       end_date=None,
612
                       member_join_policy=None,
613
                       member_leave_policy=None,
614
                       limit_on_members_number=None,
615
                       comments=None,
616
                       resource_policies=None,
617
                       request_user=None):
618

    
619
    precursor = None
620
    if precursor_id is not None:
621
        get_chain_of_application_for_update(precursor_id)
622
        precursor = ProjectApplication.objects.get(id=precursor_id)
623

    
624
        if (request_user and
625
            (not precursor.owner == request_user and
626
             not request_user.is_superuser
627
             and not request_user.is_project_admin())):
628
            m = _(astakos_messages.NOT_ALLOWED)
629
            raise PermissionDenied(m)
630

    
631
    force = request_user.is_project_admin()
632
    ok, limit = qh_add_pending_app(owner, precursor, force)
633
    if not ok:
634
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
635
        raise PermissionDenied(m)
636

    
637
    application = ProjectApplication(
638
        applicant=request_user,
639
        owner=owner,
640
        name=name,
641
        precursor_application_id=precursor_id,
642
        homepage=homepage,
643
        description=description,
644
        start_date=start_date,
645
        end_date=end_date,
646
        member_join_policy=member_join_policy,
647
        member_leave_policy=member_leave_policy,
648
        limit_on_members_number=limit_on_members_number,
649
        comments=comments)
650

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

    
662
    application.save()
663
    if resource_policies is not None:
664
        application.set_resource_policies(resource_policies)
665
    logger.info("User %s submitted %s." %
666
                (request_user.log_display, application.log_display))
667
    application_submit_notify(application)
668
    return application
669

    
670

    
671
def cancel_application(application_id, request_user=None, reason=""):
672
    get_chain_of_application_for_update(application_id)
673
    application = get_application(application_id)
674
    checkAllowed(application, request_user)
675

    
676
    if not application.can_cancel():
677
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
678
              (application.id, application.state_display()))
679
        raise PermissionDenied(m)
680

    
681
    qh_release_pending_app(application.owner)
682

    
683
    application.cancel()
684
    logger.info("%s has been cancelled." % (application.log_display))
685

    
686

    
687
def dismiss_application(application_id, request_user=None, reason=""):
688
    get_chain_of_application_for_update(application_id)
689
    application = get_application(application_id)
690
    checkAllowed(application, request_user)
691

    
692
    if not application.can_dismiss():
693
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
694
              (application.id, application.state_display()))
695
        raise PermissionDenied(m)
696

    
697
    application.dismiss()
698
    logger.info("%s has been dismissed." % (application.log_display))
699

    
700

    
701
def deny_application(application_id, request_user=None, reason=""):
702
    get_chain_of_application_for_update(application_id)
703
    application = get_application(application_id)
704

    
705
    checkAllowed(application, request_user, admin_only=True)
706

    
707
    if not application.can_deny():
708
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
709
              (application.id, application.state_display()))
710
        raise PermissionDenied(m)
711

    
712
    qh_release_pending_app(application.owner)
713

    
714
    application.deny(reason)
715
    logger.info("%s has been denied with reason \"%s\"." %
716
                (application.log_display, reason))
717
    application_deny_notify(application)
718

    
719

    
720
def check_conflicting_projects(application):
721
    try:
722
        project = get_project_by_id(application.chain)
723
    except IOError:
724
        project = None
725

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

    
738
    return project
739

    
740

    
741
def approve_application(app_id, request_user=None, reason=""):
742
    get_chain_of_application_for_update(app_id)
743
    application = get_application(app_id)
744

    
745
    checkAllowed(application, request_user, admin_only=True)
746

    
747
    if not application.can_approve():
748
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
749
              (application.id, application.state_display()))
750
        raise PermissionDenied(m)
751

    
752
    project = check_conflicting_projects(application)
753

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

    
762
    qh_release_pending_app(owner, locked=True)
763
    application.approve(reason)
764
    qh_sync_locked_users(members)
765
    logger.info("%s has been approved." % (application.log_display))
766
    application_approve_notify(application)
767

    
768

    
769
def check_expiration(execute=False):
770
    objects = Project.objects
771
    expired = objects.expired_projects()
772
    if execute:
773
        for project in expired:
774
            terminate(project.id)
775

    
776
    return [project.expiration_info() for project in expired]
777

    
778

    
779
def terminate(project_id, request_user=None):
780
    get_chain_for_update(project_id)
781
    project = get_project_by_id(project_id)
782
    checkAllowed(project, request_user, admin_only=True)
783
    checkAlive(project)
784

    
785
    project.terminate()
786
    qh_sync_project(project)
787
    logger.info("%s has been terminated." % (project))
788

    
789
    project_termination_notify(project)
790

    
791

    
792
def suspend(project_id, request_user=None):
793
    get_chain_for_update(project_id)
794
    project = get_project_by_id(project_id)
795
    checkAllowed(project, request_user, admin_only=True)
796
    checkAlive(project)
797

    
798
    project.suspend()
799
    qh_sync_project(project)
800
    logger.info("%s has been suspended." % (project))
801

    
802
    project_suspension_notify(project)
803

    
804

    
805
def resume(project_id, request_user=None):
806
    get_chain_for_update(project_id)
807
    project = get_project_by_id(project_id)
808
    checkAllowed(project, request_user, admin_only=True)
809

    
810
    if not project.is_suspended:
811
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
812
        raise PermissionDenied(m)
813

    
814
    project.resume()
815
    qh_sync_project(project)
816
    logger.info("%s has been unsuspended." % (project))
817

    
818

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

    
831

    
832
def _partition_by(f, l):
833
    d = {}
834
    for x in l:
835
        group = f(x)
836
        group_l = d.get(group, [])
837
        group_l.append(x)
838
        d[group] = group_l
839
    return d
840

    
841

    
842
def count_pending_app(users):
843
    users = list(users)
844
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
845
                                             owner__in=users)
846
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
847

    
848
    usage = {}
849
    for user in users:
850
        uuid = user.uuid
851
        usage[uuid] = len(apps_d.get(uuid, []))
852
    return usage
853

    
854

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

    
866

    
867
def qh_add_pending_app(user, precursor=None, force=False):
868
    user = AstakosUser.forupdate.get_for_update(id=user.id)
869
    diff = get_pending_app_diff(user, precursor)
870
    return register_pending_apps(user, diff, force)
871

    
872

    
873
def check_pending_app_quota(user, precursor=None):
874
    diff = get_pending_app_diff(user, precursor)
875
    quota = get_pending_app_quota(user)
876
    limit = quota['limit']
877
    usage = quota['usage']
878
    if usage + diff > limit:
879
        return False, limit
880
    return True, None
881

    
882

    
883
def qh_release_pending_app(user, locked=False):
884
    if not locked:
885
        user = AstakosUser.forupdate.get_for_update(id=user.id)
886
    register_pending_apps(user, -1)