Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 74c9dfca

History | View | Annotate | Download (33.2 kB)

1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35

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

    
42
from synnefo_branding.utils import render_to_string
43

    
44
from synnefo.lib import join_urls
45
from astakos.im.models import AstakosUser, Invitation, ProjectMembership, \
46
    ProjectApplication, Project, new_chain, Resource, ProjectLock
47
from astakos.im import quotas
48
from astakos.im import project_notif
49
from astakos.im import settings
50

    
51
import astakos.im.messages as astakos_messages
52

    
53
logger = logging.getLogger(__name__)
54

    
55

    
56
def login(request, user):
57
    auth_login(request, user)
58
    from astakos.im.models import SessionCatalog
59
    SessionCatalog(
60
        session_key=request.session.session_key,
61
        user=user
62
    ).save()
63
    logger.info('%s logged in.', user.log_display)
64

    
65

    
66
def logout(request, *args, **kwargs):
67
    user = request.user
68
    auth_logout(request, *args, **kwargs)
69
    user.delete_online_access_tokens()
70
    logger.info('%s logged out.', user.log_display)
71

    
72

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

    
91

    
92
def _send_admin_notification(template_name,
93
                             context=None,
94
                             user=None,
95
                             msg="",
96
                             subject='alpha2 testing notification',):
97
    """
98
    Send notification email to settings.HELPDESK + settings.MANAGERS +
99
    settings.ADMINS.
100
    """
101
    if context is None:
102
        context = {}
103
    if not 'user' in context:
104
        context['user'] = user
105

    
106
    message = render_to_string(template_name, context)
107
    sender = settings.SERVER_EMAIL
108
    recipient_list = [e[1] for e in settings.HELPDESK +
109
                      settings.MANAGERS + settings.ADMINS]
110
    send_mail(subject, message, sender, recipient_list,
111
              connection=get_connection())
112
    if user:
113
        msg = 'Sent admin notification (%s) for user %s' % (msg,
114
                                                            user.log_display)
115
    else:
116
        msg = 'Sent admin notification (%s)' % msg
117

    
118
    logger.log(settings.LOGGING_LEVEL, msg)
119

    
120

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

    
133

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

    
154

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

    
177

    
178
def send_greeting(user, email_template_name='im/welcome_email.txt'):
179
    """
180
    Send welcome email to an accepted/activated user.
181

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

    
198

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

    
212

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

    
230

    
231
def invite(inviter, email, realname):
232
    inv = Invitation(inviter=inviter, username=email, realname=realname)
233
    inv.save()
234
    send_invitation(inv)
235
    inviter.invitations = max(0, inviter.invitations - 1)
236
    inviter.save()
237

    
238

    
239
### PROJECT FUNCTIONS ###
240

    
241

    
242
class ProjectError(Exception):
243
    pass
244

    
245

    
246
class ProjectNotFound(ProjectError):
247
    pass
248

    
249

    
250
class ProjectForbidden(ProjectError):
251
    pass
252

    
253

    
254
class ProjectBadRequest(ProjectError):
255
    pass
256

    
257

    
258
class ProjectConflict(ProjectError):
259
    pass
260

    
261
AUTO_ACCEPT_POLICY = 1
262
MODERATED_POLICY = 2
263
CLOSED_POLICY = 3
264

    
265
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
266

    
267

    
268
def get_related_project_id(application_id):
269
    try:
270
        app = ProjectApplication.objects.get(id=application_id)
271
        return app.chain_id
272
    except ProjectApplication.DoesNotExist:
273
        return None
274

    
275

    
276
def get_project_by_id(project_id):
277
    try:
278
        return Project.objects.select_related(
279
            "application", "application__owner",
280
            "application__applicant").get(id=project_id)
281
    except Project.DoesNotExist:
282
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
283
        raise ProjectNotFound(m)
284

    
285

    
286
def get_project_for_update(project_id):
287
    try:
288
        return Project.objects.select_for_update().get(id=project_id)
289
    except Project.DoesNotExist:
290
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
291
        raise ProjectNotFound(m)
292

    
293

    
294
def get_project_of_application_for_update(app_id):
295
    app = get_application(app_id)
296
    return get_project_for_update(app.chain_id)
297

    
298

    
299
def get_project_lock():
300
    ProjectLock.objects.select_for_update().get(pk=1)
301

    
302

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

    
310

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

    
315

    
316
def get_user_by_uuid(uuid):
317
    try:
318
        return AstakosUser.objects.get(uuid=uuid)
319
    except AstakosUser.DoesNotExist:
320
        m = _(astakos_messages.UNKNOWN_USER_ID) % uuid
321
        raise ProjectNotFound(m)
322

    
323

    
324
def get_membership(project_id, user_id):
325
    try:
326
        objs = ProjectMembership.objects.select_related('project', 'person')
327
        return objs.get(project__id=project_id, person__id=user_id)
328
    except ProjectMembership.DoesNotExist:
329
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
330
        raise ProjectNotFound(m)
331

    
332

    
333
def get_membership_by_id(memb_id):
334
    try:
335
        objs = ProjectMembership.objects.select_related('project', 'person')
336
        return objs.get(id=memb_id)
337
    except ProjectMembership.DoesNotExist:
338
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
339
        raise ProjectNotFound(m)
340

    
341

    
342
ALLOWED_CHECKS = [
343
    (lambda u, a: not u or u.is_project_admin()),
344
    (lambda u, a: a.owner == u),
345
    (lambda u, a: a.applicant == u),
346
    (lambda u, a: a.chain.overall_state() == Project.O_ACTIVE
347
     or bool(a.chain.projectmembership_set.any_accepted().filter(person=u))),
348
]
349

    
350
ADMIN_LEVEL = 0
351
OWNER_LEVEL = 1
352
APPLICANT_LEVEL = 2
353
ANY_LEVEL = 3
354

    
355

    
356
def _check_yield(b, silent=False):
357
    if b:
358
        return True
359

    
360
    if silent:
361
        return False
362

    
363
    m = _(astakos_messages.NOT_ALLOWED)
364
    raise ProjectForbidden(m)
365

    
366

    
367
def membership_check_allowed(membership, request_user,
368
                             level=OWNER_LEVEL, silent=False):
369
    r = project_check_allowed(
370
        membership.project, request_user, level, silent=True)
371

    
372
    return _check_yield(r or membership.person == request_user, silent)
373

    
374

    
375
def project_check_allowed(project, request_user,
376
                          level=OWNER_LEVEL, silent=False):
377
    return app_check_allowed(project.application, request_user, level, silent)
378

    
379

    
380
def app_check_allowed(application, request_user,
381
                      level=OWNER_LEVEL, silent=False):
382
    checks = (f(request_user, application) for f in ALLOWED_CHECKS[:level+1])
383
    return _check_yield(any(checks), silent)
384

    
385

    
386
def checkAlive(project):
387
    if not project.is_alive:
388
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.id
389
        raise ProjectConflict(m)
390

    
391

    
392
def accept_membership_project_checks(project, request_user):
393
    project_check_allowed(project, request_user)
394
    checkAlive(project)
395

    
396
    join_policy = project.application.member_join_policy
397
    if join_policy == CLOSED_POLICY:
398
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
399
        raise ProjectConflict(m)
400

    
401
    if project.violates_members_limit(adding=1):
402
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
403
        raise ProjectConflict(m)
404

    
405

    
406
def accept_membership_checks(membership, request_user):
407
    if not membership.check_action("accept"):
408
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
409
        raise ProjectConflict(m)
410

    
411
    project = membership.project
412
    accept_membership_project_checks(project, request_user)
413

    
414

    
415
def accept_membership(memb_id, request_user=None, reason=None):
416
    project = get_project_of_membership_for_update(memb_id)
417
    membership = get_membership_by_id(memb_id)
418
    accept_membership_checks(membership, request_user)
419
    user = membership.person
420
    membership.perform_action("accept", actor=request_user, reason=reason)
421
    quotas.qh_sync_user(user)
422
    logger.info("User %s has been accepted in %s." %
423
                (user.log_display, project))
424

    
425
    project_notif.membership_change_notify(project, user, 'accepted')
426
    return membership
427

    
428

    
429
def reject_membership_checks(membership, request_user):
430
    if not membership.check_action("reject"):
431
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
432
        raise ProjectConflict(m)
433

    
434
    project = membership.project
435
    project_check_allowed(project, request_user)
436
    checkAlive(project)
437

    
438

    
439
def reject_membership(memb_id, request_user=None, reason=None):
440
    project = get_project_of_membership_for_update(memb_id)
441
    membership = get_membership_by_id(memb_id)
442
    reject_membership_checks(membership, request_user)
443
    user = membership.person
444
    membership.perform_action("reject", actor=request_user, reason=reason)
445
    logger.info("Request of user %s for %s has been rejected." %
446
                (user.log_display, project))
447

    
448
    project_notif.membership_change_notify(project, user, 'rejected')
449
    return membership
450

    
451

    
452
def cancel_membership_checks(membership, request_user):
453
    if not membership.check_action("cancel"):
454
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
455
        raise ProjectConflict(m)
456

    
457
    membership_check_allowed(membership, request_user, level=ADMIN_LEVEL)
458
    project = membership.project
459
    checkAlive(project)
460

    
461

    
462
def cancel_membership(memb_id, request_user, reason=None):
463
    project = get_project_of_membership_for_update(memb_id)
464
    membership = get_membership_by_id(memb_id)
465
    cancel_membership_checks(membership, request_user)
466
    membership.perform_action("cancel", actor=request_user, reason=reason)
467
    logger.info("Request of user %s for %s has been cancelled." %
468
                (membership.person.log_display, project))
469

    
470

    
471
def remove_membership_checks(membership, request_user=None):
472
    if not membership.check_action("remove"):
473
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
474
        raise ProjectConflict(m)
475

    
476
    project = membership.project
477
    project_check_allowed(project, request_user)
478
    checkAlive(project)
479

    
480
    leave_policy = project.application.member_leave_policy
481
    if leave_policy == CLOSED_POLICY:
482
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
483
        raise ProjectConflict(m)
484

    
485

    
486
def remove_membership(memb_id, request_user=None, reason=None):
487
    project = get_project_of_membership_for_update(memb_id)
488
    membership = get_membership_by_id(memb_id)
489
    remove_membership_checks(membership, request_user)
490
    user = membership.person
491
    membership.perform_action("remove", actor=request_user, reason=reason)
492
    quotas.qh_sync_user(user)
493
    logger.info("User %s has been removed from %s." %
494
                (user.log_display, project))
495

    
496
    project_notif.membership_change_notify(project, user, 'removed')
497
    return membership
498

    
499

    
500
def enroll_member_by_email(project_id, email, request_user=None, reason=None):
501
    try:
502
        user = AstakosUser.objects.accepted().get(email=email)
503
        return enroll_member(project_id, user, request_user, reason=reason)
504
    except AstakosUser.DoesNotExist:
505
        raise ProjectConflict(astakos_messages.UNKNOWN_USERS % email)
506

    
507

    
508
def enroll_member(project_id, user, request_user=None, reason=None):
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.check_action("enroll"):
518
            m = _(astakos_messages.MEMBERSHIP_ACCEPTED)
519
            raise ProjectConflict(m)
520
        membership.perform_action("enroll", actor=request_user, reason=reason)
521
    except ProjectNotFound:
522
        membership = new_membership(project, user, actor=request_user,
523
                                    enroll=True)
524

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

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

    
532

    
533
def leave_project_checks(membership, request_user):
534
    if not membership.check_action("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, reason=None):
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.perform_action("remove", actor=request_user, reason=reason)
568
        quotas.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.perform_action("leave_request", actor=request_user,
574
                                  reason=reason)
575
        logger.info("User %s requested to leave %s." %
576
                    (request_user.log_display, project))
577
        project_notif.membership_request_notify(
578
            project, membership.person, "leave")
579
    return auto_accepted
580

    
581

    
582
def join_project_checks(project):
583
    checkAlive(project)
584

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

    
590

    
591
Nothing = type('Nothing', (), {})
592

    
593

    
594
def can_join_request(project, user, membership=Nothing):
595
    try:
596
        join_project_checks(project)
597
    except ProjectError:
598
        return False
599

    
600
    m = (membership if membership is not Nothing
601
         else user.get_membership(project))
602
    if not m:
603
        return True
604
    return m.check_action("join")
605

    
606

    
607
def new_membership(project, user, actor=None, reason=None, enroll=False):
608
    state = (ProjectMembership.ACCEPTED if enroll
609
             else ProjectMembership.REQUESTED)
610
    m = ProjectMembership.objects.create(
611
        project=project, person=user, state=state)
612
    m._log_create(None, state, actor=actor, reason=reason)
613
    return m
614

    
615

    
616
def join_project(project_id, request_user, reason=None):
617
    project = get_project_for_update(project_id)
618
    join_project_checks(project)
619

    
620
    try:
621
        membership = get_membership(project.id, request_user.id)
622
        if not membership.check_action("join"):
623
            msg = _(astakos_messages.MEMBERSHIP_ASSOCIATED)
624
            raise ProjectConflict(msg)
625
        membership.perform_action("join", actor=request_user, reason=reason)
626
    except ProjectNotFound:
627
        membership = new_membership(project, request_user, actor=request_user,
628
                                    reason=reason)
629

    
630
    join_policy = project.application.member_join_policy
631
    if (join_policy == AUTO_ACCEPT_POLICY and (
632
            not project.violates_members_limit(adding=1))):
633
        membership.perform_action("accept", actor=request_user, reason=reason)
634
        quotas.qh_sync_user(request_user)
635
        logger.info("User %s joined %s." %
636
                    (request_user.log_display, project))
637
    else:
638
        project_notif.membership_request_notify(
639
            project, membership.person, "join")
640
        logger.info("User %s requested to join %s." %
641
                    (request_user.log_display, project))
642
    return membership
643

    
644

    
645
MEMBERSHIP_ACTION_CHECKS = {
646
    "leave":  leave_project_checks,
647
    "cancel": cancel_membership_checks,
648
    "accept": accept_membership_checks,
649
    "reject": reject_membership_checks,
650
    "remove": remove_membership_checks,
651
}
652

    
653

    
654
def membership_allowed_actions(membership, request_user):
655
    allowed = []
656
    for action, check in MEMBERSHIP_ACTION_CHECKS.iteritems():
657
        try:
658
            check(membership, request_user)
659
            allowed.append(action)
660
        except ProjectError:
661
            pass
662
    return allowed
663

    
664

    
665
def submit_application(owner=None,
666
                       name=None,
667
                       project_id=None,
668
                       homepage=None,
669
                       description=None,
670
                       start_date=None,
671
                       end_date=None,
672
                       member_join_policy=None,
673
                       member_leave_policy=None,
674
                       limit_on_members_number=None,
675
                       comments=None,
676
                       resources=None,
677
                       request_user=None):
678

    
679
    project = None
680
    if project_id is not None:
681
        project = get_project_for_update(project_id)
682
        project_check_allowed(project, request_user, level=APPLICANT_LEVEL)
683

    
684
    policies = validate_resource_policies(resources)
685

    
686
    force = request_user.is_project_admin()
687
    ok, limit = qh_add_pending_app(owner, project, force)
688
    if not ok:
689
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
690
        raise ProjectConflict(m)
691

    
692
    application = ProjectApplication(
693
        applicant=request_user,
694
        owner=owner,
695
        name=name,
696
        homepage=homepage,
697
        description=description,
698
        start_date=start_date,
699
        end_date=end_date,
700
        member_join_policy=member_join_policy,
701
        member_leave_policy=member_leave_policy,
702
        limit_on_members_number=limit_on_members_number,
703
        comments=comments)
704

    
705
    if project is None:
706
        chain = new_chain()
707
        application.chain_id = chain.chain
708
        application.save()
709
        Project.objects.create(id=chain.chain, application=application)
710
    else:
711
        application.chain = project
712
        application.save()
713
        if project.application.state != ProjectApplication.APPROVED:
714
            project.application = application
715
            project.save()
716

    
717
        pending = ProjectApplication.objects.filter(
718
            chain=project,
719
            state=ProjectApplication.PENDING).exclude(id=application.id)
720
        for app in pending:
721
            app.state = ProjectApplication.REPLACED
722
            app.save()
723

    
724
    if policies is not None:
725
        set_resource_policies(application, policies)
726
    logger.info("User %s submitted %s." %
727
                (request_user.log_display, application.log_display))
728
    project_notif.application_notify(application, "submit")
729
    return application
730

    
731

    
732
def validate_resource_policies(policies):
733
    if not isinstance(policies, dict):
734
        raise ProjectBadRequest("Malformed resource policies")
735

    
736
    resource_names = policies.keys()
737
    resources = Resource.objects.filter(name__in=resource_names,
738
                                        api_visible=True)
739
    resource_d = {}
740
    for resource in resources:
741
        resource_d[resource.name] = resource
742

    
743
    found = resource_d.keys()
744
    nonex = [name for name in resource_names if name not in found]
745
    if nonex:
746
        raise ProjectBadRequest("Malformed resource policies")
747

    
748
    pols = []
749
    for resource_name, specs in policies.iteritems():
750
        p_capacity = specs.get("project_capacity")
751
        m_capacity = specs.get("member_capacity")
752

    
753
        if p_capacity is not None and not isinstance(p_capacity, (int, long)):
754
            raise ProjectBadRequest("Malformed resource policies")
755
        if not isinstance(m_capacity, (int, long)):
756
            raise ProjectBadRequest("Malformed resource policies")
757
        pols.append((resource_d[resource_name], m_capacity, p_capacity))
758
    return pols
759

    
760

    
761
def set_resource_policies(application, policies):
762
    for resource, m_capacity, p_capacity in policies:
763
        g = application.projectresourcegrant_set
764
        g.create(resource=resource,
765
                 member_capacity=m_capacity,
766
                 project_capacity=p_capacity)
767

    
768

    
769
def cancel_application(application_id, request_user=None, reason=""):
770
    get_project_of_application_for_update(application_id)
771
    application = get_application(application_id)
772
    app_check_allowed(application, request_user, level=APPLICANT_LEVEL)
773

    
774
    if not application.can_cancel():
775
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
776
              (application.id, application.state_display()))
777
        raise ProjectConflict(m)
778

    
779
    qh_release_pending_app(application.owner)
780

    
781
    application.cancel(actor=request_user, reason=reason)
782
    logger.info("%s has been cancelled." % (application.log_display))
783

    
784

    
785
def dismiss_application(application_id, request_user=None, reason=""):
786
    get_project_of_application_for_update(application_id)
787
    application = get_application(application_id)
788
    app_check_allowed(application, request_user, level=APPLICANT_LEVEL)
789

    
790
    if not application.can_dismiss():
791
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
792
              (application.id, application.state_display()))
793
        raise ProjectConflict(m)
794

    
795
    application.dismiss(actor=request_user, reason=reason)
796
    logger.info("%s has been dismissed." % (application.log_display))
797

    
798

    
799
def deny_application(application_id, request_user=None, reason=""):
800
    get_project_of_application_for_update(application_id)
801
    application = get_application(application_id)
802

    
803
    app_check_allowed(application, request_user, level=ADMIN_LEVEL)
804

    
805
    if not application.can_deny():
806
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
807
              (application.id, application.state_display()))
808
        raise ProjectConflict(m)
809

    
810
    qh_release_pending_app(application.owner)
811

    
812
    application.deny(actor=request_user, reason=reason)
813
    logger.info("%s has been denied with reason \"%s\"." %
814
                (application.log_display, reason))
815
    project_notif.application_notify(application, "deny")
816

    
817

    
818
def check_conflicting_projects(application):
819
    project = application.chain
820
    new_project_name = application.name
821
    try:
822
        q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
823
        conflicting_project = Project.objects.get(q)
824
        if (conflicting_project != project):
825
            m = (_("cannot approve: project with name '%s' "
826
                   "already exists (id: %s)") %
827
                 (new_project_name, conflicting_project.id))
828
            raise ProjectConflict(m)  # invalid argument
829
    except Project.DoesNotExist:
830
        pass
831

    
832

    
833
def approve_application(app_id, request_user=None, reason=""):
834
    get_project_lock()
835
    project = get_project_of_application_for_update(app_id)
836
    application = get_application(app_id)
837

    
838
    app_check_allowed(application, request_user, level=ADMIN_LEVEL)
839

    
840
    if not application.can_approve():
841
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
842
              (application.id, application.state_display()))
843
        raise ProjectConflict(m)
844

    
845
    check_conflicting_projects(application)
846

    
847
    # Pre-lock members and owner together in order to impose an ordering
848
    # on locking users
849
    members = quotas.members_to_sync(project)
850
    uids_to_sync = [member.id for member in members]
851
    owner = application.owner
852
    uids_to_sync.append(owner.id)
853
    quotas.get_users_for_update(uids_to_sync)
854

    
855
    qh_release_pending_app(owner, locked=True)
856
    application.approve(actor=request_user, reason=reason)
857
    project.application = application
858
    project.name = application.name
859
    project.save()
860
    if project.is_deactivated():
861
        project.resume(actor=request_user, reason="APPROVE")
862
    quotas.qh_sync_locked_users(members)
863
    logger.info("%s has been approved." % (application.log_display))
864
    project_notif.application_notify(application, "approve")
865

    
866

    
867
def check_expiration(execute=False):
868
    objects = Project.objects
869
    expired = objects.expired_projects()
870
    if execute:
871
        for project in expired:
872
            terminate(project.pk)
873

    
874
    return [project.expiration_info() for project in expired]
875

    
876

    
877
def terminate(project_id, request_user=None, reason=None):
878
    project = get_project_for_update(project_id)
879
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
880
    checkAlive(project)
881

    
882
    project.terminate(actor=request_user, reason=reason)
883
    quotas.qh_sync_project(project)
884
    logger.info("%s has been terminated." % (project))
885

    
886
    project_notif.project_notify(project, "terminate")
887

    
888

    
889
def suspend(project_id, request_user=None, reason=None):
890
    project = get_project_for_update(project_id)
891
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
892
    checkAlive(project)
893

    
894
    project.suspend(actor=request_user, reason=reason)
895
    quotas.qh_sync_project(project)
896
    logger.info("%s has been suspended." % (project))
897

    
898
    project_notif.project_notify(project, "suspend")
899

    
900

    
901
def unsuspend(project_id, request_user=None, reason=None):
902
    project = get_project_for_update(project_id)
903
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
904

    
905
    if not project.is_suspended:
906
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.id
907
        raise ProjectConflict(m)
908

    
909
    project.resume(actor=request_user, reason=reason)
910
    quotas.qh_sync_project(project)
911
    logger.info("%s has been unsuspended." % (project))
912
    project_notif.project_notify(project, "unsuspend")
913

    
914

    
915
def reinstate(project_id, request_user=None, reason=None):
916
    get_project_lock()
917
    project = get_project_for_update(project_id)
918
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
919

    
920
    if not project.is_terminated:
921
        m = _(astakos_messages.NOT_TERMINATED_PROJECT) % project.id
922
        raise ProjectConflict(m)
923

    
924
    check_conflicting_projects(project.application)
925
    project.resume(actor=request_user, reason=reason)
926
    quotas.qh_sync_project(project)
927
    logger.info("%s has been reinstated" % (project))
928
    project_notif.project_notify(project, "reinstate")
929

    
930

    
931
def _partition_by(f, l):
932
    d = {}
933
    for x in l:
934
        group = f(x)
935
        group_l = d.get(group, [])
936
        group_l.append(x)
937
        d[group] = group_l
938
    return d
939

    
940

    
941
def count_pending_app(users):
942
    users = list(users)
943
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
944
                                             owner__in=users)
945
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
946

    
947
    usage = {}
948
    for user in users:
949
        uuid = user.uuid
950
        usage[uuid] = len(apps_d.get(uuid, []))
951
    return usage
952

    
953

    
954
def get_pending_app_diff(project):
955
    if project is None:
956
        diff = 1
957
    else:
958
        objs = ProjectApplication.objects
959
        q = objs.filter(chain=project, state=ProjectApplication.PENDING)
960
        count = q.count()
961
        diff = 1 - count
962
    return diff
963

    
964

    
965
def qh_add_pending_app(user, project=None, force=False):
966
    user = AstakosUser.objects.select_for_update().get(id=user.id)
967
    diff = get_pending_app_diff(project)
968
    return quotas.register_pending_apps(user, diff, force)
969

    
970

    
971
def check_pending_app_quota(user, project=None):
972
    diff = get_pending_app_diff(project)
973
    quota = quotas.get_pending_app_quota(user)
974
    limit = quota['limit']
975
    usage = quota['usage']
976
    if usage + diff > limit:
977
        return False, limit
978
    return True, None
979

    
980

    
981
def qh_release_pending_app(user, locked=False):
982
    if not locked:
983
        user = AstakosUser.objects.select_for_update().get(id=user.id)
984
    quotas.register_pending_apps(user, -1)