Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 9202a57d

History | View | Annotate | Download (41 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
from synnefo.util.keypath import set_path
46

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

    
55
import astakos.im.messages as astakos_messages
56

    
57
logger = logging.getLogger(__name__)
58

    
59

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

    
69

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

    
76

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

    
95

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

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

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

    
124

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

    
137

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

    
158

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

    
181

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

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

    
202

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

    
216

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

    
234

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

    
242

    
243
### PROJECT FUNCTIONS ###
244

    
245

    
246
class ProjectError(Exception):
247
    pass
248

    
249

    
250
class ProjectNotFound(ProjectError):
251
    pass
252

    
253

    
254
class ProjectForbidden(ProjectError):
255
    pass
256

    
257

    
258
class ProjectBadRequest(ProjectError):
259
    pass
260

    
261

    
262
class ProjectConflict(ProjectError):
263
    pass
264

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

    
269
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
270

    
271

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

    
279

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

    
289

    
290
def get_project_by_uuid(uuid):
291
    try:
292
        return Project.objects.get(uuid=uuid)
293
    except Project.DoesNotExist:
294
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % uuid
295
        raise ProjectNotFound(m)
296

    
297

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

    
309

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

    
314

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

    
318

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

    
326

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

    
331

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

    
339

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

    
348

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

    
357

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

    
363

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

    
367

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

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

    
375

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

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

    
385

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

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

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

    
404

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

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

    
416

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

    
422

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

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

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

    
436

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

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

    
445

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

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

    
459

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

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

    
469

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

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

    
482

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

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

    
492

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

    
501

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

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

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

    
516

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

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

    
530

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

    
538

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

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

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

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

    
563

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

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

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

    
578

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

    
585

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

    
596

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

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

    
619

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

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

    
628

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

    
631

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

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

    
644

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

    
653

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

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

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

    
682

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

    
691

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

    
702

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

    
723

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

    
731

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

    
737

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

    
741

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

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

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

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

    
764

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

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

    
774

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

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

    
801

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

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

    
824
    policies = validate_resource_policies(resources)
825

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

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

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

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

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

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

    
878

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

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

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

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

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

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

    
908

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

    
920

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

    
932

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

    
941

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

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

    
954
    qh_release_pending_app(application.applicant)
955

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

    
961

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

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

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

    
979

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

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

    
992
    qh_release_pending_app(application.applicant)
993

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

    
999

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

    
1011

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

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

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

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

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

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

    
1042

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

    
1060

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

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

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

    
1098

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

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

    
1108

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

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

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

    
1120

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

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

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

    
1132

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

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

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

    
1146

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

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

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

    
1162

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

    
1172

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

    
1179
    usage = {}
1180
    for user in users:
1181
        uuid = user.uuid
1182
        set_path(usage,
1183
                 [uuid, user.base_project.uuid, quotas.PENDING_APP_RESOURCE],
1184
                 len(apps_d.get(uuid, [])), createpath=True)
1185
    return usage
1186

    
1187

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

    
1198

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

    
1206

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

    
1216

    
1217
def qh_release_pending_app(user, assign_project=None):
1218
    if assign_project is None:
1219
        assign_project = user.base_project
1220
    quotas.register_pending_apps(user, assign_project, -1)