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

import logging

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

42
43
from synnefo_branding.utils import render_to_string

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

Olga Brani's avatar
Olga Brani committed
58
import astakos.im.messages as astakos_messages
Antony Chazapis's avatar
Antony Chazapis committed
59
60
61

logger = logging.getLogger(__name__)

62

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

72

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

78

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

97

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

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

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

126
127
128
129
130
131
132
133

def send_account_pending_moderation_notification(
        user,
        template_name='im/account_pending_moderation_notification.txt'):
    """
    Notify admins that a new user has verified his email address and moderation
    step is required to activate his account.
    """
134
135
    subject = (_(astakos_messages.ACCOUNT_CREATION_SUBJECT) %
               {'user': user.email})
136
137
    return _send_admin_notification(template_name, {}, subject=subject,
                                    user=user, msg="account creation")
Olga Brani's avatar
Olga Brani committed
138
139


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

160

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

183

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

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

204

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

218

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

236

237
238
239
240
def invite(inviter, email, realname):
    inv = Invitation(inviter=inviter, username=email, realname=realname)
    inv.save()
    send_invitation(inv)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
241
    inviter.invitations = max(0, inviter.invitations - 1)
242
    inviter.save()
243

244

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
245
### PROJECT FUNCTIONS ###
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
246

247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266

class ProjectError(Exception):
    pass


class ProjectNotFound(ProjectError):
    pass


class ProjectForbidden(ProjectError):
    pass


class ProjectBadRequest(ProjectError):
    pass


class ProjectConflict(ProjectError):
    pass

267
AUTO_ACCEPT_POLICY = 1
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
268
269
270
271
MODERATED_POLICY = 2
CLOSED_POLICY = 3

POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
272

273

274
275
276
def get_related_project_id(application_id):
    try:
        app = ProjectApplication.objects.get(id=application_id)
277
        return app.chain_id
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
278
    except ProjectApplication.DoesNotExist:
279
280
        return None

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
281

282
283
def get_project_by_id(project_id):
    try:
284
285
286
        return Project.objects.select_related(
            "application", "application__owner",
            "application__applicant").get(id=project_id)
287
    except Project.DoesNotExist:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
288
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
289
        raise ProjectNotFound(m)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
290

291

292
def get_project_for_update(project_id):
293
    try:
294
        return Project.objects.select_for_update().get(id=project_id)
295
296
    except Project.DoesNotExist:
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
297
        raise ProjectNotFound(m)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
298

299

300
def get_project_of_application_for_update(app_id):
301
    app = get_application(app_id)
302
    return get_project_for_update(app.chain_id)
303
304


305
def get_project_lock():
306
    ProjectLock.objects.select_for_update().get(pk=1)
307
308


309
def get_application(application_id):
310
    try:
311
        return ProjectApplication.objects.get(id=application_id)
312
313
    except ProjectApplication.DoesNotExist:
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % application_id
314
        raise ProjectNotFound(m)
315

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
316

317
def get_project_of_membership_for_update(memb_id):
318
    m = get_membership_by_id(memb_id)
319
    return get_project_for_update(m.project_id)
320
321


Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
322
323
324
325
def get_user_by_uuid(uuid):
    try:
        return AstakosUser.objects.get(uuid=uuid)
    except AstakosUser.DoesNotExist:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
326
        m = _(astakos_messages.UNKNOWN_USER_ID) % uuid
327
        raise ProjectNotFound(m)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
328

329

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

338

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


348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
ALLOWED_CHECKS = [
    (lambda u, a: not u or u.is_project_admin()),
    (lambda u, a: a.owner == u),
    (lambda u, a: a.applicant == u),
    (lambda u, a: a.chain.overall_state() == Project.O_ACTIVE
     or bool(a.chain.projectmembership_set.any_accepted().filter(person=u))),
]

ADMIN_LEVEL = 0
OWNER_LEVEL = 1
APPLICANT_LEVEL = 2
ANY_LEVEL = 3


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

366
367
    if silent:
        return False
368
369

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

372

373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
def membership_check_allowed(membership, request_user,
                             level=OWNER_LEVEL, silent=False):
    r = project_check_allowed(
        membership.project, request_user, level, silent=True)

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


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


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


392
393
def checkAlive(project):
    if not project.is_alive:
394
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.id
395
        raise ProjectConflict(m)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
396

397

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

    join_policy = project.application.member_join_policy
    if join_policy == CLOSED_POLICY:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
404
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
405
        raise ProjectConflict(m)
406

407
    if project.violates_members_limit(adding=1):
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
408
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
409
        raise ProjectConflict(m)
410

411

412
def accept_membership_checks(membership, request_user):
413
    if not membership.check_action("accept"):
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
414
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
415
        raise ProjectConflict(m)
416

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


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

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

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
434

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

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

444

445
def reject_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
    reject_membership_checks(membership, request_user)
449
    user = membership.person
450
    membership.perform_action("reject", actor=request_user, reason=reason)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
451
    logger.info("Request of user %s for %s has been rejected." %
452
                (user.log_display, project))
453

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

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
457

458
def cancel_membership_checks(membership, request_user):
459
    if not membership.check_action("cancel"):
460
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
461
        raise ProjectConflict(m)
462

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


468
def cancel_membership(memb_id, request_user, reason=None):
469
470
471
    project = get_project_of_membership_for_update(memb_id)
    membership = get_membership_by_id(memb_id)
    cancel_membership_checks(membership, request_user)
472
    membership.perform_action("cancel", actor=request_user, reason=reason)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
473
474
    logger.info("Request of user %s for %s has been cancelled." %
                (membership.person.log_display, project))
475

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
476

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

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

486
487
    leave_policy = project.application.member_leave_policy
    if leave_policy == CLOSED_POLICY:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
488
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
489
        raise ProjectConflict(m)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
490

491

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

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

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
505

506
507
def enroll_member_by_email(project_id, email, request_user=None, reason=None):
    try:
508
        user = AstakosUser.objects.accepted().get(email=email)
509
510
        return enroll_member(project_id, user, request_user, reason=reason)
    except AstakosUser.DoesNotExist:
511
        raise ProjectConflict(astakos_messages.UNKNOWN_USERS % email)
512
513


514
def enroll_member(project_id, user, request_user=None, reason=None):
515
516
517
518
    try:
        project = get_project_for_update(project_id)
    except ProjectNotFound as e:
        raise ProjectConflict(e.message)
519
    accept_membership_project_checks(project, request_user)
520

521
522
    try:
        membership = get_membership(project_id, user.id)
523
        if not membership.check_action("enroll"):
524
            m = _(astakos_messages.MEMBERSHIP_ACCEPTED)
525
            raise ProjectConflict(m)
526
        membership.perform_action("enroll", actor=request_user, reason=reason)
527
    except ProjectNotFound:
528
529
        membership = new_membership(project, user, actor=request_user,
                                    enroll=True)
530

531
    qh_sync_user(user)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
532
533
    logger.info("User %s has been enrolled in %s." %
                (membership.person.log_display, project))
534

535
    membership_enroll_notify(project, membership.person)
536
    return membership
537

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
538

539
def leave_project_checks(membership, request_user):
540
    if not membership.check_action("leave"):
541
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
542
        raise ProjectConflict(m)
543

544
    membership_check_allowed(membership, request_user, level=ADMIN_LEVEL)
545
    project = membership.project
546
    checkAlive(project)
547

548
    leave_policy = project.application.member_leave_policy
549
    if leave_policy == CLOSED_POLICY:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
550
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
551
        raise ProjectConflict(m)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
552

553

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
554
555
556
557
def can_leave_request(project, user):
    m = user.get_membership(project)
    if m is None:
        return False
558
559
    try:
        leave_project_checks(m, user)
560
    except ProjectError:
561
562
        return False
    return True
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
563

564

565
def leave_project(memb_id, request_user, reason=None):
566
    project = get_project_of_membership_for_update(memb_id)
567
    membership = get_membership_by_id(memb_id)
568
    leave_project_checks(membership, request_user)
569

570
    auto_accepted = False
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
571
    leave_policy = project.application.member_leave_policy
572
    if leave_policy == AUTO_ACCEPT_POLICY:
573
        membership.perform_action("remove", actor=request_user, reason=reason)
574
        qh_sync_user(request_user)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
575
        logger.info("User %s has left %s." %
576
                    (request_user.log_display, project))
577
        auto_accepted = True
578
    else:
579
580
        membership.perform_action("leave_request", actor=request_user,
                                  reason=reason)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
581
        logger.info("User %s requested to leave %s." %
582
                    (request_user.log_display, project))
583
584
        membership_leave_request_notify(project, membership.person)
    return auto_accepted
585

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
586

587
def join_project_checks(project):
588
    checkAlive(project)
589

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
590
    join_policy = project.application.member_join_policy
591
    if join_policy == CLOSED_POLICY:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
592
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
593
        raise ProjectConflict(m)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
594

595

596
597
598
599
Nothing = type('Nothing', (), {})


def can_join_request(project, user, membership=Nothing):
600
601
    try:
        join_project_checks(project)
602
    except ProjectError:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
603
        return False
604

605
606
    m = (membership if membership is not Nothing
         else user.get_membership(project))
607
608
    if not m:
        return True
609
    return m.check_action("join")
610
611


612
613
614
615
616
617
def new_membership(project, user, actor=None, reason=None, enroll=False):
    state = (ProjectMembership.ACCEPTED if enroll
             else ProjectMembership.REQUESTED)
    m = ProjectMembership.objects.create(
        project=project, person=user, state=state)
    m._log_create(None, state, actor=actor, reason=reason)
618
    return m
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
619

620

621
def join_project(project_id, request_user, reason=None):
622
    project = get_project_for_update(project_id)
623
    join_project_checks(project)
624

625
626
    try:
        membership = get_membership(project.id, request_user.id)
627
        if not membership.check_action("join"):
628
            msg = _(astakos_messages.MEMBERSHIP_ASSOCIATED)
629
            raise ProjectConflict(msg)
630
        membership.perform_action("join", actor=request_user, reason=reason)
631
    except ProjectNotFound:
632
633
        membership = new_membership(project, request_user, actor=request_user,
                                    reason=reason)
634

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
635
    join_policy = project.application.member_join_policy
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
636
637
    if (join_policy == AUTO_ACCEPT_POLICY and (
            not project.violates_members_limit(adding=1))):
638
        membership.perform_action("accept", actor=request_user, reason=reason)
639
        qh_sync_user(request_user)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
640
        logger.info("User %s joined %s." %
641
                    (request_user.log_display, project))
642
643
    else:
        membership_request_notify(project, membership.person)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
644
        logger.info("User %s requested to join %s." %
645
                    (request_user.log_display, project))
646
    return membership
647

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
648

649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
MEMBERSHIP_ACTION_CHECKS = {
    "leave":  leave_project_checks,
    "cancel": cancel_membership_checks,
    "accept": accept_membership_checks,
    "reject": reject_membership_checks,
    "remove": remove_membership_checks,
}


def membership_allowed_actions(membership, request_user):
    allowed = []
    for action, check in MEMBERSHIP_ACTION_CHECKS.iteritems():
        try:
            check(membership, request_user)
            allowed.append(action)
664
        except ProjectError:
665
666
667
668
            pass
    return allowed


669
670
def submit_application(owner=None,
                       name=None,
671
                       project_id=None,
672
673
674
675
676
677
678
679
                       homepage=None,
                       description=None,
                       start_date=None,
                       end_date=None,
                       member_join_policy=None,
                       member_leave_policy=None,
                       limit_on_members_number=None,
                       comments=None,
680
                       resources=None,
681
                       request_user=None):
682

683
684
    project = None
    if project_id is not None:
685
        project = get_project_for_update(project_id)
686
        project_check_allowed(project, request_user, level=APPLICANT_LEVEL)
687

688
689
    policies = validate_resource_policies(resources)

690
    force = request_user.is_project_admin()
691
    ok, limit = qh_add_pending_app(owner, project, force)
692
    if not ok:
693
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
694
        raise ProjectConflict(m)
695

696
697
698
699
700
701
702
703
704
705
706
707
    application = ProjectApplication(
        applicant=request_user,
        owner=owner,
        name=name,
        homepage=homepage,
        description=description,
        start_date=start_date,
        end_date=end_date,
        member_join_policy=member_join_policy,
        member_leave_policy=member_leave_policy,
        limit_on_members_number=limit_on_members_number,
        comments=comments)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
708

709
    if project is None:
710
711
712
713
        chain = new_chain()
        application.chain_id = chain.chain
        application.save()
        Project.objects.create(id=chain.chain, application=application)
714
    else:
715
        application.chain = project
716
        application.save()
717
718
719
        if project.application.state != ProjectApplication.APPROVED:
            project.application = application
            project.save()
720
721

        pending = ProjectApplication.objects.filter(
722
            chain=project,
723
            state=ProjectApplication.PENDING).exclude(id=application.id)
724
725
726
727
        for app in pending:
            app.state = ProjectApplication.REPLACED
            app.save()

728
729
    if policies is not None:
        set_resource_policies(application, policies)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
730
731
    logger.info("User %s submitted %s." %
                (request_user.log_display, application.log_display))
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
732
733
    application_submit_notify(application)
    return application
734

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
735

736
737
738
739
740
def validate_resource_policies(policies):
    if not isinstance(policies, dict):
        raise ProjectBadRequest("Malformed resource policies")

    resource_names = policies.keys()
741
742
    resources = Resource.objects.filter(name__in=resource_names,
                                        api_visible=True)
743
744
745
746
747
748
749
    resource_d = {}
    for resource in resources:
        resource_d[resource.name] = resource

    found = resource_d.keys()
    nonex = [name for name in resource_names if name not in found]
    if nonex:
750
        raise ProjectBadRequest("Malformed resource policies")
751
752
753
754
755
756
757

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

        if p_capacity is not None and not isinstance(p_capacity, (int, long)):
758
            raise ProjectBadRequest("Malformed resource policies")
759
        if not isinstance(m_capacity, (int, long)):
760
            raise ProjectBadRequest("Malformed resource policies")
761
762
763
764
765
766
767
768
769
770
771
772
        pols.append((resource_d[resource_name], m_capacity, p_capacity))
    return pols


def set_resource_policies(application, policies):
    for resource, m_capacity, p_capacity in policies:
        g = application.projectresourcegrant_set
        g.create(resource=resource,
                 member_capacity=m_capacity,
                 project_capacity=p_capacity)


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

778
    if not application.can_cancel():
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
779
780
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
              (application.id, application.state_display()))