Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (31 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
from datetime import datetime
36

    
37
from django.utils.translation import ugettext as _
38
from django.core.mail import send_mail, get_connection
39
from django.core.urlresolvers import reverse
40
from django.contrib.auth import login as auth_login, logout as auth_logout
41
from django.db.models import Q
42

    
43
from synnefo_branding.utils import render_to_string
44

    
45
from synnefo.lib import join_urls
46
from astakos.im.models import AstakosUser, Invitation, ProjectMembership, \
47
    ProjectApplication, Project, new_chain, Resource
48
from astakos.im.quotas import qh_sync_user, get_pending_app_quota, \
49
    register_pending_apps, qh_sync_project, qh_sync_locked_users, \
50
    get_users_for_update, members_to_sync
51
from astakos.im.project_notif import membership_change_notify, \
52
    membership_enroll_notify, membership_request_notify, \
53
    membership_leave_request_notify, application_submit_notify, \
54
    application_approve_notify, application_deny_notify, \
55
    project_termination_notify, project_suspension_notify
56
from astakos.im import settings
57

    
58
import astakos.im.messages as astakos_messages
59

    
60
logger = logging.getLogger(__name__)
61

    
62

    
63
def login(request, user):
64
    auth_login(request, user)
65
    from astakos.im.models import SessionCatalog
66
    SessionCatalog(
67
        session_key=request.session.session_key,
68
        user=user
69
    ).save()
70
    logger.info('%s logged in.', user.log_display)
71

    
72

    
73
def logout(request, *args, **kwargs):
74
    user = request.user
75
    auth_logout(request, *args, **kwargs)
76
    logger.info('%s logged out.', user.log_display)
77

    
78

    
79
def send_verification(user, template_name='im/activation_email.txt'):
80
    """
81
    Send email to user to verify his/her email and activate his/her account.
82
    """
83
    url = join_urls(settings.BASE_HOST,
84
                    user.get_activation_url(nxt=reverse('index')))
85
    message = render_to_string(template_name, {
86
                               'user': user,
87
                               'url': url,
88
                               'baseurl': settings.BASE_URL,
89
                               'site_name': settings.SITENAME,
90
                               'support': settings.CONTACT_EMAIL})
91
    sender = settings.SERVER_EMAIL
92
    send_mail(_(astakos_messages.VERIFICATION_EMAIL_SUBJECT), message, sender,
93
              [user.email],
94
              connection=get_connection())
95
    logger.info("Sent user verirfication email: %s", user.log_display)
96

    
97

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

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

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

    
126

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

    
139

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

    
160

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

    
183

    
184
def send_greeting(user, email_template_name='im/welcome_email.txt'):
185
    """
186
    Send welcome email to an accepted/activated user.
187

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

    
204

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

    
218

    
219
def send_change_email(
220
    ec, request, email_template_name='registration/email_change_email.txt'
221
):
222
    url = ec.get_url()
223
    url = request.build_absolute_uri(url)
224
    c = {'url': url, 'site_name': settings.SITENAME,
225
         'support': settings.CONTACT_EMAIL,
226
         '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' % ec.user.log_display
233
    logger.log(settings.LOGGING_LEVEL, msg)
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

    
247
class ProjectError(Exception):
248
    pass
249

    
250

    
251
class ProjectNotFound(ProjectError):
252
    pass
253

    
254

    
255
class ProjectForbidden(ProjectError):
256
    pass
257

    
258

    
259
class ProjectBadRequest(ProjectError):
260
    pass
261

    
262

    
263
class ProjectConflict(ProjectError):
264
    pass
265

    
266
AUTO_ACCEPT_POLICY = 1
267
MODERATED_POLICY = 2
268
CLOSED_POLICY = 3
269

    
270
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
271

    
272

    
273
def get_related_project_id(application_id):
274
    try:
275
        app = ProjectApplication.objects.get(id=application_id)
276
        return app.chain_id
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 ProjectNotFound(m)
287

    
288

    
289
def get_project_for_update(project_id):
290
    try:
291
        return Project.objects.get_for_update(id=project_id)
292
    except Project.DoesNotExist:
293
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
294
        raise ProjectNotFound(m)
295

    
296

    
297
def get_project_of_application_for_update(app_id):
298
    app = get_application(app_id)
299
    return get_project_for_update(app.chain_id)
300

    
301

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

    
309

    
310
def get_project_of_membership_for_update(memb_id):
311
    m = get_membership_by_id(memb_id)
312
    return get_project_for_update(m.project_id)
313

    
314

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

    
322

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

    
330

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

    
339

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

    
348

    
349
ALLOWED_CHECKS = [
350
    (lambda u, a: not u or u.is_project_admin()),
351
    (lambda u, a: a.owner == u),
352
    (lambda u, a: a.applicant == u),
353
    (lambda u, a: a.chain.overall_state() == Project.O_ACTIVE
354
     or bool(a.chain.projectmembership_set.any_accepted().filter(person=u))),
355
]
356

    
357
ADMIN_LEVEL = 0
358
OWNER_LEVEL = 1
359
APPLICANT_LEVEL = 2
360
ANY_LEVEL = 3
361

    
362

    
363
def _check_yield(b, silent=False):
364
    if b:
365
        return True
366

    
367
    if silent:
368
        return False
369

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

    
373

    
374
def membership_check_allowed(membership, request_user,
375
                             level=OWNER_LEVEL, silent=False):
376
    r = project_check_allowed(
377
        membership.project, request_user, level, silent=True)
378

    
379
    return _check_yield(r or membership.person == request_user, silent)
380

    
381

    
382
def project_check_allowed(project, request_user,
383
                          level=OWNER_LEVEL, silent=False):
384
    return app_check_allowed(project.application, request_user, level, silent)
385

    
386

    
387
def app_check_allowed(application, request_user,
388
                      level=OWNER_LEVEL, silent=False):
389
    checks = (f(request_user, application) for f in ALLOWED_CHECKS[:level+1])
390
    return _check_yield(any(checks), silent)
391

    
392

    
393
def checkAlive(project):
394
    if not project.is_alive:
395
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.id
396
        raise ProjectConflict(m)
397

    
398

    
399
def accept_membership_project_checks(project, request_user):
400
    project_check_allowed(project, request_user)
401
    checkAlive(project)
402

    
403
    join_policy = project.application.member_join_policy
404
    if join_policy == CLOSED_POLICY:
405
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
406
        raise ProjectConflict(m)
407

    
408
    if project.violates_members_limit(adding=1):
409
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
410
        raise ProjectConflict(m)
411

    
412

    
413
def accept_membership_checks(membership, request_user):
414
    if not membership.can_accept():
415
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
416
        raise ProjectConflict(m)
417

    
418
    project = membership.project
419
    accept_membership_project_checks(project, request_user)
420

    
421

    
422
def accept_membership(memb_id, request_user=None):
423
    project = get_project_of_membership_for_update(memb_id)
424
    membership = get_membership_by_id(memb_id)
425
    accept_membership_checks(membership, request_user)
426
    user = membership.person
427
    membership.accept()
428
    qh_sync_user(user)
429
    logger.info("User %s has been accepted in %s." %
430
                (user.log_display, project))
431

    
432
    membership_change_notify(project, user, 'accepted')
433
    return membership
434

    
435

    
436
def reject_membership_checks(membership, request_user):
437
    if not membership.can_reject():
438
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
439
        raise ProjectConflict(m)
440

    
441
    project = membership.project
442
    project_check_allowed(project, request_user)
443
    checkAlive(project)
444

    
445

    
446
def reject_membership(memb_id, request_user=None):
447
    project = get_project_of_membership_for_update(memb_id)
448
    membership = get_membership_by_id(memb_id)
449
    reject_membership_checks(membership, request_user)
450
    user = membership.person
451
    membership.reject()
452
    logger.info("Request of user %s for %s has been rejected." %
453
                (user.log_display, project))
454

    
455
    membership_change_notify(project, user, 'rejected')
456
    return membership
457

    
458

    
459
def cancel_membership_checks(membership, request_user):
460
    if not membership.can_cancel():
461
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
462
        raise ProjectConflict(m)
463

    
464
    membership_check_allowed(membership, request_user, level=ADMIN_LEVEL)
465
    project = membership.project
466
    checkAlive(project)
467

    
468

    
469
def cancel_membership(memb_id, request_user):
470
    project = get_project_of_membership_for_update(memb_id)
471
    membership = get_membership_by_id(memb_id)
472
    cancel_membership_checks(membership, request_user)
473
    membership.cancel()
474
    logger.info("Request of user %s for %s has been cancelled." %
475
                (membership.person.log_display, project))
476

    
477

    
478
def remove_membership_checks(membership, request_user=None):
479
    if not membership.can_remove():
480
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
481
        raise ProjectConflict(m)
482

    
483
    project = membership.project
484
    project_check_allowed(project, request_user)
485
    checkAlive(project)
486

    
487
    leave_policy = project.application.member_leave_policy
488
    if leave_policy == CLOSED_POLICY:
489
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
490
        raise ProjectConflict(m)
491

    
492

    
493
def remove_membership(memb_id, request_user=None):
494
    project = get_project_of_membership_for_update(memb_id)
495
    membership = get_membership_by_id(memb_id)
496
    remove_membership_checks(membership, request_user)
497
    user = membership.person
498
    membership.remove()
499
    qh_sync_user(user)
500
    logger.info("User %s has been removed from %s." %
501
                (user.log_display, project))
502

    
503
    membership_change_notify(project, user, 'removed')
504
    return membership
505

    
506

    
507
def enroll_member(project_id, user, request_user=None):
508
    project = get_project_for_update(project_id)
509
    try:
510
        project = get_project_for_update(project_id)
511
    except ProjectNotFound as e:
512
        raise ProjectConflict(e.message)
513
    accept_membership_project_checks(project, request_user)
514

    
515
    try:
516
        membership = get_membership(project_id, user.id)
517
        if not membership.can_enroll():
518
            m = _(astakos_messages.MEMBERSHIP_ACCEPTED)
519
            raise ProjectConflict(m)
520
        membership.join()
521
    except ProjectNotFound:
522
        membership = new_membership(project, user)
523

    
524
    membership.accept()
525
    qh_sync_user(user)
526
    logger.info("User %s has been enrolled in %s." %
527
                (membership.person.log_display, project))
528

    
529
    membership_enroll_notify(project, membership.person)
530
    return membership
531

    
532

    
533
def leave_project_checks(membership, request_user):
534
    if not membership.can_leave():
535
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
536
        raise ProjectConflict(m)
537

    
538
    membership_check_allowed(membership, request_user, level=ADMIN_LEVEL)
539
    project = membership.project
540
    checkAlive(project)
541

    
542
    leave_policy = project.application.member_leave_policy
543
    if leave_policy == CLOSED_POLICY:
544
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
545
        raise ProjectConflict(m)
546

    
547

    
548
def can_leave_request(project, user):
549
    m = user.get_membership(project)
550
    if m is None:
551
        return False
552
    try:
553
        leave_project_checks(m, user)
554
    except ProjectError:
555
        return False
556
    return True
557

    
558

    
559
def leave_project(memb_id, request_user):
560
    project = get_project_of_membership_for_update(memb_id)
561
    membership = get_membership_by_id(memb_id)
562
    leave_project_checks(membership, request_user)
563

    
564
    auto_accepted = False
565
    leave_policy = project.application.member_leave_policy
566
    if leave_policy == AUTO_ACCEPT_POLICY:
567
        membership.remove()
568
        qh_sync_user(request_user)
569
        logger.info("User %s has left %s." %
570
                    (request_user.log_display, project))
571
        auto_accepted = True
572
    else:
573
        membership.leave_request()
574
        logger.info("User %s requested to leave %s." %
575
                    (request_user.log_display, project))
576
        membership_leave_request_notify(project, membership.person)
577
    return auto_accepted
578

    
579

    
580
def join_project_checks(project):
581
    checkAlive(project)
582

    
583
    join_policy = project.application.member_join_policy
584
    if join_policy == CLOSED_POLICY:
585
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
586
        raise ProjectConflict(m)
587

    
588

    
589
def can_join_request(project, user):
590
    try:
591
        join_project_checks(project)
592
    except ProjectError:
593
        return False
594

    
595
    m = user.get_membership(project)
596
    if not m:
597
        return True
598
    return m.can_join()
599

    
600

    
601
def new_membership(project, user):
602
    m = ProjectMembership.objects.create(project=project, person=user)
603
    m._log_create(None, ProjectMembership.REQUESTED)
604
    return m
605

    
606

    
607
def join_project(project_id, request_user):
608
    project = get_project_for_update(project_id)
609
    join_project_checks(project)
610

    
611
    try:
612
        membership = get_membership(project.id, request_user.id)
613
        if not membership.can_join():
614
            msg = _(astakos_messages.MEMBERSHIP_ASSOCIATED)
615
            raise ProjectConflict(msg)
616
        membership.join()
617
    except ProjectNotFound:
618
        membership = new_membership(project, request_user)
619

    
620
    join_policy = project.application.member_join_policy
621
    if (join_policy == AUTO_ACCEPT_POLICY and (
622
            not project.violates_members_limit(adding=1))):
623
        membership.accept()
624
        qh_sync_user(request_user)
625
        logger.info("User %s joined %s." %
626
                    (request_user.log_display, project))
627
    else:
628
        membership_request_notify(project, membership.person)
629
        logger.info("User %s requested to join %s." %
630
                    (request_user.log_display, project))
631
    return membership
632

    
633

    
634
MEMBERSHIP_ACTION_CHECKS = {
635
    "leave":  leave_project_checks,
636
    "cancel": cancel_membership_checks,
637
    "accept": accept_membership_checks,
638
    "reject": reject_membership_checks,
639
    "remove": remove_membership_checks,
640
}
641

    
642

    
643
def membership_allowed_actions(membership, request_user):
644
    allowed = []
645
    for action, check in MEMBERSHIP_ACTION_CHECKS.iteritems():
646
        try:
647
            check(membership, request_user)
648
            allowed.append(action)
649
        except ProjectError:
650
            pass
651
    return allowed
652

    
653

    
654
def submit_application(owner=None,
655
                       name=None,
656
                       project_id=None,
657
                       homepage=None,
658
                       description=None,
659
                       start_date=None,
660
                       end_date=None,
661
                       member_join_policy=None,
662
                       member_leave_policy=None,
663
                       limit_on_members_number=None,
664
                       comments=None,
665
                       resources=None,
666
                       request_user=None):
667

    
668
    project = None
669
    if project_id is not None:
670
        project = get_project_for_update(project_id)
671
        project_check_allowed(project, request_user, level=APPLICANT_LEVEL)
672

    
673
    policies = validate_resource_policies(resources)
674

    
675
    force = request_user.is_project_admin()
676
    ok, limit = qh_add_pending_app(owner, project, force)
677
    if not ok:
678
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
679
        raise ProjectConflict(m)
680

    
681
    application = ProjectApplication(
682
        applicant=request_user,
683
        owner=owner,
684
        name=name,
685
        homepage=homepage,
686
        description=description,
687
        start_date=start_date,
688
        end_date=end_date,
689
        member_join_policy=member_join_policy,
690
        member_leave_policy=member_leave_policy,
691
        limit_on_members_number=limit_on_members_number,
692
        comments=comments)
693

    
694
    if project is None:
695
        chain = new_chain()
696
        application.chain_id = chain.chain
697
        application.save()
698
        Project.objects.create(id=chain.chain, application=application)
699
    else:
700
        application.chain = project
701
        application.save()
702
        if project.application.state != ProjectApplication.APPROVED:
703
            project.application = application
704
            project.save()
705

    
706
        pending = ProjectApplication.objects.filter(
707
            chain=project,
708
            state=ProjectApplication.PENDING).exclude(id=application.id)
709
        for app in pending:
710
            app.state = ProjectApplication.REPLACED
711
            app.save()
712

    
713
    if policies is not None:
714
        set_resource_policies(application, policies)
715
    logger.info("User %s submitted %s." %
716
                (request_user.log_display, application.log_display))
717
    application_submit_notify(application)
718
    return application
719

    
720

    
721
def validate_resource_policies(policies):
722
    if not isinstance(policies, dict):
723
        raise ProjectBadRequest("Malformed resource policies")
724

    
725
    resource_names = policies.keys()
726
    resources = Resource.objects.filter(name__in=resource_names)
727
    resource_d = {}
728
    for resource in resources:
729
        resource_d[resource.name] = resource
730

    
731
    found = resource_d.keys()
732
    nonex = [name for name in resource_names if name not in found]
733
    if nonex:
734
        raise ProjectBadRequest("Malformed resource policies")
735

    
736
    pols = []
737
    for resource_name, specs in policies.iteritems():
738
        p_capacity = specs.get("project_capacity")
739
        m_capacity = specs.get("member_capacity")
740

    
741
        if p_capacity is not None and not isinstance(p_capacity, (int, long)):
742
            raise ProjectBadRequest("Malformed resource policies")
743
        if not isinstance(m_capacity, (int, long)):
744
            raise ProjectBadRequest("Malformed resource policies")
745
        pols.append((resource_d[resource_name], m_capacity, p_capacity))
746
    return pols
747

    
748

    
749
def set_resource_policies(application, policies):
750
    for resource, m_capacity, p_capacity in policies:
751
        g = application.projectresourcegrant_set
752
        g.create(resource=resource,
753
                 member_capacity=m_capacity,
754
                 project_capacity=p_capacity)
755

    
756

    
757
def cancel_application(application_id, request_user=None, reason=""):
758
    get_project_of_application_for_update(application_id)
759
    application = get_application(application_id)
760
    app_check_allowed(application, request_user, level=APPLICANT_LEVEL)
761

    
762
    if not application.can_cancel():
763
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
764
              (application.id, application.state_display()))
765
        raise ProjectConflict(m)
766

    
767
    qh_release_pending_app(application.owner)
768

    
769
    application.cancel()
770
    logger.info("%s has been cancelled." % (application.log_display))
771

    
772

    
773
def dismiss_application(application_id, request_user=None, reason=""):
774
    get_project_of_application_for_update(application_id)
775
    application = get_application(application_id)
776
    app_check_allowed(application, request_user, level=APPLICANT_LEVEL)
777

    
778
    if not application.can_dismiss():
779
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
780
              (application.id, application.state_display()))
781
        raise ProjectConflict(m)
782

    
783
    application.dismiss()
784
    logger.info("%s has been dismissed." % (application.log_display))
785

    
786

    
787
def deny_application(application_id, request_user=None, reason=""):
788
    get_project_of_application_for_update(application_id)
789
    application = get_application(application_id)
790

    
791
    app_check_allowed(application, request_user, level=ADMIN_LEVEL)
792

    
793
    if not application.can_deny():
794
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
795
              (application.id, application.state_display()))
796
        raise ProjectConflict(m)
797

    
798
    qh_release_pending_app(application.owner)
799

    
800
    application.deny(reason)
801
    logger.info("%s has been denied with reason \"%s\"." %
802
                (application.log_display, reason))
803
    application_deny_notify(application)
804

    
805

    
806
def check_conflicting_projects(application):
807
    project = application.chain
808
    new_project_name = application.name
809
    try:
810
        q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
811
        conflicting_project = Project.objects.get(q)
812
        if (conflicting_project != project):
813
            m = (_("cannot approve: project with name '%s' "
814
                   "already exists (id: %s)") %
815
                 (new_project_name, conflicting_project.id))
816
            raise ProjectConflict(m)  # invalid argument
817
    except Project.DoesNotExist:
818
        pass
819

    
820

    
821
def approve_application(app_id, request_user=None, reason=""):
822
    project = get_project_of_application_for_update(app_id)
823
    application = get_application(app_id)
824

    
825
    app_check_allowed(application, request_user, level=ADMIN_LEVEL)
826

    
827
    if not application.can_approve():
828
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
829
              (application.id, application.state_display()))
830
        raise ProjectConflict(m)
831

    
832
    check_conflicting_projects(application)
833

    
834
    # Pre-lock members and owner together in order to impose an ordering
835
    # on locking users
836
    members = members_to_sync(project)
837
    uids_to_sync = [member.id for member in members]
838
    owner = application.owner
839
    uids_to_sync.append(owner.id)
840
    get_users_for_update(uids_to_sync)
841

    
842
    qh_release_pending_app(owner, locked=True)
843
    application.approve(reason)
844
    project.application = application
845
    project.name = application.name
846
    project.save()
847
    if project.is_deactivated():
848
        project.resume()
849
    qh_sync_locked_users(members)
850
    logger.info("%s has been approved." % (application.log_display))
851
    application_approve_notify(application)
852

    
853

    
854
def check_expiration(execute=False):
855
    objects = Project.objects
856
    expired = objects.expired_projects()
857
    if execute:
858
        for project in expired:
859
            terminate(project.pk)
860

    
861
    return [project.expiration_info() for project in expired]
862

    
863

    
864
def terminate(project_id, request_user=None):
865
    project = get_project_for_update(project_id)
866
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
867
    checkAlive(project)
868

    
869
    project.terminate()
870
    qh_sync_project(project)
871
    logger.info("%s has been terminated." % (project))
872

    
873
    project_termination_notify(project)
874

    
875

    
876
def suspend(project_id, request_user=None):
877
    project = get_project_for_update(project_id)
878
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
879
    checkAlive(project)
880

    
881
    project.suspend()
882
    qh_sync_project(project)
883
    logger.info("%s has been suspended." % (project))
884

    
885
    project_suspension_notify(project)
886

    
887

    
888
def resume(project_id, request_user=None):
889
    project = get_project_for_update(project_id)
890
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
891

    
892
    if not project.is_suspended:
893
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.id
894
        raise ProjectConflict(m)
895

    
896
    project.resume()
897
    qh_sync_project(project)
898
    logger.info("%s has been unsuspended." % (project))
899

    
900

    
901
def _partition_by(f, l):
902
    d = {}
903
    for x in l:
904
        group = f(x)
905
        group_l = d.get(group, [])
906
        group_l.append(x)
907
        d[group] = group_l
908
    return d
909

    
910

    
911
def count_pending_app(users):
912
    users = list(users)
913
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
914
                                             owner__in=users)
915
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
916

    
917
    usage = {}
918
    for user in users:
919
        uuid = user.uuid
920
        usage[uuid] = len(apps_d.get(uuid, []))
921
    return usage
922

    
923

    
924
def get_pending_app_diff(user, project):
925
    if project is None:
926
        diff = 1
927
    else:
928
        objs = ProjectApplication.objects
929
        q = objs.filter(chain=project, state=ProjectApplication.PENDING)
930
        count = q.count()
931
        diff = 1 - count
932
    return diff
933

    
934

    
935
def qh_add_pending_app(user, project=None, force=False):
936
    user = AstakosUser.forupdate.get_for_update(id=user.id)
937
    diff = get_pending_app_diff(user, project)
938
    return register_pending_apps(user, diff, force)
939

    
940

    
941
def check_pending_app_quota(user, project=None):
942
    diff = get_pending_app_diff(user, project)
943
    quota = get_pending_app_quota(user)
944
    limit = quota['limit']
945
    usage = quota['usage']
946
    if usage + diff > limit:
947
        return False, limit
948
    return True, None
949

    
950

    
951
def qh_release_pending_app(user, locked=False):
952
    if not locked:
953
        user = AstakosUser.forupdate.get_for_update(id=user.id)
954
    register_pending_apps(user, -1)