functions.py 28.3 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
# 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
35
import socket
Antony Chazapis's avatar
Antony Chazapis committed
36
37
38

from django.utils.translation import ugettext as _
from django.template.loader import render_to_string
39
from django.core.mail import send_mail, get_connection
Antony Chazapis's avatar
Antony Chazapis committed
40
from django.core.urlresolvers import reverse
41
from django.template import Context, loader
42
43
from django.contrib.auth import (
    login as auth_login,
44
    logout as auth_logout)
45
from django.conf import settings
Olga Brani's avatar
Olga Brani committed
46
from django.contrib.auth.models import AnonymousUser
47
from django.core.exceptions import PermissionDenied
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
48
from django.db import IntegrityError
49
from django.http import Http404
50

51
from urllib import quote
Antony Chazapis's avatar
Antony Chazapis committed
52
from urlparse import urljoin
53
from smtplib import SMTPException
54
from datetime import datetime
55
from functools import wraps
Antony Chazapis's avatar
Antony Chazapis committed
56

57
import astakos.im.settings as astakos_settings
58
from astakos.im.settings import (
59
60
61
62
    DEFAULT_CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL,
    VERIFICATION_EMAIL_SUBJECT, ACCOUNT_CREATION_SUBJECT,
    GROUP_CREATION_SUBJECT, HELPDESK_NOTIFICATION_EMAIL_SUBJECT,
    INVITATION_EMAIL_SUBJECT, GREETING_EMAIL_SUBJECT, FEEDBACK_EMAIL_SUBJECT,
63
64
65
    EMAIL_CHANGE_EMAIL_SUBJECT,
    PROJECT_CREATION_SUBJECT, PROJECT_APPROVED_SUBJECT,
    PROJECT_TERMINATION_SUBJECT, PROJECT_SUSPENSION_SUBJECT,
66
67
    PROJECT_MEMBERSHIP_CHANGE_SUBJECT,
    PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES)
68
69
from astakos.im.notifications import build_notification, NotificationError
from astakos.im.models import (
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
70
    AstakosUser, Invitation, ProjectMembership, ProjectApplication, Project,
71
    UserSetting,
72
73
    PendingMembershipError, get_resource_names, new_chain,
    users_quotas)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
74
from astakos.im.project_notif import (
75
    membership_change_notify, membership_enroll_notify,
76
    membership_request_notify, membership_leave_request_notify,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
77
    application_submit_notify, application_approve_notify,
78
    application_deny_notify,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
79
    project_termination_notify, project_suspension_notify)
80
81
from astakos.im.endpoints.qh import (
    register_users, register_quotas, qh_get_quota)
82

Olga Brani's avatar
Olga Brani committed
83
import astakos.im.messages as astakos_messages
Antony Chazapis's avatar
Antony Chazapis committed
84
85
86

logger = logging.getLogger(__name__)

87

88
89
def login(request, user):
    auth_login(request, user)
90
91
    from astakos.im.models import SessionCatalog
    SessionCatalog(
92
        session_key=request.session.session_key,
93
94
        user=user
    ).save()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
95
96
    logger.info('%s logged in.', user.log_display)

97

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
98
99
100
101
def logout(request, *args, **kwargs):
    user = request.user
    auth_logout(request, *args, **kwargs)
    logger.info('%s logged out.', user.log_display)
102

103

104
def send_verification(user, template_name='im/activation_email.txt'):
Antony Chazapis's avatar
Antony Chazapis committed
105
    """
106
    Send email to user to verify his/her email and activate his/her account.
107

108
    Raises SendVerificationError
Antony Chazapis's avatar
Antony Chazapis committed
109
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
110
    url = '%s?auth=%s&next=%s' % (urljoin(BASEURL, reverse('activate')),
111
112
                                  quote(user.auth_token),
                                  quote(urljoin(BASEURL, reverse('index'))))
113
    message = render_to_string(template_name, {
114
115
116
117
118
                               'user': user,
                               'url': url,
                               'baseurl': BASEURL,
                               'site_name': SITENAME,
                               'support': DEFAULT_CONTACT_EMAIL})
119
    sender = settings.SERVER_EMAIL
120
    try:
121
122
123
        send_mail(_(VERIFICATION_EMAIL_SUBJECT), message, sender, [user.email],
                  connection=get_connection())

124
125
126
127
    except (SMTPException, socket.error) as e:
        logger.exception(e)
        raise SendVerificationError()
    else:
128
        msg = 'Sent activation %s' % user.email
129
        logger.log(LOGGING_LEVEL, msg)
130

131

132
133
134
135
def send_activation(user, template_name='im/activation_email.txt'):
    send_verification(user, template_name)
    user.activation_sent = datetime.now()
    user.save()
136

137

Olga Brani's avatar
Olga Brani committed
138
139
140
def _send_admin_notification(template_name,
                             dictionary=None,
                             subject='alpha2 testing notification',):
141
    """
142
    Send notification email to settings.ADMINS.
143

144
    Raises SendNotificationError
145
    """
146
    if not settings.ADMINS:
147
        return
148
    dictionary = dictionary or {}
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
149
    message = render_to_string(template_name, dictionary)
150
    sender = settings.SERVER_EMAIL
151
    try:
152
153
        send_mail(subject, message, sender, [i[1] for i in settings.ADMINS],
                  connection=get_connection())
154
155
156
157
    except (SMTPException, socket.error) as e:
        logger.exception(e)
        raise SendNotificationError()
    else:
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
158
159
        msg = 'Sent admin notification for user %s' % \
              (dictionary.get('user', {}).get('email', None), )
160
        logger.log(LOGGING_LEVEL, msg)
161

162

Olga Brani's avatar
Olga Brani committed
163
164
def send_account_creation_notification(template_name, dictionary=None):
    user = dictionary.get('user', AnonymousUser())
165
    subject = _(ACCOUNT_CREATION_SUBJECT) % {'user':user.get('email', '')}
Olga Brani's avatar
Olga Brani committed
166
167
168
    return _send_admin_notification(template_name, dictionary, subject=subject)


169
def send_helpdesk_notification(user, template_name='im/helpdesk_notification.txt'):
170
171
    """
    Send email to DEFAULT_CONTACT_EMAIL to notify for a new user activation.
172

173
174
175
176
    Raises SendNotificationError
    """
    if not DEFAULT_CONTACT_EMAIL:
        return
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
177
178
179
180
    message = render_to_string(
        template_name,
        {'user': user}
    )
181
    sender = settings.SERVER_EMAIL
182
    try:
183
184
185
        send_mail(_(HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % {'user': user.email},
                  message, sender, [DEFAULT_CONTACT_EMAIL],
                  connection=get_connection())
186
187
188
189
    except (SMTPException, socket.error) as e:
        logger.exception(e)
        raise SendNotificationError()
    else:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
190
        msg = 'Sent helpdesk admin notification for %s' % user.email
191
        logger.log(LOGGING_LEVEL, msg)
192

193

194
195
196
def send_invitation(invitation, template_name='im/invitation.txt'):
    """
    Send invitation email.
197

198
    Raises SendInvitationError
199
    """
200
    subject = _(INVITATION_EMAIL_SUBJECT)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
201
    url = '%s?code=%d' % (urljoin(BASEURL, reverse('index')), invitation.code)
202
    message = render_to_string(template_name, {
203
204
205
206
207
                               'invitation': invitation,
                               'url': url,
                               'baseurl': BASEURL,
                               'site_name': SITENAME,
                               'support': DEFAULT_CONTACT_EMAIL})
208
    sender = settings.SERVER_EMAIL
209
    try:
210
211
        send_mail(subject, message, sender, [invitation.username],
                  connection=get_connection())
212
213
214
215
    except (SMTPException, socket.error) as e:
        logger.exception(e)
        raise SendInvitationError()
    else:
216
        msg = 'Sent invitation %s' % invitation
217
        logger.log(LOGGING_LEVEL, msg)
218
219
        inviter_invitations = invitation.inviter.invitations
        invitation.inviter.invitations = max(0, inviter_invitations - 1)
Olga Brani's avatar
Olga Brani committed
220
        invitation.inviter.save()
221

222

223
224
225
def send_greeting(user, email_template_name='im/welcome_email.txt'):
    """
    Send welcome email.
226

Antony Chazapis's avatar
Antony Chazapis committed
227
228
    Raises SMTPException, socket.error
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
229
    subject = _(GREETING_EMAIL_SUBJECT)
Antony Chazapis's avatar
Antony Chazapis committed
230
    message = render_to_string(email_template_name, {
231
232
233
234
235
                               'user': user,
                               'url': urljoin(BASEURL, reverse('index')),
                               'baseurl': BASEURL,
                               'site_name': SITENAME,
                               'support': DEFAULT_CONTACT_EMAIL})
236
    sender = settings.SERVER_EMAIL
237
    try:
238
239
        send_mail(subject, message, sender, [user.email],
                  connection=get_connection())
240
241
242
243
    except (SMTPException, socket.error) as e:
        logger.exception(e)
        raise SendGreetingError()
    else:
244
        msg = 'Sent greeting %s' % user.email
245
        logger.log(LOGGING_LEVEL, msg)
246

247

248
def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
249
    subject = _(FEEDBACK_EMAIL_SUBJECT)
250
    from_email = settings.SERVER_EMAIL
251
252
253
254
255
256
    recipient_list = [DEFAULT_CONTACT_EMAIL]
    content = render_to_string(email_template_name, {
        'message': msg,
        'data': data,
        'user': user})
    try:
257
258
        send_mail(subject, content, from_email, recipient_list,
                  connection=get_connection())
259
260
261
262
    except (SMTPException, socket.error) as e:
        logger.exception(e)
        raise SendFeedbackError()
    else:
263
        msg = 'Sent feedback from %s' % user.email
264
        logger.log(LOGGING_LEVEL, msg)
Antony Chazapis's avatar
Antony Chazapis committed
265

266

267
268
def send_change_email(
    ec, request, email_template_name='registration/email_change_email.txt'):
269
    try:
270
        url = ec.get_url()
271
272
        url = request.build_absolute_uri(url)
        t = loader.get_template(email_template_name)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
273
        c = {'url': url, 'site_name': SITENAME,
274
             'support': DEFAULT_CONTACT_EMAIL, 'ec': ec}
275
        from_email = settings.SERVER_EMAIL
276
277
278
        send_mail(_(EMAIL_CHANGE_EMAIL_SUBJECT), t.render(Context(c)),
                  from_email, [ec.new_email_address],
                  connection=get_connection())
279
280
281
282
    except (SMTPException, socket.error) as e:
        logger.exception(e)
        raise ChangeEmailError()
    else:
283
        msg = 'Sent change email for %s' % ec.user.email
284
        logger.log(LOGGING_LEVEL, msg)
Antony Chazapis's avatar
Antony Chazapis committed
285

286

287
288
289
290
def activate(
    user,
    email_template_name='im/welcome_email.txt',
    helpdesk_email_template_name='im/helpdesk_notification.txt',
291
    verify_email=False):
292
293
    """
    Activates the specific user and sends email.
294

295
    Raises SendGreetingError, ValidationError
296
297
    """
    user.is_active = True
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
298
    user.email_verified = True
299
300
    if not user.activation_sent:
        user.activation_sent = datetime.now()
301
    user.save()
302
    register_user_with_quotas(user)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
303
    send_helpdesk_notification(user, helpdesk_email_template_name)
304
305
    send_greeting(user, email_template_name)

306
307
308
309
def deactivate(user):
    user.is_active = False
    user.save()

310
311
312
313
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
314
    inviter.invitations = max(0, inviter.invitations - 1)
315
    inviter.save()
316

Olga Brani's avatar
Olga Brani committed
317
318
319
320
321
def switch_account_to_shibboleth(user, local_user,
                                 greeting_template_name='im/welcome_email.txt'):
    try:
        provider = user.provider
    except AttributeError:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
322
        return
Olga Brani's avatar
Olga Brani committed
323
324
325
326
327
328
329
330
331
    else:
        if not provider == 'shibboleth':
            return
        user.delete()
        local_user.provider = 'shibboleth'
        local_user.third_party_identifier = user.third_party_identifier
        local_user.save()
        send_greeting(local_user, greeting_template_name)
        return local_user
332

333

334
class SendMailError(Exception):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
335
    pass
336

337

338
339
class SendAdminNotificationError(SendMailError):
    def __init__(self):
Olga Brani's avatar
Olga Brani committed
340
        self.message = _(astakos_messages.ADMIN_NOTIFICATION_SEND_ERR)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
341
        super(SendAdminNotificationError, self).__init__()
342

343

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
344
class SendVerificationError(SendMailError):
345
    def __init__(self):
Olga Brani's avatar
Olga Brani committed
346
        self.message = _(astakos_messages.VERIFICATION_SEND_ERR)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
347
        super(SendVerificationError, self).__init__()
348

349

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
350
class SendInvitationError(SendMailError):
351
    def __init__(self):
Olga Brani's avatar
Olga Brani committed
352
        self.message = _(astakos_messages.INVITATION_SEND_ERR)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
353
        super(SendInvitationError, self).__init__()
354

355

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
356
class SendGreetingError(SendMailError):
357
    def __init__(self):
Olga Brani's avatar
Olga Brani committed
358
        self.message = _(astakos_messages.GREETING_SEND_ERR)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
359
        super(SendGreetingError, self).__init__()
360

361

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
362
class SendFeedbackError(SendMailError):
363
    def __init__(self):
Olga Brani's avatar
Olga Brani committed
364
        self.message = _(astakos_messages.FEEDBACK_SEND_ERR)
365
366
        super(SendFeedbackError, self).__init__()

367

368
369
class ChangeEmailError(SendMailError):
    def __init__(self):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
370
        self.message = _(astakos_messages.CHANGE_EMAIL_SEND_ERR)
371
        super(ChangeEmailError, self).__init__()
372

373

374
375
class SendNotificationError(SendMailError):
    def __init__(self):
Olga Brani's avatar
Olga Brani committed
376
        self.message = _(astakos_messages.NOTIFICATION_SEND_ERR)
377
        super(SendNotificationError, self).__init__()
378
379


380
381
382
383
384
385
386
def register_user_with_quotas(user):
    rejected = register_users([user])
    if not rejected:
        quotas = users_quotas([user])
        register_quotas(quotas)


387
def get_quota(users):
388
    resources = get_resource_names()
389
    return qh_get_quota(users, resources)
390
391


392
### PROJECT VIEWS ###
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
393

394
395
396
397
398
AUTO_ACCEPT_POLICY = 1
MODERATED_POLICY   = 2
CLOSED_POLICY      = 3

POLICIES = [ AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY ]
399
400
401
402
403
404
405
406

def get_project_by_application_id(project_application_id):
    try:
        return Project.objects.get(application__id=project_application_id)
    except Project.DoesNotExist:
        raise IOError(
            _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % project_application_id)

407
408
409
410
411
412
413
414
415
def get_related_project_id(application_id):
    try:
        app = ProjectApplication.objects.get(id=application_id)
        chain = app.chain
        project = Project.objects.get(id=chain)
        return chain
    except:
        return None

416
417
418
419
420
421
422
423
def get_chain_of_application_id(application_id):
    try:
        app = ProjectApplication.objects.get(id=application_id)
        chain = app.chain
        return chain.chain
    except:
        return None

424
425
426
427
428
429
430
def get_project_by_id(project_id):
    try:
        return Project.objects.get(id=project_id)
    except Project.DoesNotExist:
        raise IOError(
            _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id)

431
432
433
434
435
def get_project_by_name(name):
    try:
        return Project.objects.get(name=name)
    except Project.DoesNotExist:
        raise IOError(
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
436
            _(astakos_messages.UNKNOWN_PROJECT_ID) % name)
437
438


439
440
def get_project_for_update(project_id):
    try:
441
        return Project.objects.get_for_update(id=project_id)
442
443
444
445
    except Project.DoesNotExist:
        raise IOError(
            _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id)

446
447
def get_application_for_update(application_id):
    try:
448
        return ProjectApplication.objects.get_for_update(id=application_id)
449
450
451
452
    except ProjectApplication.DoesNotExist:
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % application_id
        raise IOError(m)

453
454
def get_user_by_id(user_id):
    try:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
455
        return AstakosUser.objects.get(id=user_id)
456
457
458
    except AstakosUser.DoesNotExist:
        raise IOError(_(astakos_messages.UNKNOWN_USER_ID) % user_id)

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
459
460
461
462
def get_user_by_uuid(uuid):
    try:
        return AstakosUser.objects.get(uuid=uuid)
    except AstakosUser.DoesNotExist:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
463
        raise IOError(_(astakos_messages.UNKNOWN_USER_ID) % uuid)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
464

465
def create_membership(project, user):
466
    if isinstance(user, (int, long)):
467
        user = get_user_by_id(user)
468

469
470
471
472
    if not user.is_active:
        m = _(astakos_messages.ACCOUNT_NOT_ACTIVE)
        raise PermissionDenied(m)

473
    m, created = ProjectMembership.objects.get_or_create(
474
        project=project,
475
476
477
        person=user)

    if created:
478
        return m
479
480
481
482
    else:
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
        raise PermissionDenied(msg)

483

484
def get_membership_for_update(project, user):
485
    if isinstance(user, (int, long)):
486
487
        user = get_user_by_id(user)
    try:
488
489
        objs = ProjectMembership.objects
        m = objs.get_for_update(project=project, person=user)
490
491
492
        if m.is_pending:
            raise PendingMembershipError()
        return m
493
494
495
    except ProjectMembership.DoesNotExist:
        raise IOError(_(astakos_messages.NOT_MEMBERSHIP_REQUEST))

496
497
498
499
500
501
502
503
504
def checkAllowed(entity, request_user):
    if isinstance(entity, Project):
        application = entity.application
    elif isinstance(entity, ProjectApplication):
        application = entity
    else:
        m = "%s not a Project nor a ProjectApplication" % (entity,)
        raise ValueError(m)

505
    if request_user and \
506
        (not application.owner == request_user and \
507
508
509
510
511
512
513
514
            not request_user.is_superuser):
        raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))

def checkAlive(project):
    if not project.is_alive:
        raise PermissionDenied(
            _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)

515
def accept_membership_checks(project, request_user):
516
517
    checkAllowed(project, request_user)
    checkAlive(project)
518
519
520
521
522

    join_policy = project.application.member_join_policy
    if join_policy == CLOSED_POLICY:
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))

523
    if project.violates_members_limit(adding=1):
524
525
        raise PermissionDenied(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))

526
def accept_membership(project_id, user, request_user=None):
527
    project = get_project_for_update(project_id)
528
    accept_membership_checks(project, request_user)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
529

530
    membership = get_membership_for_update(project, user)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
531
532
533
    if not membership.can_accept():
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
        raise PermissionDenied(m)
534

535
536
    membership.accept()

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
537
538
    membership_change_notify(project, membership.person, 'accepted')

539
540
    return membership

541
def reject_membership_checks(project, request_user):
542
543
    checkAllowed(project, request_user)
    checkAlive(project)
544

545
def reject_membership(project_id, user, request_user=None):
546
    project = get_project_for_update(project_id)
547
    reject_membership_checks(project, request_user)
548
    membership = get_membership_for_update(project, user)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
549
550
551
    if not membership.can_reject():
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
        raise PermissionDenied(m)
552

553
554
    membership.reject()

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
555
556
    membership_change_notify(project, membership.person, 'rejected')

557
558
    return membership

559
560
561
562
563
564
565
566
567
568
569
570
571
def cancel_membership_checks(project):
    checkAlive(project)

def cancel_membership(project_id, user_id):
    project = get_project_for_update(project_id)
    cancel_membership_checks(project)
    membership = get_membership_for_update(project, user_id)
    if not membership.can_cancel():
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
        raise PermissionDenied(m)

    membership.cancel()

572
def remove_membership_checks(project, request_user=None):
573
574
    checkAllowed(project, request_user)
    checkAlive(project)
575

576
577
578
579
    leave_policy = project.application.member_leave_policy
    if leave_policy == CLOSED_POLICY:
        raise PermissionDenied(_(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED))

580
def remove_membership(project_id, user, request_user=None):
581
    project = get_project_for_update(project_id)
582
    remove_membership_checks(project, request_user)
583
    membership = get_membership_for_update(project, user)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
584
585
586
    if not membership.can_remove():
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
        raise PermissionDenied(m)
587

588
589
    membership.remove()

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
590
591
    membership_change_notify(project, membership.person, 'removed')

592
593
    return membership

594
def enroll_member(project_id, user, request_user=None):
595
    project = get_project_for_update(project_id)
596
    accept_membership_checks(project, request_user)
597
    membership = create_membership(project, user)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
598
599
600
601

    if not membership.can_accept():
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
        raise PermissionDenied(m)
602

603
    membership.accept()
604
    membership_enroll_notify(project, membership.person)
605
606

    return membership
607

608
def leave_project_checks(project):
609
    checkAlive(project)
610

611
    leave_policy = project.application.member_leave_policy
612
    if leave_policy == CLOSED_POLICY:
613
614
        raise PermissionDenied(_(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED))

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
615
616
617
618
619
620
621
622
623
624
625
def can_leave_request(project, user):
    leave_policy = project.application.member_leave_policy
    if leave_policy == CLOSED_POLICY:
        return False
    m = user.get_membership(project)
    if m is None:
        return False
    if m.state != ProjectMembership.ACCEPTED:
        return False
    return True

626
def leave_project(project_id, user_id):
627
    project = get_project_for_update(project_id)
628
    leave_project_checks(project)
629
    membership = get_membership_for_update(project, user_id)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
630
631
632
    if not membership.can_leave():
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
        raise PermissionDenied(m)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
633

634
    auto_accepted = False
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
635
    leave_policy = project.application.member_leave_policy
636
    if leave_policy == AUTO_ACCEPT_POLICY:
637
        membership.remove()
638
        auto_accepted = True
639
    else:
640
        membership.leave_request()
641
642
        membership_leave_request_notify(project, membership.person)
    return auto_accepted
643

644
def join_project_checks(project):
645
    checkAlive(project)
646

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
647
    join_policy = project.application.member_join_policy
648
    if join_policy == CLOSED_POLICY:
649
650
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
651
652
653
654
655
656
657
658
659
def can_join_request(project, user):
    join_policy = project.application.member_join_policy
    if join_policy == CLOSED_POLICY:
        return False
    m = user.get_membership(project)
    if m:
        return False
    return True

660
def join_project(project_id, user_id):
661
    project = get_project_for_update(project_id)
662
    join_project_checks(project)
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
663
    membership = create_membership(project, user_id)
664

665
    auto_accepted = False
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
666
    join_policy = project.application.member_join_policy
667
    if (join_policy == AUTO_ACCEPT_POLICY and
668
        not project.violates_members_limit(adding=1)):
669
        membership.accept()
670
671
672
673
674
        auto_accepted = True
    else:
        membership_request_notify(project, membership.person)

    return auto_accepted
675

676
677
678
def submit_application(kw, request_user=None):

    kw['applicant'] = request_user
679
    resource_policies = kw.pop('resource_policies', None)
680

681
    precursor = None
682
683
    precursor_id = kw.get('precursor_application', None)
    if precursor_id is not None:
684
685
        objs = ProjectApplication.objects
        precursor = objs.get_for_update(id=precursor_id)
686
        kw['precursor_application'] = precursor
687

688
689
        if (request_user and
            (not precursor.owner == request_user and
690
691
             not request_user.is_superuser
             and not request_user.is_project_admin())):
692
693
            m = _(astakos_messages.NOT_ALLOWED)
            raise PermissionDenied(m)
694

695
696
697
    owner = kw['owner']
    reached, limit = reached_pending_application_limit(owner.id, precursor)
    if not request_user.is_project_admin() and reached:
698
699
700
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
        raise PermissionDenied(m)

701
    application = ProjectApplication(**kw)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
702

703
704
705
706
707
    if precursor is None:
        application.chain = new_chain()
    else:
        chain = precursor.chain
        application.chain = chain
708
709
710
        objs = ProjectApplication.objects
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
        pending = q.select_for_update()
711
712
713
714
715
716
        for app in pending:
            app.state = ProjectApplication.REPLACED
            app.save()

    application.save()
    application.resource_policies = resource_policies
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
717
718
    application_submit_notify(application)
    return application
719

720
721
722
723
def cancel_application(application_id, request_user=None):
    application = get_application_for_update(application_id)
    checkAllowed(application, request_user)

724
725
726
727
    if not application.can_cancel():
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL % (
                application.id, application.state_display()))
        raise PermissionDenied(m)
728
729
730
731
732
733
734

    application.cancel()

def dismiss_application(application_id, request_user=None):
    application = get_application_for_update(application_id)
    checkAllowed(application, request_user)

735
736
737
738
    if not application.can_dismiss():
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS % (
                application.id, application.state_display()))
        raise PermissionDenied(m)
739
740
741

    application.dismiss()

742
def deny_application(application_id, reason=None):
743
    application = get_application_for_update(application_id)
744
745
746
747
748

    if not application.can_deny():
        m = _(astakos_messages.APPLICATION_CANNOT_DENY % (
                application.id, application.state_display()))
        raise PermissionDenied(m)
749

750
751
752
    if reason is None:
        reason = ""
    application.deny(reason)
753
754
    application_deny_notify(application)

755
def approve_application(app_id):
756
757

    try:
758
759
        objects = ProjectApplication.objects
        application = objects.get_for_update(id=app_id)
760
    except ProjectApplication.DoesNotExist:
761
762
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID % (app_id,))
        raise PermissionDenied(m)
763

764
765
766
767
768
    if not application.can_approve():
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE % (
                application.id, application.state_display()))
        raise PermissionDenied(m)

769
    application.approve()
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
770
    application_approve_notify(application)
771

772
773
774
775
776
777
778
779
780
def check_expiration(execute=False):
    objects = Project.objects
    expired = objects.expired_projects()
    if execute:
        for project in expired:
            terminate(project.id)

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

781
def terminate(project_id):
782
    project = get_project_for_update(project_id)
783
    checkAlive(project)
784
785

    project.terminate()
786

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
787
    project_termination_notify(project)
788
789
790

def suspend(project_id):
    project = get_project_by_id(project_id)
791
792
793
    checkAlive(project)

    project.suspend()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
794

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
795
    project_suspension_notify(project)
796
797
798
799
800
801
802
803
804

def resume(project_id):
    project = get_project_for_update(project_id)

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

    project.resume()
805
806
807
808
809
810
811
812
813
814
815
816

def get_by_chain_or_404(chain_id):
    try:
        project = Project.objects.get(id=chain_id)
        application = project.application
        return project, application
    except:
        application = ProjectApplication.objects.latest_of_chain(chain_id)
        if application is None:
            raise Http404
        else:
            return None, application
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863


def get_user_setting(user_id, key):
    try:
        setting = UserSetting.objects.get(
            user=user_id, setting=key)
        return setting.value
    except UserSetting.DoesNotExist:
        return getattr(astakos_settings, key)


def set_user_setting(user_id, key, value):
    try:
        setting = UserSetting.objects.get_for_update(
            user=user_id, setting=key)
    except UserSetting.DoesNotExist:
        setting = UserSetting(user_id=user_id, setting=key)
    setting.value = value
    setting.save()


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


PENDING_APPLICATION_LIMIT_SETTING = 'PENDING_APPLICATION_LIMIT'

def get_pending_application_limit(user_id):
    key = PENDING_APPLICATION_LIMIT_SETTING
    return get_user_setting(user_id, key)


def set_pending_application_limit(user_id, value):
    key = PENDING_APPLICATION_LIMIT_SETTING
    return set_user_setting(user_id, key, value)


def unset_pending_application_limit(user_id):
    key = PENDING_APPLICATION_LIMIT_SETTING
    return unset_user_setting(user_id, key)


def _reached_pending_application_limit(user_id):
    limit = get_pending_application_limit(user_id)

    PENDING = ProjectApplication.PENDING
    pending = ProjectApplication.objects.filter(
864
        owner__id=user_id, state=PENDING).count()
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883

    return pending >= limit, limit


def reached_pending_application_limit(user_id, precursor=None):
    reached, limit = _reached_pending_application_limit(user_id)

    if precursor is None:
        return reached, limit

    chain = precursor.chain
    objs = ProjectApplication.objects
    q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
    has_pending = q.exists()

    if not has_pending:
        return reached, limit

    return False, limit