Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 9efd0075

History | View | Annotate | Download (40.8 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
from dateutil.relativedelta import relativedelta
37

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

    
44
from synnefo_branding.utils import render_to_string
45

    
46
from synnefo.lib import join_urls
47
from astakos.im.models import AstakosUser, Invitation, ProjectMembership, \
48
    ProjectApplication, Project, new_chain, Resource, ProjectLock, \
49
    create_project, ProjectResourceQuota, ProjectResourceGrant
50
from astakos.im import quotas
51
from astakos.im import project_notif
52
from astakos.im import settings
53

    
54
import astakos.im.messages as astakos_messages
55

    
56
logger = logging.getLogger(__name__)
57

    
58

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

    
68

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

    
75

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

    
94

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

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

    
121
    logger.log(settings.LOGGING_LEVEL, msg)
122

    
123

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

    
136

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

    
157

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

    
180

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

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

    
201

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

    
215

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

    
233

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

    
241

    
242
### PROJECT FUNCTIONS ###
243

    
244

    
245
class ProjectError(Exception):
246
    pass
247

    
248

    
249
class ProjectNotFound(ProjectError):
250
    pass
251

    
252

    
253
class ProjectForbidden(ProjectError):
254
    pass
255

    
256

    
257
class ProjectBadRequest(ProjectError):
258
    pass
259

    
260

    
261
class ProjectConflict(ProjectError):
262
    pass
263

    
264
AUTO_ACCEPT_POLICY = 1
265
MODERATED_POLICY = 2
266
CLOSED_POLICY = 3
267

    
268
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
269

    
270

    
271
def get_related_project_id(application_id):
272
    try:
273
        app = ProjectApplication.objects.get(id=application_id)
274
        return app.chain_id
275
    except ProjectApplication.DoesNotExist:
276
        return None
277

    
278

    
279
def get_project_by_id(project_id):
280
    try:
281
        return Project.objects.select_related(
282
            "application", "application__owner",
283
            "application__applicant").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_by_uuid(uuid):
290
    try:
291
        return Project.objects.get(uuid=uuid)
292
    except Project.DoesNotExist:
293
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % uuid
294
        raise ProjectNotFound(m)
295

    
296

    
297
def get_project_for_update(project_id):
298
    try:
299
        try:
300
            project_id = int(project_id)
301
            return Project.objects.select_for_update().get(id=project_id)
302
        except ValueError:
303
            return Project.objects.select_for_update().get(uuid=project_id)
304
    except Project.DoesNotExist:
305
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
306
        raise ProjectNotFound(m)
307

    
308

    
309
def get_project_of_application_for_update(app_id):
310
    app = get_application(app_id)
311
    return get_project_for_update(app.chain_id)
312

    
313

    
314
def get_project_lock():
315
    ProjectLock.objects.select_for_update().get(pk=1)
316

    
317

    
318
def get_application(application_id):
319
    try:
320
        return ProjectApplication.objects.get(id=application_id)
321
    except ProjectApplication.DoesNotExist:
322
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % application_id
323
        raise ProjectNotFound(m)
324

    
325

    
326
def get_project_of_membership_for_update(memb_id):
327
    m = get_membership_by_id(memb_id)
328
    return get_project_for_update(m.project_id)
329

    
330

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

    
338

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

    
347

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

    
356

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

    
362

    
363
def is_admin(user):
364
    return not user or user.is_project_admin()
365

    
366

    
367
def _failure(silent=False):
368
    if silent:
369
        return False
370

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

    
374

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

    
380
    if r or membership.person == request_user:
381
        return True
382
    return _failure(silent)
383

    
384

    
385
def project_check_allowed(project, request_user,
386
                          level=OWNER_LEVEL, silent=False):
387
    if is_admin(request_user):
388
        return True
389
    if level <= ADMIN_LEVEL:
390
        return _failure(silent)
391

    
392
    if project.owner == request_user:
393
        return True
394
    if level <= OWNER_LEVEL:
395
        return _failure(silent)
396

    
397
    if project.state == Project.NORMAL and not project.private \
398
            or bool(project.projectmembership_set.any_accepted().
399
                    filter(person=request_user)):
400
            return True
401
    return _failure(silent)
402

    
403

    
404
def app_check_allowed(application, request_user,
405
                      level=OWNER_LEVEL, silent=False):
406
    if is_admin(request_user):
407
        return True
408
    if level <= ADMIN_LEVEL:
409
        return _failure(silent)
410

    
411
    if application.applicant == request_user:
412
        return True
413
    return _failure(silent)
414

    
415

    
416
def checkAlive(project):
417
    if not project.is_alive:
418
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.uuid
419
        raise ProjectConflict(m)
420

    
421

    
422
def accept_membership_project_checks(project, request_user):
423
    project_check_allowed(project, request_user)
424
    checkAlive(project)
425

    
426
    join_policy = project.member_join_policy
427
    if join_policy == CLOSED_POLICY:
428
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
429
        raise ProjectConflict(m)
430

    
431
    if project.violates_members_limit(adding=1):
432
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
433
        raise ProjectConflict(m)
434

    
435

    
436
def accept_membership_checks(membership, request_user):
437
    if not membership.check_action("accept"):
438
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
439
        raise ProjectConflict(m)
440

    
441
    project = membership.project
442
    accept_membership_project_checks(project, request_user)
443

    
444

    
445
def accept_membership(memb_id, request_user=None, reason=None):
446
    project = get_project_of_membership_for_update(memb_id)
447
    membership = get_membership_by_id(memb_id)
448
    accept_membership_checks(membership, request_user)
449
    user = membership.person
450
    membership.perform_action("accept", actor=request_user, reason=reason)
451
    quotas.qh_sync_membership(membership)
452
    logger.info("User %s has been accepted in %s." %
453
                (user.log_display, project))
454

    
455
    project_notif.membership_change_notify(project, user, 'accepted')
456
    return membership
457

    
458

    
459
def reject_membership_checks(membership, request_user):
460
    if not membership.check_action("reject"):
461
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
462
        raise ProjectConflict(m)
463

    
464
    project = membership.project
465
    project_check_allowed(project, request_user)
466
    checkAlive(project)
467

    
468

    
469
def reject_membership(memb_id, request_user=None, reason=None):
470
    project = get_project_of_membership_for_update(memb_id)
471
    membership = get_membership_by_id(memb_id)
472
    reject_membership_checks(membership, request_user)
473
    user = membership.person
474
    membership.perform_action("reject", actor=request_user, reason=reason)
475
    logger.info("Request of user %s for %s has been rejected." %
476
                (user.log_display, project))
477

    
478
    project_notif.membership_change_notify(project, user, 'rejected')
479
    return membership
480

    
481

    
482
def cancel_membership_checks(membership, request_user):
483
    if not membership.check_action("cancel"):
484
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
485
        raise ProjectConflict(m)
486

    
487
    membership_check_allowed(membership, request_user, level=ADMIN_LEVEL)
488
    project = membership.project
489
    checkAlive(project)
490

    
491

    
492
def cancel_membership(memb_id, request_user, reason=None):
493
    project = get_project_of_membership_for_update(memb_id)
494
    membership = get_membership_by_id(memb_id)
495
    cancel_membership_checks(membership, request_user)
496
    membership.perform_action("cancel", actor=request_user, reason=reason)
497
    logger.info("Request of user %s for %s has been cancelled." %
498
                (membership.person.log_display, project))
499

    
500

    
501
def remove_membership_checks(membership, request_user=None):
502
    if not membership.check_action("remove"):
503
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
504
        raise ProjectConflict(m)
505

    
506
    project = membership.project
507
    project_check_allowed(project, request_user)
508
    checkAlive(project)
509

    
510
    leave_policy = project.member_leave_policy
511
    if leave_policy == CLOSED_POLICY:
512
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
513
        raise ProjectConflict(m)
514

    
515

    
516
def remove_membership(memb_id, request_user=None, reason=None):
517
    project = get_project_of_membership_for_update(memb_id)
518
    membership = get_membership_by_id(memb_id)
519
    remove_membership_checks(membership, request_user)
520
    user = membership.person
521
    membership.perform_action("remove", actor=request_user, reason=reason)
522
    quotas.qh_sync_membership(membership)
523
    logger.info("User %s has been removed from %s." %
524
                (user.log_display, project))
525

    
526
    project_notif.membership_change_notify(project, user, 'removed')
527
    return membership
528

    
529

    
530
def enroll_member_by_email(project_id, email, request_user=None, reason=None):
531
    try:
532
        user = AstakosUser.objects.accepted().get(email=email)
533
        return enroll_member(project_id, user, request_user, reason=reason)
534
    except AstakosUser.DoesNotExist:
535
        raise ProjectConflict(astakos_messages.UNKNOWN_USERS % email)
536

    
537

    
538
def enroll_member(project_id, user, request_user=None, reason=None):
539
    try:
540
        project = get_project_for_update(project_id)
541
    except ProjectNotFound as e:
542
        raise ProjectConflict(e.message)
543
    accept_membership_project_checks(project, request_user)
544

    
545
    try:
546
        membership = get_membership(project.id, user.id)
547
        if not membership.check_action("enroll"):
548
            m = _(astakos_messages.MEMBERSHIP_ACCEPTED)
549
            raise ProjectConflict(m)
550
        membership.perform_action("enroll", actor=request_user, reason=reason)
551
    except ProjectNotFound:
552
        membership = new_membership(project, user, actor=request_user,
553
                                    enroll=True)
554

    
555
    quotas.qh_sync_membership(membership)
556
    logger.info("User %s has been enrolled in %s." %
557
                (membership.person.log_display, project))
558

    
559
    project_notif.membership_enroll_notify(project, membership.person)
560
    return membership
561

    
562

    
563
def leave_project_checks(membership, request_user):
564
    if not membership.check_action("leave"):
565
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
566
        raise ProjectConflict(m)
567

    
568
    membership_check_allowed(membership, request_user, level=ADMIN_LEVEL)
569
    project = membership.project
570
    checkAlive(project)
571

    
572
    leave_policy = project.member_leave_policy
573
    if leave_policy == CLOSED_POLICY:
574
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
575
        raise ProjectConflict(m)
576

    
577

    
578
def can_cancel_join_request(project, user):
579
    m = user.get_membership(project)
580
    if m is None:
581
        return False
582
    return m.state in [m.REQUESTED]
583

    
584

    
585
def can_leave_request(project, user):
586
    m = user.get_membership(project)
587
    if m is None:
588
        return False
589
    try:
590
        leave_project_checks(m, user)
591
    except ProjectError:
592
        return False
593
    return True
594

    
595

    
596
def leave_project(memb_id, request_user, reason=None):
597
    project = get_project_of_membership_for_update(memb_id)
598
    membership = get_membership_by_id(memb_id)
599
    leave_project_checks(membership, request_user)
600

    
601
    auto_accepted = False
602
    leave_policy = project.member_leave_policy
603
    if leave_policy == AUTO_ACCEPT_POLICY:
604
        membership.perform_action("remove", actor=request_user, reason=reason)
605
        quotas.qh_sync_membership(membership)
606
        logger.info("User %s has left %s." %
607
                    (request_user.log_display, project))
608
        auto_accepted = True
609
    else:
610
        membership.perform_action("leave_request", actor=request_user,
611
                                  reason=reason)
612
        logger.info("User %s requested to leave %s." %
613
                    (request_user.log_display, project))
614
        project_notif.membership_request_notify(
615
            project, membership.person, "leave")
616
    return auto_accepted
617

    
618

    
619
def join_project_checks(project):
620
    checkAlive(project)
621

    
622
    join_policy = project.member_join_policy
623
    if join_policy == CLOSED_POLICY:
624
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
625
        raise ProjectConflict(m)
626

    
627

    
628
Nothing = type('Nothing', (), {})
629

    
630

    
631
def can_join_request(project, user, membership=Nothing):
632
    try:
633
        join_project_checks(project)
634
    except ProjectError:
635
        return False
636

    
637
    m = (membership if membership is not Nothing
638
         else user.get_membership(project))
639
    if not m:
640
        return True
641
    return m.check_action("join")
642

    
643

    
644
def new_membership(project, user, actor=None, reason=None, enroll=False):
645
    state = (ProjectMembership.ACCEPTED if enroll
646
             else ProjectMembership.REQUESTED)
647
    m = ProjectMembership.objects.create(
648
        project=project, person=user, state=state, initialized=enroll)
649
    m._log_create(None, state, actor=actor, reason=reason)
650
    return m
651

    
652

    
653
def join_project(project_id, request_user, reason=None):
654
    project = get_project_for_update(project_id)
655
    join_project_checks(project)
656

    
657
    try:
658
        membership = get_membership(project.id, request_user.id)
659
        if not membership.check_action("join"):
660
            msg = _(astakos_messages.MEMBERSHIP_ASSOCIATED)
661
            raise ProjectConflict(msg)
662
        membership.perform_action("join", actor=request_user, reason=reason)
663
    except ProjectNotFound:
664
        membership = new_membership(project, request_user, actor=request_user,
665
                                    reason=reason)
666

    
667
    join_policy = project.member_join_policy
668
    if (join_policy == AUTO_ACCEPT_POLICY and (
669
            not project.violates_members_limit(adding=1))):
670
        membership.perform_action("accept", actor=request_user, reason=reason)
671
        quotas.qh_sync_membership(membership)
672
        logger.info("User %s joined %s." %
673
                    (request_user.log_display, project))
674
    else:
675
        project_notif.membership_request_notify(
676
            project, membership.person, "join")
677
        logger.info("User %s requested to join %s." %
678
                    (request_user.log_display, project))
679
    return membership
680

    
681

    
682
MEMBERSHIP_ACTION_CHECKS = {
683
    "leave":  leave_project_checks,
684
    "cancel": cancel_membership_checks,
685
    "accept": accept_membership_checks,
686
    "reject": reject_membership_checks,
687
    "remove": remove_membership_checks,
688
}
689

    
690

    
691
def membership_allowed_actions(membership, request_user):
692
    allowed = []
693
    for action, check in MEMBERSHIP_ACTION_CHECKS.iteritems():
694
        try:
695
            check(membership, request_user)
696
            allowed.append(action)
697
        except ProjectError:
698
            pass
699
    return allowed
700

    
701

    
702
def make_base_project(username):
703
    chain = new_chain()
704
    proj = create_project(
705
        id=chain.chain,
706
        last_application=None,
707
        owner=None,
708
        realname="tmp",
709
        homepage="",
710
        description=("base project for user " + username),
711
        end_date=(datetime.now() + relativedelta(years=100)),
712
        member_join_policy=CLOSED_POLICY,
713
        member_leave_policy=CLOSED_POLICY,
714
        limit_on_members_number=1,
715
        private=True,
716
        is_base=True)
717
    proj.realname = "base:" + proj.uuid
718
    proj.save()
719
    # No quota are set; they will be filled in upon user acceptance
720
    return proj
721

    
722

    
723
def enable_base_project(user):
724
    project = user.base_project
725
    _fill_from_skeleton(project)
726
    project.activate()
727
    new_membership(project, user, enroll=True)
728
    quotas.qh_sync_project(project)
729

    
730

    
731
MODIFY_KEYS_MAIN = ["owner", "realname", "homepage", "description"]
732
MODIFY_KEYS_EXTRA = ["end_date", "member_join_policy", "member_leave_policy",
733
                     "limit_on_members_number", "private"]
734
MODIFY_KEYS = MODIFY_KEYS_MAIN + MODIFY_KEYS_EXTRA
735

    
736

    
737
def modifies_main_fields(request):
738
    return set(request.keys()).intersection(MODIFY_KEYS_MAIN)
739

    
740

    
741
def modify_project(project_id, request):
742
    project = get_project_for_update(project_id)
743
    if project.state not in Project.INITIALIZED_STATES:
744
        m = _(astakos_messages.UNINITIALIZED_NO_MODIFY) % project.uuid
745
        raise ProjectConflict(m)
746

    
747
    if project.is_base:
748
        main_fields = modifies_main_fields(request)
749
        if main_fields:
750
            m = (_(astakos_messages.BASE_NO_MODIFY_FIELDS)
751
                 % ", ".join(map(str, main_fields)))
752
            raise ProjectBadRequest(m)
753

    
754
    new_name = request.get("realname")
755
    if new_name is not None and project.is_alive:
756
        check_conflicting_projects(project, new_name)
757
        project.realname = new_name
758
        project.name = new_name
759
        project.save()
760

    
761
    _modify_projects(Project.objects.filter(id=project.id), request)
762

    
763

    
764
def modify_projects_in_bulk(flt, request):
765
    main_fields = modifies_main_fields(request)
766
    if main_fields:
767
        raise ProjectBadRequest("Cannot modify field(s) '%s' in bulk" %
768
                                ", ".join(map(str, main_fields)))
769

    
770
    projects = Project.objects.initialized(flt).select_for_update()
771
    _modify_projects(projects, request)
772

    
773

    
774
def _modify_projects(projects, request):
775
    upds = {}
776
    for key in MODIFY_KEYS:
777
        value = request.get(key)
778
        if value is not None:
779
            upds[key] = value
780
    projects.update(**upds)
781

    
782
    changed_resources = set()
783
    pquotas = []
784
    req_policies = request.get("resources", {})
785
    req_policies = validate_resource_policies(req_policies, admin=True)
786
    for project in projects:
787
        for resource, m_capacity, p_capacity in req_policies:
788
            changed_resources.add(resource)
789
            pquotas.append(
790
                ProjectResourceQuota(
791
                    project=project,
792
                    resource=resource,
793
                    member_capacity=m_capacity,
794
                    project_capacity=p_capacity))
795
    ProjectResourceQuota.objects.\
796
        filter(project__in=projects, resource__in=changed_resources).delete()
797
    ProjectResourceQuota.objects.bulk_create(pquotas)
798
    quotas.qh_sync_projects(projects)
799

    
800

    
801
def submit_application(owner=None,
802
                       name=None,
803
                       project_id=None,
804
                       homepage=None,
805
                       description=None,
806
                       start_date=None,
807
                       end_date=None,
808
                       member_join_policy=None,
809
                       member_leave_policy=None,
810
                       limit_on_members_number=None,
811
                       private=False,
812
                       comments=None,
813
                       resources=None,
814
                       request_user=None):
815

    
816
    project = None
817
    if project_id is not None:
818
        project = get_project_for_update(project_id)
819
        project_check_allowed(project, request_user, level=APPLICANT_LEVEL)
820
        if project.state not in Project.INITIALIZED_STATES:
821
            raise ProjectConflict("Cannot modify an uninitialized project.")
822

    
823
    policies = validate_resource_policies(resources)
824

    
825
    force = request_user.is_project_admin()
826
    ok, limit = qh_add_pending_app(request_user, project, force)
827
    if not ok:
828
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
829
        raise ProjectConflict(m)
830

    
831
    if project is None:
832
        chain = new_chain()
833
        project = create_project(
834
            id=chain.chain,
835
            owner=owner,
836
            realname=name,
837
            homepage=homepage,
838
            description=description,
839
            end_date=end_date,
840
            member_join_policy=member_join_policy,
841
            member_leave_policy=member_leave_policy,
842
            limit_on_members_number=limit_on_members_number,
843
            private=private)
844
        if policies is not None:
845
            set_project_resources(project, policies)
846

    
847
    application = ProjectApplication.objects.create(
848
        applicant=request_user,
849
        chain=project,
850
        owner=owner,
851
        name=name,
852
        homepage=homepage,
853
        description=description,
854
        start_date=start_date,
855
        end_date=end_date,
856
        member_join_policy=member_join_policy,
857
        member_leave_policy=member_leave_policy,
858
        limit_on_members_number=limit_on_members_number,
859
        private=private,
860
        comments=comments)
861
    if policies is not None:
862
        set_application_resources(application, policies)
863

    
864
    project.last_application = application
865
    project.save()
866

    
867
    ProjectApplication.objects.\
868
        filter(chain=project, state=ProjectApplication.PENDING).\
869
        exclude(id=application.id).\
870
        update(state=ProjectApplication.REPLACED)
871

    
872
    logger.info("User %s submitted %s." %
873
                (request_user.log_display, application.log_display))
874
    project_notif.application_notify(application, "submit")
875
    return application
876

    
877

    
878
def validate_resource_policies(policies, admin=False):
879
    if not isinstance(policies, dict):
880
        raise ProjectBadRequest("Malformed resource policies")
881

    
882
    resource_names = policies.keys()
883
    resources = Resource.objects.filter(name__in=resource_names)
884
    if not admin:
885
        resources = resources.filter(api_visible=True)
886

    
887
    resource_d = {}
888
    for resource in resources:
889
        resource_d[resource.name] = resource
890

    
891
    found = resource_d.keys()
892
    nonex = [name for name in resource_names if name not in found]
893
    if nonex:
894
        raise ProjectBadRequest("Malformed resource policies")
895

    
896
    pols = []
897
    for resource_name, specs in policies.iteritems():
898
        p_capacity = specs.get("project_capacity")
899
        m_capacity = specs.get("member_capacity")
900

    
901
        if not isinstance(p_capacity, (int, long)) or \
902
                not isinstance(m_capacity, (int, long)):
903
            raise ProjectBadRequest("Malformed resource policies")
904
        pols.append((resource_d[resource_name], m_capacity, p_capacity))
905
    return pols
906

    
907

    
908
def set_application_resources(application, policies):
909
    grants = []
910
    for resource, m_capacity, p_capacity in policies:
911
        grants.append(
912
            ProjectResourceGrant(
913
                project_application=application,
914
                resource=resource,
915
                member_capacity=m_capacity,
916
                project_capacity=p_capacity))
917
    ProjectResourceGrant.objects.bulk_create(grants)
918

    
919

    
920
def set_project_resources(project, policies):
921
    grants = []
922
    for resource, m_capacity, p_capacity in policies:
923
        grants.append(
924
            ProjectResourceQuota(
925
                project=project,
926
                resource=resource,
927
                member_capacity=m_capacity,
928
                project_capacity=p_capacity))
929
    ProjectResourceQuota.objects.bulk_create(grants)
930

    
931

    
932
def check_app_relevant(application, project, project_id):
933
    if project_id is not None and project.uuid != project_id or \
934
            project.last_application != application:
935
        pid = project_id if project_id is not None else project.uuid
936
        m = (_("%s is not a pending application for project %s.") %
937
             (application.id, pid))
938
        raise ProjectConflict(m)
939

    
940

    
941
def cancel_application(application_id, project_id=None, request_user=None,
942
                       reason=""):
943
    project = get_project_of_application_for_update(application_id)
944
    application = get_application(application_id)
945
    check_app_relevant(application, project, project_id)
946
    app_check_allowed(application, request_user, level=APPLICANT_LEVEL)
947

    
948
    if not application.can_cancel():
949
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
950
              (application.id, application.state_display()))
951
        raise ProjectConflict(m)
952

    
953
    qh_release_pending_app(application.applicant)
954

    
955
    application.cancel(actor=request_user, reason=reason)
956
    if project.state == Project.UNINITIALIZED:
957
        project.set_deleted()
958
    logger.info("%s has been cancelled." % (application.log_display))
959

    
960

    
961
def dismiss_application(application_id, project_id=None, request_user=None,
962
                        reason=""):
963
    project = get_project_of_application_for_update(application_id)
964
    application = get_application(application_id)
965
    check_app_relevant(application, project, project_id)
966
    app_check_allowed(application, request_user, level=APPLICANT_LEVEL)
967

    
968
    if not application.can_dismiss():
969
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
970
              (application.id, application.state_display()))
971
        raise ProjectConflict(m)
972

    
973
    application.dismiss(actor=request_user, reason=reason)
974
    if project.state == Project.UNINITIALIZED:
975
        project.set_deleted()
976
    logger.info("%s has been dismissed." % (application.log_display))
977

    
978

    
979
def deny_application(application_id, project_id=None, request_user=None,
980
                     reason=""):
981
    project = get_project_of_application_for_update(application_id)
982
    application = get_application(application_id)
983
    check_app_relevant(application, project, project_id)
984
    app_check_allowed(application, request_user, level=ADMIN_LEVEL)
985

    
986
    if not application.can_deny():
987
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
988
              (application.id, application.state_display()))
989
        raise ProjectConflict(m)
990

    
991
    qh_release_pending_app(application.applicant)
992

    
993
    application.deny(actor=request_user, reason=reason)
994
    logger.info("%s has been denied with reason \"%s\"." %
995
                (application.log_display, reason))
996
    project_notif.application_notify(application, "deny")
997

    
998

    
999
def check_conflicting_projects(project, new_project_name):
1000
    try:
1001
        q = Q(name=new_project_name) & ~Q(id=project.id)
1002
        conflicting_project = Project.objects.get(q)
1003
        m = (_("cannot approve: project with name '%s' "
1004
               "already exists (id: %s)") %
1005
             (new_project_name, conflicting_project.uuid))
1006
        raise ProjectConflict(m)  # invalid argument
1007
    except Project.DoesNotExist:
1008
        pass
1009

    
1010

    
1011
def approve_application(application_id, project_id=None, request_user=None,
1012
                        reason=""):
1013
    get_project_lock()
1014
    project = get_project_of_application_for_update(application_id)
1015
    application = get_application(application_id)
1016
    check_app_relevant(application, project, project_id)
1017
    app_check_allowed(application, request_user, level=ADMIN_LEVEL)
1018

    
1019
    if not application.can_approve():
1020
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
1021
              (application.id, application.state_display()))
1022
        raise ProjectConflict(m)
1023

    
1024
    if application.name:
1025
        check_conflicting_projects(project, application.name)
1026

    
1027
    qh_release_pending_app(application.applicant)
1028
    application.approve(actor=request_user, reason=reason)
1029

    
1030
    if project.state == Project.UNINITIALIZED:
1031
        _fill_from_skeleton(project)
1032
    else:
1033
        _apply_modifications(project, application)
1034
    project.activate(actor=request_user, reason=reason)
1035

    
1036
    quotas.qh_sync_project(project)
1037
    logger.info("%s has been approved." % (application.log_display))
1038
    project_notif.application_notify(application, "approve")
1039
    return project
1040

    
1041

    
1042
def _fill_from_skeleton(project):
1043
    current_resources = set(ProjectResourceQuota.objects.
1044
                            filter(project=project).
1045
                            values_list("resource_id", flat=True))
1046
    resources = Resource.objects.all()
1047
    new_quotas = []
1048
    for resource in resources:
1049
        if resource.id not in current_resources:
1050
            limit = quotas.pick_limit_scheme(project, resource)
1051
            new_quotas.append(
1052
                ProjectResourceQuota(
1053
                    project=project,
1054
                    resource=resource,
1055
                    member_capacity=limit,
1056
                    project_capacity=limit))
1057
    ProjectResourceQuota.objects.bulk_create(new_quotas)
1058

    
1059

    
1060
def _apply_modifications(project, application):
1061
    FIELDS = [
1062
        ("owner", "owner"),
1063
        ("name", "realname"),
1064
        ("homepage", "homepage"),
1065
        ("description", "description"),
1066
        ("end_date", "end_date"),
1067
        ("member_join_policy", "member_join_policy"),
1068
        ("member_leave_policy", "member_leave_policy"),
1069
        ("limit_on_members_number", "limit_on_members_number"),
1070
        ("private", "private"),
1071
        ]
1072

    
1073
    changed = False
1074
    for appfield, projectfield in FIELDS:
1075
        value = getattr(application, appfield)
1076
        if value is not None:
1077
            changed = True
1078
            setattr(project, projectfield, value)
1079
    if changed:
1080
        project.save()
1081

    
1082
    grants = application.projectresourcegrant_set.all()
1083
    pquotas = []
1084
    resources = []
1085
    for grant in grants:
1086
        resources.append(grant.resource)
1087
        pquotas.append(
1088
            ProjectResourceQuota(
1089
                project=project,
1090
                resource=grant.resource,
1091
                member_capacity=grant.member_capacity,
1092
                project_capacity=grant.project_capacity))
1093
    ProjectResourceQuota.objects.\
1094
        filter(project=project, resource__in=resources).delete()
1095
    ProjectResourceQuota.objects.bulk_create(pquotas)
1096

    
1097

    
1098
def check_expiration(execute=False):
1099
    objects = Project.objects
1100
    expired = objects.expired_projects()
1101
    if execute:
1102
        for project in expired:
1103
            terminate(project.pk)
1104

    
1105
    return [project.expiration_info() for project in expired]
1106

    
1107

    
1108
def terminate(project_id, request_user=None, reason=None):
1109
    project = get_project_for_update(project_id)
1110
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
1111
    checkAlive(project)
1112

    
1113
    project.terminate(actor=request_user, reason=reason)
1114
    quotas.qh_sync_project(project)
1115
    logger.info("%s has been terminated." % (project))
1116

    
1117
    project_notif.project_notify(project, "terminate")
1118

    
1119

    
1120
def suspend(project_id, request_user=None, reason=None):
1121
    project = get_project_for_update(project_id)
1122
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
1123
    checkAlive(project)
1124

    
1125
    project.suspend(actor=request_user, reason=reason)
1126
    quotas.qh_sync_project(project)
1127
    logger.info("%s has been suspended." % (project))
1128

    
1129
    project_notif.project_notify(project, "suspend")
1130

    
1131

    
1132
def unsuspend(project_id, request_user=None, reason=None):
1133
    project = get_project_for_update(project_id)
1134
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
1135

    
1136
    if not project.is_suspended:
1137
        m = _(astakos_messages.NOT_SUSPENDED_PROJECT) % project.uuid
1138
        raise ProjectConflict(m)
1139

    
1140
    project.resume(actor=request_user, reason=reason)
1141
    quotas.qh_sync_project(project)
1142
    logger.info("%s has been unsuspended." % (project))
1143
    project_notif.project_notify(project, "unsuspend")
1144

    
1145

    
1146
def reinstate(project_id, request_user=None, reason=None):
1147
    get_project_lock()
1148
    project = get_project_for_update(project_id)
1149
    project_check_allowed(project, request_user, level=ADMIN_LEVEL)
1150

    
1151
    if not project.is_terminated:
1152
        m = _(astakos_messages.NOT_TERMINATED_PROJECT) % project.uuid
1153
        raise ProjectConflict(m)
1154

    
1155
    check_conflicting_projects(project, project.realname)
1156
    project.resume(actor=request_user, reason=reason)
1157
    quotas.qh_sync_project(project)
1158
    logger.info("%s has been reinstated" % (project))
1159
    project_notif.project_notify(project, "reinstate")
1160

    
1161

    
1162
def _partition_by(f, l):
1163
    d = {}
1164
    for x in l:
1165
        group = f(x)
1166
        group_l = d.get(group, [])
1167
        group_l.append(x)
1168
        d[group] = group_l
1169
    return d
1170

    
1171

    
1172
def count_pending_app(users):
1173
    users = list(users)
1174
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
1175
                                             owner__in=users)
1176
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
1177

    
1178
    usage = {}
1179
    for user in users:
1180
        uuid = user.uuid
1181
        usage[uuid] = len(apps_d.get(uuid, []))
1182
    return usage
1183

    
1184

    
1185
def get_pending_app_diff(project):
1186
    if project is None:
1187
        diff = 1
1188
    else:
1189
        objs = ProjectApplication.objects
1190
        q = objs.filter(chain=project, state=ProjectApplication.PENDING)
1191
        count = q.count()
1192
        diff = 1 - count
1193
    return diff
1194

    
1195

    
1196
def qh_add_pending_app(user, project=None, force=False, assign_project=None):
1197
    if assign_project is None:
1198
        assign_project = user.base_project
1199
    diff = get_pending_app_diff(project)
1200
    return quotas.register_pending_apps(user, assign_project,
1201
                                        diff, force=force)
1202

    
1203

    
1204
def check_pending_app_quota(user, project=None):
1205
    diff = get_pending_app_diff(project)
1206
    quota = quotas.get_pending_app_quota(user)
1207
    limit = quota['limit']
1208
    usage = quota['usage']
1209
    if usage + diff > limit:
1210
        return False, limit
1211
    return True, None
1212

    
1213

    
1214
def qh_release_pending_app(user, assign_project=None):
1215
    if assign_project is None:
1216
        assign_project = user.base_project
1217
    quotas.register_pending_apps(user, assign_project, -1)