Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 3c22bad0

History | View | Annotate | Download (29.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
import socket
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.template import Context, loader
41
from django.contrib.auth import (
42
    login as auth_login,
43
    logout as auth_logout)
44
from django.contrib.auth.models import AnonymousUser
45
from django.core.exceptions import PermissionDenied
46
from django.db import IntegrityError
47
from django.db.models import Q
48
from django.http import Http404
49

    
50
from synnefo_branding.utils import render_to_string
51

    
52
from urllib import quote
53
from smtplib import SMTPException
54
from datetime import datetime
55
from functools import wraps
56

    
57
from synnefo.lib import join_urls
58
from astakos.im.settings import (
59
    CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL,
60
    VERIFICATION_EMAIL_SUBJECT, ACCOUNT_CREATION_SUBJECT,
61
    GROUP_CREATION_SUBJECT, HELPDESK_NOTIFICATION_EMAIL_SUBJECT,
62
    INVITATION_EMAIL_SUBJECT, GREETING_EMAIL_SUBJECT, FEEDBACK_EMAIL_SUBJECT,
63
    EMAIL_CHANGE_EMAIL_SUBJECT,
64
    )
65
from astakos.im.notifications import build_notification, NotificationError
66
from astakos.im.models import (
67
    AstakosUser, Invitation, ProjectMembership, ProjectApplication, Project,
68
    UserSetting, Chain, new_chain)
69
from astakos.im.quotas import (qh_sync_user, get_pending_app_quota,
70
                               register_pending_apps, qh_sync_project,
71
                               qh_sync_locked_users, get_users_for_update,
72
                               members_to_sync)
73
from astakos.im.project_notif import (
74
    membership_change_notify, membership_enroll_notify,
75
    membership_request_notify, membership_leave_request_notify,
76
    application_submit_notify, application_approve_notify,
77
    application_deny_notify,
78
    project_termination_notify, project_suspension_notify)
79
from astakos.im import settings
80
import astakos.im.messages as astakos_messages
81

    
82
logger = logging.getLogger(__name__)
83

    
84

    
85
def login(request, user):
86
    auth_login(request, user)
87
    from astakos.im.models import SessionCatalog
88
    SessionCatalog(
89
        session_key=request.session.session_key,
90
        user=user
91
    ).save()
92
    logger.info('%s logged in.', user.log_display)
93

    
94

    
95
def logout(request, *args, **kwargs):
96
    user = request.user
97
    auth_logout(request, *args, **kwargs)
98
    logger.info('%s logged out.', user.log_display)
99

    
100

    
101
def send_verification(user, template_name='im/activation_email.txt'):
102
    """
103
    Send email to user to verify his/her email and activate his/her account.
104
    """
105
    url = join_urls(BASEURL, user.get_activation_url(nxt=reverse('index')))
106
    message = render_to_string(template_name, {
107
                               'user': user,
108
                               'url': url,
109
                               'baseurl': BASEURL,
110
                               'site_name': SITENAME,
111
                               'support': CONTACT_EMAIL})
112
    sender = settings.SERVER_EMAIL
113
    send_mail(_(VERIFICATION_EMAIL_SUBJECT), message, sender, [user.email],
114
              connection=get_connection())
115
    logger.info("Sent user verirfication email: %s", user.log_display)
116

    
117

    
118
def _send_admin_notification(template_name,
119
                             context=None,
120
                             user=None,
121
                             msg="",
122
                             subject='alpha2 testing notification',):
123
    """
124
    Send notification email to settings.HELPDESK + settings.MANAGERS +
125
    settings.ADMINS.
126
    """
127
    if context is None:
128
        context = {}
129
    if not 'user' in context:
130
        context['user'] = user
131

    
132
    message = render_to_string(template_name, context)
133
    sender = settings.SERVER_EMAIL
134
    recipient_list = [e[1] for e in settings.HELPDESK +
135
                      settings.MANAGERS + settings.ADMINS]
136
    send_mail(subject, message, sender, recipient_list,
137
              connection=get_connection())
138
    if user:
139
        msg = 'Sent admin notification (%s) for user %s' % (msg,
140
                                                            user.log_display)
141
    else:
142
        msg = 'Sent admin notification (%s)' % msg
143

    
144
    logger.log(LOGGING_LEVEL, msg)
145

    
146

    
147
def send_account_pending_moderation_notification(
148
        user,
149
        template_name='im/account_pending_moderation_notification.txt'):
150
    """
151
    Notify admins that a new user has verified his email address and moderation
152
    step is required to activate his account.
153
    """
154
    subject = _(ACCOUNT_CREATION_SUBJECT) % {'user': user.email}
155
    return _send_admin_notification(template_name, {}, subject=subject,
156
                                    user=user, msg="account creation")
157

    
158

    
159
def send_account_activated_notification(
160
        user,
161
        template_name='im/account_activated_notification.txt'):
162
    """
163
    Send email to settings.HELPDESK + settings.MANAGERES + settings.ADMINS
164
    lists to notify that a new account has been accepted and activated.
165
    """
166
    message = render_to_string(
167
        template_name,
168
        {'user': user}
169
    )
170
    sender = settings.SERVER_EMAIL
171
    recipient_list = [e[1] for e in settings.HELPDESK +
172
                      settings.MANAGERS + settings.ADMINS]
173
    send_mail(_(HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % {'user': user.email},
174
              message, sender, recipient_list, connection=get_connection())
175
    msg = 'Sent helpdesk admin notification for %s' % user.email
176
    logger.log(LOGGING_LEVEL, msg)
177

    
178

    
179
def send_invitation(invitation, template_name='im/invitation.txt'):
180
    """
181
    Send invitation email.
182
    """
183
    subject = _(INVITATION_EMAIL_SUBJECT)
184
    url = '%s?code=%d' % (join_urls(BASEURL, reverse('index')), invitation.code)
185
    message = render_to_string(template_name, {
186
                               'invitation': invitation,
187
                               'url': url,
188
                               'baseurl': BASEURL,
189
                               'site_name': SITENAME,
190
                               'support': CONTACT_EMAIL})
191
    sender = settings.SERVER_EMAIL
192
    send_mail(subject, message, sender, [invitation.username],
193
              connection=get_connection())
194
    msg = 'Sent invitation %s' % invitation
195
    logger.log(LOGGING_LEVEL, msg)
196
    inviter_invitations = invitation.inviter.invitations
197
    invitation.inviter.invitations = max(0, inviter_invitations - 1)
198
    invitation.inviter.save()
199

    
200

    
201
def send_greeting(user, email_template_name='im/welcome_email.txt'):
202
    """
203
    Send welcome email to an accepted/activated user.
204

205
    Raises SMTPException, socket.error
206
    """
207
    subject = _(GREETING_EMAIL_SUBJECT)
208
    message = render_to_string(email_template_name, {
209
                               'user': user,
210
                               'url': join_urls(BASEURL, reverse('index')),
211
                               'baseurl': BASEURL,
212
                               'site_name': SITENAME,
213
                               'support': CONTACT_EMAIL})
214
    sender = settings.SERVER_EMAIL
215
    send_mail(subject, message, sender, [user.email],
216
              connection=get_connection())
217
    msg = 'Sent greeting %s' % user.log_display
218
    logger.log(LOGGING_LEVEL, msg)
219

    
220

    
221
def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'):
222
    subject = _(FEEDBACK_EMAIL_SUBJECT)
223
    from_email = settings.SERVER_EMAIL
224
    recipient_list = [e[1] for e in settings.HELPDESK]
225
    content = render_to_string(email_template_name, {
226
        'message': msg,
227
        'data': data,
228
        'user': user})
229
    send_mail(subject, content, from_email, recipient_list,
230
              connection=get_connection())
231
    msg = 'Sent feedback from %s' % user.log_display
232
    logger.log(LOGGING_LEVEL, msg)
233

    
234

    
235
def send_change_email(
236
    ec, request, email_template_name='registration/email_change_email.txt'):
237
    url = ec.get_url()
238
    url = request.build_absolute_uri(url)
239
    c = {'url': url, 'site_name': SITENAME, 'support': CONTACT_EMAIL,
240
         'ec': ec}
241
    message = render_to_string(email_template_name, c)
242
    from_email = settings.SERVER_EMAIL
243
    send_mail(_(EMAIL_CHANGE_EMAIL_SUBJECT), message, from_email,
244
              [ec.new_email_address], connection=get_connection())
245
    msg = 'Sent change email for %s' % ec.user.log_display
246
    logger.log(LOGGING_LEVEL, msg)
247

    
248

    
249
def invite(inviter, email, realname):
250
    inv = Invitation(inviter=inviter, username=email, realname=realname)
251
    inv.save()
252
    send_invitation(inv)
253
    inviter.invitations = max(0, inviter.invitations - 1)
254
    inviter.save()
255

    
256

    
257
### PROJECT FUNCTIONS ###
258

    
259
AUTO_ACCEPT_POLICY = 1
260
MODERATED_POLICY = 2
261
CLOSED_POLICY = 3
262

    
263
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
264

    
265

    
266
def get_project_by_application_id(project_application_id):
267
    try:
268
        return Project.objects.get(application__id=project_application_id)
269
    except Project.DoesNotExist:
270
        m = (_(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) %
271
             project_application_id)
272
        raise IOError(m)
273

    
274

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

    
284

    
285
def get_chain_of_application_id(application_id):
286
    try:
287
        app = ProjectApplication.objects.get(id=application_id)
288
        chain = app.chain
289
        return chain.chain
290
    except ProjectApplication.DoesNotExist:
291
        return None
292

    
293

    
294
def get_project_by_id(project_id):
295
    try:
296
        return Project.objects.get(id=project_id)
297
    except Project.DoesNotExist:
298
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
299
        raise IOError(m)
300

    
301

    
302
def get_project_by_name(name):
303
    try:
304
        return Project.objects.get(name=name)
305
    except Project.DoesNotExist:
306
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % name
307
        raise IOError(m)
308

    
309

    
310
def get_chain_for_update(chain_id):
311
    try:
312
        return Chain.objects.get_for_update(chain=chain_id)
313
    except Chain.DoesNotExist:
314
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % chain_id
315
        raise IOError(m)
316

    
317

    
318
def get_chain_of_application_for_update(app_id):
319
    app = get_application(app_id)
320
    return Chain.objects.get_for_update(chain=app.chain_id)
321

    
322

    
323
def get_application(application_id):
324
    try:
325
        return ProjectApplication.objects.get(id=application_id)
326
    except ProjectApplication.DoesNotExist:
327
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % application_id
328
        raise IOError(m)
329

    
330

    
331
def get_user_by_id(user_id):
332
    try:
333
        return AstakosUser.objects.get(id=user_id)
334
    except AstakosUser.DoesNotExist:
335
        m = _(astakos_messages.UNKNOWN_USER_ID) % user_id
336
        raise IOError(m)
337

    
338

    
339
def get_user_by_uuid(uuid):
340
    try:
341
        return AstakosUser.objects.get(uuid=uuid)
342
    except AstakosUser.DoesNotExist:
343
        m = _(astakos_messages.UNKNOWN_USER_ID) % uuid
344
        raise IOError(m)
345

    
346

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

    
355

    
356
def get_membership_by_id(project_id, memb_id):
357
    try:
358
        objs = ProjectMembership.objects.select_related('project', 'person')
359
        return objs.get(project__id=project_id, id=memb_id)
360
    except ProjectMembership.DoesNotExist:
361
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
362
        raise IOError(m)
363

    
364

    
365
def checkAllowed(entity, request_user, admin_only=False):
366
    if isinstance(entity, Project):
367
        application = entity.application
368
    elif isinstance(entity, ProjectApplication):
369
        application = entity
370
    else:
371
        m = "%s not a Project nor a ProjectApplication" % (entity,)
372
        raise ValueError(m)
373

    
374
    if not request_user or request_user.is_project_admin():
375
        return
376

    
377
    if not admin_only and application.owner == request_user:
378
        return
379

    
380
    m = _(astakos_messages.NOT_ALLOWED)
381
    raise PermissionDenied(m)
382

    
383

    
384
def checkAlive(project):
385
    if not project.is_alive:
386
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__
387
        raise PermissionDenied(m)
388

    
389

    
390
def accept_membership_checks(project, request_user):
391
    checkAllowed(project, request_user)
392
    checkAlive(project)
393

    
394
    join_policy = project.application.member_join_policy
395
    if join_policy == CLOSED_POLICY:
396
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
397
        raise PermissionDenied(m)
398

    
399
    if project.violates_members_limit(adding=1):
400
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
401
        raise PermissionDenied(m)
402

    
403

    
404
def accept_membership(project_id, memb_id, request_user=None):
405
    get_chain_for_update(project_id)
406

    
407
    membership = get_membership_by_id(project_id, memb_id)
408
    if not membership.can_accept():
409
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
410
        raise PermissionDenied(m)
411

    
412
    project = membership.project
413
    accept_membership_checks(project, request_user)
414
    user = membership.person
415
    membership.accept()
416
    qh_sync_user(user)
417
    logger.info("User %s has been accepted in %s." %
418
                (user.log_display, project))
419

    
420
    membership_change_notify(project, user, 'accepted')
421
    return membership
422

    
423

    
424
def reject_membership_checks(project, request_user):
425
    checkAllowed(project, request_user)
426
    checkAlive(project)
427

    
428

    
429
def reject_membership(project_id, memb_id, request_user=None):
430
    get_chain_for_update(project_id)
431

    
432
    membership = get_membership_by_id(project_id, memb_id)
433
    if not membership.can_reject():
434
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
435
        raise PermissionDenied(m)
436

    
437
    project = membership.project
438
    reject_membership_checks(project, request_user)
439
    user = membership.person
440
    membership.reject()
441
    logger.info("Request of user %s for %s has been rejected." %
442
                (user.log_display, project))
443

    
444
    membership_change_notify(project, user, 'rejected')
445
    return membership
446

    
447

    
448
def cancel_membership_checks(project):
449
    checkAlive(project)
450

    
451

    
452
def cancel_membership(project_id, request_user):
453
    get_chain_for_update(project_id)
454

    
455
    membership = get_membership(project_id, request_user.id)
456
    if not membership.can_cancel():
457
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
458
        raise PermissionDenied(m)
459

    
460
    project = membership.project
461
    cancel_membership_checks(project)
462
    membership.cancel()
463
    logger.info("Request of user %s for %s has been cancelled." %
464
                (membership.person.log_display, project))
465

    
466

    
467
def remove_membership_checks(project, request_user=None):
468
    checkAllowed(project, request_user)
469
    checkAlive(project)
470

    
471
    leave_policy = project.application.member_leave_policy
472
    if leave_policy == CLOSED_POLICY:
473
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
474
        raise PermissionDenied(m)
475

    
476

    
477
def remove_membership(project_id, memb_id, request_user=None):
478
    get_chain_for_update(project_id)
479

    
480
    membership = get_membership_by_id(project_id, memb_id)
481
    if not membership.can_remove():
482
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
483
        raise PermissionDenied(m)
484

    
485
    project = membership.project
486
    remove_membership_checks(project, request_user)
487
    user = membership.person
488
    membership.remove()
489
    qh_sync_user(user)
490
    logger.info("User %s has been removed from %s." %
491
                (user.log_display, project))
492

    
493
    membership_change_notify(project, user, 'removed')
494
    return membership
495

    
496

    
497
def enroll_member(project_id, user, request_user=None):
498
    get_chain_for_update(project_id)
499
    project = get_project_by_id(project_id)
500
    accept_membership_checks(project, request_user)
501

    
502
    membership, created = ProjectMembership.objects.get_or_create(
503
        project=project,
504
        person=user)
505

    
506
    if not membership.can_accept():
507
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
508
        raise PermissionDenied(m)
509

    
510
    membership.accept()
511
    qh_sync_user(user)
512
    logger.info("User %s has been enrolled in %s." %
513
                (membership.person.log_display, project))
514

    
515
    membership_enroll_notify(project, membership.person)
516
    return membership
517

    
518

    
519
def leave_project_checks(project):
520
    checkAlive(project)
521

    
522
    leave_policy = project.application.member_leave_policy
523
    if leave_policy == CLOSED_POLICY:
524
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
525
        raise PermissionDenied(m)
526

    
527

    
528
def can_leave_request(project, user):
529
    leave_policy = project.application.member_leave_policy
530
    if leave_policy == CLOSED_POLICY:
531
        return False
532
    m = user.get_membership(project)
533
    if m is None:
534
        return False
535
    if m.state != ProjectMembership.ACCEPTED:
536
        return False
537
    return True
538

    
539

    
540
def leave_project(project_id, request_user):
541
    get_chain_for_update(project_id)
542

    
543
    membership = get_membership(project_id, request_user.id)
544
    if not membership.can_leave():
545
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
546
        raise PermissionDenied(m)
547

    
548
    project = membership.project
549
    leave_project_checks(project)
550

    
551
    auto_accepted = False
552
    leave_policy = project.application.member_leave_policy
553
    if leave_policy == AUTO_ACCEPT_POLICY:
554
        membership.remove()
555
        qh_sync_user(request_user)
556
        logger.info("User %s has left %s." %
557
                    (request_user.log_display, project))
558
        auto_accepted = True
559
    else:
560
        membership.leave_request()
561
        logger.info("User %s requested to leave %s." %
562
                    (request_user.log_display, project))
563
        membership_leave_request_notify(project, membership.person)
564
    return auto_accepted
565

    
566

    
567
def join_project_checks(project):
568
    checkAlive(project)
569

    
570
    join_policy = project.application.member_join_policy
571
    if join_policy == CLOSED_POLICY:
572
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
573
        raise PermissionDenied(m)
574

    
575

    
576
def can_join_request(project, user):
577
    join_policy = project.application.member_join_policy
578
    if join_policy == CLOSED_POLICY:
579
        return False
580
    m = user.get_membership(project)
581
    if m:
582
        return False
583
    return True
584

    
585

    
586
def join_project(project_id, request_user):
587
    get_chain_for_update(project_id)
588
    project = get_project_by_id(project_id)
589
    join_project_checks(project)
590

    
591
    membership, created = ProjectMembership.objects.get_or_create(
592
        project=project,
593
        person=request_user)
594

    
595
    if not created:
596
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
597
        raise PermissionDenied(msg)
598

    
599
    auto_accepted = False
600
    join_policy = project.application.member_join_policy
601
    if (join_policy == AUTO_ACCEPT_POLICY and (
602
            not project.violates_members_limit(adding=1))):
603
        membership.accept()
604
        qh_sync_user(request_user)
605
        logger.info("User %s joined %s." %
606
                    (request_user.log_display, project))
607
        auto_accepted = True
608
    else:
609
        membership_request_notify(project, membership.person)
610
        logger.info("User %s requested to join %s." %
611
                    (request_user.log_display, project))
612
    return auto_accepted
613

    
614

    
615
def submit_application(owner=None,
616
                       name=None,
617
                       precursor_id=None,
618
                       homepage=None,
619
                       description=None,
620
                       start_date=None,
621
                       end_date=None,
622
                       member_join_policy=None,
623
                       member_leave_policy=None,
624
                       limit_on_members_number=None,
625
                       comments=None,
626
                       resource_policies=None,
627
                       request_user=None):
628

    
629
    precursor = None
630
    if precursor_id is not None:
631
        get_chain_of_application_for_update(precursor_id)
632
        precursor = ProjectApplication.objects.get(id=precursor_id)
633

    
634
        if (request_user and
635
            (not precursor.owner == request_user and
636
             not request_user.is_superuser
637
             and not request_user.is_project_admin())):
638
            m = _(astakos_messages.NOT_ALLOWED)
639
            raise PermissionDenied(m)
640

    
641
    force = request_user.is_project_admin()
642
    ok, limit = qh_add_pending_app(owner, precursor, force)
643
    if not ok:
644
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
645
        raise PermissionDenied(m)
646

    
647
    application = ProjectApplication(
648
        applicant=request_user,
649
        owner=owner,
650
        name=name,
651
        precursor_application_id=precursor_id,
652
        homepage=homepage,
653
        description=description,
654
        start_date=start_date,
655
        end_date=end_date,
656
        member_join_policy=member_join_policy,
657
        member_leave_policy=member_leave_policy,
658
        limit_on_members_number=limit_on_members_number,
659
        comments=comments)
660

    
661
    if precursor is None:
662
        application.chain = new_chain()
663
    else:
664
        chain = precursor.chain
665
        application.chain = chain
666
        objs = ProjectApplication.objects
667
        pending = objs.filter(chain=chain, state=ProjectApplication.PENDING)
668
        for app in pending:
669
            app.state = ProjectApplication.REPLACED
670
            app.save()
671

    
672
    application.save()
673
    if resource_policies is not None:
674
        application.set_resource_policies(resource_policies)
675
    logger.info("User %s submitted %s." %
676
                (request_user.log_display, application.log_display))
677
    application_submit_notify(application)
678
    return application
679

    
680

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

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

    
691
    qh_release_pending_app(application.owner)
692

    
693
    application.cancel()
694
    logger.info("%s has been cancelled." % (application.log_display))
695

    
696

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

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

    
707
    application.dismiss()
708
    logger.info("%s has been dismissed." % (application.log_display))
709

    
710

    
711
def deny_application(application_id, request_user=None, reason=""):
712
    get_chain_of_application_for_update(application_id)
713
    application = get_application(application_id)
714

    
715
    checkAllowed(application, request_user, admin_only=True)
716

    
717
    if not application.can_deny():
718
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
719
              (application.id, application.state_display()))
720
        raise PermissionDenied(m)
721

    
722
    qh_release_pending_app(application.owner)
723

    
724
    application.deny(reason)
725
    logger.info("%s has been denied with reason \"%s\"." %
726
                (application.log_display, reason))
727
    application_deny_notify(application)
728

    
729

    
730
def check_conflicting_projects(application):
731
    try:
732
        project = get_project_by_id(application.chain)
733
    except IOError:
734
        project = None
735

    
736
    new_project_name = application.name
737
    try:
738
        q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
739
        conflicting_project = Project.objects.get(q)
740
        if (conflicting_project != project):
741
            m = (_("cannot approve: project with name '%s' "
742
                   "already exists (id: %s)") % (
743
                    new_project_name, conflicting_project.id))
744
            raise PermissionDenied(m) # invalid argument
745
    except Project.DoesNotExist:
746
        pass
747

    
748
    return project
749

    
750

    
751
def approve_application(app_id, request_user=None, reason=""):
752
    get_chain_of_application_for_update(app_id)
753
    application = get_application(app_id)
754

    
755
    checkAllowed(application, request_user, admin_only=True)
756

    
757
    if not application.can_approve():
758
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
759
              (application.id, application.state_display()))
760
        raise PermissionDenied(m)
761

    
762
    project = check_conflicting_projects(application)
763

    
764
    # Pre-lock members and owner together in order to impose an ordering
765
    # on locking users
766
    members = members_to_sync(project) if project is not None else []
767
    uids_to_sync = [member.id for member in members]
768
    owner = application.owner
769
    uids_to_sync.append(owner.id)
770
    get_users_for_update(uids_to_sync)
771

    
772
    qh_release_pending_app(owner, locked=True)
773
    application.approve(reason)
774
    qh_sync_locked_users(members)
775
    logger.info("%s has been approved." % (application.log_display))
776
    application_approve_notify(application)
777

    
778

    
779
def check_expiration(execute=False):
780
    objects = Project.objects
781
    expired = objects.expired_projects()
782
    if execute:
783
        for project in expired:
784
            terminate(project.id)
785

    
786
    return [project.expiration_info() for project in expired]
787

    
788

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

    
795
    project.terminate()
796
    qh_sync_project(project)
797
    logger.info("%s has been terminated." % (project))
798

    
799
    project_termination_notify(project)
800

    
801

    
802
def suspend(project_id, request_user=None):
803
    get_chain_for_update(project_id)
804
    project = get_project_by_id(project_id)
805
    checkAllowed(project, request_user, admin_only=True)
806
    checkAlive(project)
807

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

    
812
    project_suspension_notify(project)
813

    
814

    
815
def resume(project_id, request_user=None):
816
    get_chain_for_update(project_id)
817
    project = get_project_by_id(project_id)
818
    checkAllowed(project, request_user, admin_only=True)
819

    
820
    if not project.is_suspended:
821
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.__dict__
822
        raise PermissionDenied(m)
823

    
824
    project.resume()
825
    qh_sync_project(project)
826
    logger.info("%s has been unsuspended." % (project))
827

    
828

    
829
def get_by_chain_or_404(chain_id):
830
    try:
831
        project = Project.objects.get(id=chain_id)
832
        application = project.application
833
        return project, application
834
    except:
835
        application = ProjectApplication.objects.latest_of_chain(chain_id)
836
        if application is None:
837
            raise Http404
838
        else:
839
            return None, application
840

    
841

    
842
def get_user_setting(user_id, key):
843
    try:
844
        setting = UserSetting.objects.get(
845
            user=user_id, setting=key)
846
        return setting.value
847
    except UserSetting.DoesNotExist:
848
        return getattr(settings, key)
849

    
850

    
851
def set_user_setting(user_id, key, value):
852
    try:
853
        setting = UserSetting.objects.get_for_update(
854
            user=user_id, setting=key)
855
    except UserSetting.DoesNotExist:
856
        setting = UserSetting(user_id=user_id, setting=key)
857
    setting.value = value
858
    setting.save()
859

    
860

    
861
def unset_user_setting(user_id, key):
862
    UserSetting.objects.filter(user=user_id, setting=key).delete()
863

    
864

    
865
def _partition_by(f, l):
866
    d = {}
867
    for x in l:
868
        group = f(x)
869
        group_l = d.get(group, [])
870
        group_l.append(x)
871
        d[group] = group_l
872
    return d
873

    
874

    
875
def count_pending_app(users):
876
    users = list(users)
877
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
878
                                             owner__in=users)
879
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
880

    
881
    usage = {}
882
    for user in users:
883
        uuid = user.uuid
884
        usage[uuid] = len(apps_d.get(uuid, []))
885
    return usage
886

    
887

    
888
def get_pending_app_diff(user, precursor):
889
    if precursor is None:
890
        diff = 1
891
    else:
892
        chain = precursor.chain
893
        objs = ProjectApplication.objects
894
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
895
        count = q.count()
896
        diff = 1 - count
897
    return diff
898

    
899

    
900
def qh_add_pending_app(user, precursor=None, force=False):
901
    user = AstakosUser.forupdate.get_for_update(id=user.id)
902
    diff = get_pending_app_diff(user, precursor)
903
    return register_pending_apps(user, diff, force)
904

    
905

    
906
def check_pending_app_quota(user, precursor=None):
907
    diff = get_pending_app_diff(user, precursor)
908
    quota = get_pending_app_quota(user)
909
    limit = quota['limit']
910
    usage = quota['usage']
911
    if usage + diff > limit:
912
        return False, limit
913
    return True, None
914

    
915

    
916
def qh_release_pending_app(user, locked=False):
917
    if not locked:
918
        user = AstakosUser.forupdate.get_for_update(id=user.id)
919
    register_pending_apps(user, -1)