Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 903ce7dc

History | View | Annotate | Download (28.7 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
import socket
36

    
37
from django.utils.translation import ugettext as _
38
from synnefo_branding.utils import render_to_string
39
from django.core.mail import send_mail, get_connection
40
from django.core.urlresolvers import reverse
41
from django.template import Context, loader
42
from django.contrib.auth import (
43
    login as auth_login,
44
    logout as auth_logout)
45
from django.contrib.auth.models import AnonymousUser
46
from django.core.exceptions import PermissionDenied
47
from django.db import IntegrityError
48
from django.http import Http404
49

    
50
from urllib import quote
51
from smtplib import SMTPException
52
from datetime import datetime
53
from functools import wraps
54

    
55
from synnefo.lib import join_urls
56
from astakos.im.settings import (
57
    CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL,
58
    VERIFICATION_EMAIL_SUBJECT, ACCOUNT_CREATION_SUBJECT,
59
    GROUP_CREATION_SUBJECT, HELPDESK_NOTIFICATION_EMAIL_SUBJECT,
60
    INVITATION_EMAIL_SUBJECT, GREETING_EMAIL_SUBJECT, FEEDBACK_EMAIL_SUBJECT,
61
    EMAIL_CHANGE_EMAIL_SUBJECT,
62
    PROJECT_CREATION_SUBJECT, PROJECT_APPROVED_SUBJECT,
63
    PROJECT_TERMINATION_SUBJECT, PROJECT_SUSPENSION_SUBJECT,
64
    PROJECT_MEMBERSHIP_CHANGE_SUBJECT)
65
from astakos.im.notifications import build_notification, NotificationError
66
from astakos.im.models import (
67
    AstakosUser, Invitation, ProjectMembership, ProjectApplication, Project,
68
    UserSetting,
69
    get_resource_names, new_chain)
70
from astakos.im.quotas import (qh_sync_user, qh_sync_users,
71
                               register_pending_apps, qh_sync_project)
72
from astakos.im.project_notif import (
73
    membership_change_notify, membership_enroll_notify,
74
    membership_request_notify, membership_leave_request_notify,
75
    application_submit_notify, application_approve_notify,
76
    application_deny_notify,
77
    project_termination_notify, project_suspension_notify)
78
from astakos.im import settings
79
import astakos.im.messages as astakos_messages
80

    
81
logger = logging.getLogger(__name__)
82

    
83

    
84
def login(request, user):
85
    auth_login(request, user)
86
    from astakos.im.models import SessionCatalog
87
    SessionCatalog(
88
        session_key=request.session.session_key,
89
        user=user
90
    ).save()
91
    logger.info('%s logged in.', user.log_display)
92

    
93

    
94
def logout(request, *args, **kwargs):
95
    user = request.user
96
    auth_logout(request, *args, **kwargs)
97
    logger.info('%s logged out.', user.log_display)
98

    
99

    
100
def send_verification(user, template_name='im/activation_email.txt'):
101
    """
102
    Send email to user to verify his/her email and activate his/her account.
103
    """
104
    url = join_urls(BASEURL, user.get_activation_url(nxt=reverse('index')))
105
    message = render_to_string(template_name, {
106
                               'user': user,
107
                               'url': url,
108
                               'baseurl': BASEURL,
109
                               'site_name': SITENAME,
110
                               'support': CONTACT_EMAIL})
111
    sender = settings.SERVER_EMAIL
112
    send_mail(_(VERIFICATION_EMAIL_SUBJECT), message, sender, [user.email],
113
              connection=get_connection())
114
    logger.info("Sent user verirfication email: %s", user.log_display)
115

    
116

    
117
def _send_admin_notification(template_name,
118
                             context=None,
119
                             user=None,
120
                             msg="",
121
                             subject='alpha2 testing notification',):
122
    """
123
    Send notification email to settings.HELPDESK + settings.MANAGERS +
124
    settings.ADMINS.
125
    """
126
    if context is None:
127
        context = {}
128
    if not 'user' in context:
129
        context['user'] = user
130

    
131
    message = render_to_string(template_name, context)
132
    sender = settings.SERVER_EMAIL
133
    recipient_list = [e[1] for e in settings.HELPDESK +
134
                      settings.MANAGERS + settings.ADMINS]
135
    send_mail(subject, message, sender, recipient_list,
136
              connection=get_connection())
137
    if user:
138
        msg = 'Sent admin notification (%s) for user %s' % (msg,
139
                                                            user.log_display)
140
    else:
141
        msg = 'Sent admin notification (%s)' % msg
142

    
143
    logger.log(LOGGING_LEVEL, msg)
144

    
145

    
146
def send_account_pending_moderation_notification(
147
        user,
148
        template_name='im/account_pending_moderation_notification.txt'):
149
    """
150
    Notify admins that a new user has verified his email address and moderation
151
    step is required to activate his account.
152
    """
153
    subject = _(ACCOUNT_CREATION_SUBJECT) % {'user': user.email}
154
    return _send_admin_notification(template_name, {}, subject=subject,
155
                                    user=user, msg="account creation")
156

    
157

    
158
def send_account_activated_notification(
159
        user,
160
        template_name='im/account_activated_notification.txt'):
161
    """
162
    Send email to settings.HELPDESK + settings.MANAGERES + settings.ADMINS
163
    lists to notify that a new account has been accepted and activated.
164
    """
165
    message = render_to_string(
166
        template_name,
167
        {'user': user}
168
    )
169
    sender = settings.SERVER_EMAIL
170
    recipient_list = [e[1] for e in settings.HELPDESK +
171
                      settings.MANAGERS + settings.ADMINS]
172
    send_mail(_(HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % {'user': user.email},
173
              message, sender, recipient_list, connection=get_connection())
174
    msg = 'Sent helpdesk admin notification for %s' % user.email
175
    logger.log(LOGGING_LEVEL, msg)
176

    
177

    
178
def send_invitation(invitation, template_name='im/invitation.txt'):
179
    """
180
    Send invitation email.
181
    """
182
    subject = _(INVITATION_EMAIL_SUBJECT)
183
    url = '%s?code=%d' % (join_urls(BASEURL, reverse('index')), invitation.code)
184
    message = render_to_string(template_name, {
185
                               'invitation': invitation,
186
                               'url': url,
187
                               'baseurl': BASEURL,
188
                               'site_name': SITENAME,
189
                               'support': CONTACT_EMAIL})
190
    sender = settings.SERVER_EMAIL
191
    send_mail(subject, message, sender, [invitation.username],
192
              connection=get_connection())
193
    msg = 'Sent invitation %s' % invitation
194
    logger.log(LOGGING_LEVEL, msg)
195
    inviter_invitations = invitation.inviter.invitations
196
    invitation.inviter.invitations = max(0, inviter_invitations - 1)
197
    invitation.inviter.save()
198

    
199

    
200
def send_greeting(user, email_template_name='im/welcome_email.txt'):
201
    """
202
    Send welcome email to an accepted/activated user.
203

204
    Raises SMTPException, socket.error
205
    """
206
    subject = _(GREETING_EMAIL_SUBJECT)
207
    message = render_to_string(email_template_name, {
208
                               'user': user,
209
                               'url': join_urls(BASEURL, reverse('index')),
210
                               'baseurl': BASEURL,
211
                               'site_name': SITENAME,
212
                               'support': CONTACT_EMAIL})
213
    sender = settings.SERVER_EMAIL
214
    send_mail(subject, message, sender, [user.email],
215
              connection=get_connection())
216
    msg = 'Sent greeting %s' % user.log_display
217
    logger.log(LOGGING_LEVEL, msg)
218

    
219

    
220
def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'):
221
    subject = _(FEEDBACK_EMAIL_SUBJECT)
222
    from_email = settings.SERVER_EMAIL
223
    recipient_list = [e[1] for e in settings.HELPDESK]
224
    content = render_to_string(email_template_name, {
225
        'message': msg,
226
        'data': data,
227
        'user': user})
228
    try:
229
        send_mail(subject, content, from_email, recipient_list,
230
                  connection=get_connection())
231
    except (SMTPException, socket.error) as e:
232
        logger.exception(e)
233
        raise SendFeedbackError()
234
    else:
235
        msg = 'Sent feedback from %s' % user.log_display
236
        logger.log(LOGGING_LEVEL, msg)
237

    
238

    
239
def send_change_email(
240
    ec, request, email_template_name='registration/email_change_email.txt'):
241
    try:
242
        url = ec.get_url()
243
        url = request.build_absolute_uri(url)
244
        t = loader.get_template(email_template_name)
245
        c = {'url': url, 'site_name': SITENAME,
246
             'support': CONTACT_EMAIL, 'ec': ec}
247
        from_email = settings.SERVER_EMAIL
248
        send_mail(_(EMAIL_CHANGE_EMAIL_SUBJECT), t.render(Context(c)),
249
                  from_email, [ec.new_email_address],
250
                  connection=get_connection())
251
    except (SMTPException, socket.error) as e:
252
        logger.exception(e)
253
        raise ChangeEmailError()
254
    else:
255
        msg = 'Sent change email for %s' % ec.user.log_display
256
        logger.log(LOGGING_LEVEL, msg)
257

    
258

    
259
def invite(inviter, email, realname):
260
    inv = Invitation(inviter=inviter, username=email, realname=realname)
261
    inv.save()
262
    send_invitation(inv)
263
    inviter.invitations = max(0, inviter.invitations - 1)
264
    inviter.save()
265

    
266

    
267
### PROJECT FUNCTIONS ###
268

    
269
AUTO_ACCEPT_POLICY = 1
270
MODERATED_POLICY = 2
271
CLOSED_POLICY = 3
272

    
273
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
274

    
275

    
276
def get_project_by_application_id(project_application_id):
277
    try:
278
        return Project.objects.get(application__id=project_application_id)
279
    except Project.DoesNotExist:
280
        m = (_(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) %
281
             project_application_id)
282
        raise IOError(m)
283

    
284

    
285
def get_related_project_id(application_id):
286
    try:
287
        app = ProjectApplication.objects.get(id=application_id)
288
        chain = app.chain
289
        Project.objects.get(id=chain)
290
        return chain
291
    except (ProjectApplication.DoesNotExist, Project.DoesNotExist):
292
        return None
293

    
294

    
295
def get_chain_of_application_id(application_id):
296
    try:
297
        app = ProjectApplication.objects.get(id=application_id)
298
        chain = app.chain
299
        return chain.chain
300
    except ProjectApplication.DoesNotExist:
301
        return None
302

    
303

    
304
def get_project_by_id(project_id):
305
    try:
306
        return Project.objects.get(id=project_id)
307
    except Project.DoesNotExist:
308
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
309
        raise IOError(m)
310

    
311

    
312
def get_project_by_name(name):
313
    try:
314
        return Project.objects.get(name=name)
315
    except Project.DoesNotExist:
316
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % name
317
        raise IOError(m)
318

    
319

    
320
def get_project_for_update(project_id):
321
    try:
322
        return Project.objects.get_for_update(id=project_id)
323
    except Project.DoesNotExist:
324
        m = _(astakos_messages.UNKNOWN_PROJECT_ID) % project_id
325
        raise IOError(m)
326

    
327

    
328
def get_application_for_update(application_id):
329
    try:
330
        return ProjectApplication.objects.get_for_update(id=application_id)
331
    except ProjectApplication.DoesNotExist:
332
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % application_id
333
        raise IOError(m)
334

    
335

    
336
def get_user_by_id(user_id):
337
    try:
338
        return AstakosUser.objects.get(id=user_id)
339
    except AstakosUser.DoesNotExist:
340
        m = _(astakos_messages.UNKNOWN_USER_ID) % user_id
341
        raise IOError(m)
342

    
343

    
344
def get_user_by_uuid(uuid):
345
    try:
346
        return AstakosUser.objects.get(uuid=uuid)
347
    except AstakosUser.DoesNotExist:
348
        m = _(astakos_messages.UNKNOWN_USER_ID) % uuid
349
        raise IOError(m)
350

    
351

    
352
def get_membership_for_update(project_id, user_id):
353
    try:
354
        objs = ProjectMembership.objects
355
        return objs.get_for_update(project__id=project_id,
356
                                   person__id=user_id)
357
    except ProjectMembership.DoesNotExist:
358
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
359
        raise IOError(m)
360

    
361

    
362
def get_membership_for_update_by_id(project_id, memb_id):
363
    try:
364
        objs = ProjectMembership.objects
365
        return objs.get_for_update(project__id=project_id,
366
                                   id=memb_id)
367
    except ProjectMembership.DoesNotExist:
368
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
369
        raise IOError(m)
370

    
371

    
372
def checkAllowed(entity, request_user, admin_only=False):
373
    if isinstance(entity, Project):
374
        application = entity.application
375
    elif isinstance(entity, ProjectApplication):
376
        application = entity
377
    else:
378
        m = "%s not a Project nor a ProjectApplication" % (entity,)
379
        raise ValueError(m)
380

    
381
    if not request_user or request_user.is_project_admin():
382
        return
383

    
384
    if not admin_only and application.owner == request_user:
385
        return
386

    
387
    m = _(astakos_messages.NOT_ALLOWED)
388
    raise PermissionDenied(m)
389

    
390

    
391
def checkAlive(project):
392
    if not project.is_alive:
393
        m = _(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__
394
        raise PermissionDenied(m)
395

    
396

    
397
def accept_membership_checks(project, request_user):
398
    checkAllowed(project, request_user)
399
    checkAlive(project)
400

    
401
    join_policy = project.application.member_join_policy
402
    if join_policy == CLOSED_POLICY:
403
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
404
        raise PermissionDenied(m)
405

    
406
    if project.violates_members_limit(adding=1):
407
        m = _(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED)
408
        raise PermissionDenied(m)
409

    
410

    
411
def accept_membership(project_id, memb_id, request_user=None):
412
    project = get_project_for_update(project_id)
413
    accept_membership_checks(project, request_user)
414

    
415
    membership = get_membership_for_update_by_id(project_id, memb_id)
416
    if not membership.can_accept():
417
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
418
        raise PermissionDenied(m)
419

    
420
    user = membership.person
421
    membership.accept()
422
    qh_sync_user(user)
423
    logger.info("User %s has been accepted in %s." %
424
                (user.log_display, project))
425

    
426
    membership_change_notify(project, user, 'accepted')
427
    return membership
428

    
429

    
430
def reject_membership_checks(project, request_user):
431
    checkAllowed(project, request_user)
432
    checkAlive(project)
433

    
434

    
435
def reject_membership(project_id, memb_id, request_user=None):
436
    project = get_project_for_update(project_id)
437
    reject_membership_checks(project, request_user)
438
    membership = get_membership_for_update_by_id(project_id, memb_id)
439
    if not membership.can_reject():
440
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
441
        raise PermissionDenied(m)
442

    
443
    user = membership.person
444
    membership.reject()
445
    logger.info("Request of user %s for %s has been rejected." %
446
                (user.log_display, project))
447

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

    
451

    
452
def cancel_membership_checks(project):
453
    checkAlive(project)
454

    
455

    
456
def cancel_membership(project_id, request_user):
457
    project = get_project_for_update(project_id)
458
    cancel_membership_checks(project)
459
    membership = get_membership_for_update(project_id, request_user.id)
460
    if not membership.can_cancel():
461
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
462
        raise PermissionDenied(m)
463

    
464
    membership.cancel()
465
    logger.info("Request of user %s for %s has been cancelled." %
466
                (membership.person.log_display, project))
467

    
468

    
469
def remove_membership_checks(project, request_user=None):
470
    checkAllowed(project, request_user)
471
    checkAlive(project)
472

    
473
    leave_policy = project.application.member_leave_policy
474
    if leave_policy == CLOSED_POLICY:
475
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
476
        raise PermissionDenied(m)
477

    
478

    
479
def remove_membership(project_id, memb_id, request_user=None):
480
    project = get_project_for_update(project_id)
481
    remove_membership_checks(project, request_user)
482
    membership = get_membership_for_update_by_id(project_id, memb_id)
483
    if not membership.can_remove():
484
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
485
        raise PermissionDenied(m)
486

    
487
    user = membership.person
488
    membership.remove()
489
    qh_sync_user(user)
490
    logger.info("User %s has been removed from %s." %
491
                (user.log_display, project))
492

    
493
    membership_change_notify(project, user, 'removed')
494
    return membership
495

    
496

    
497
def enroll_member(project_id, user, request_user=None):
498
    project = get_project_for_update(project_id)
499
    accept_membership_checks(project, request_user)
500

    
501
    membership, created = ProjectMembership.objects.get_or_create(
502
        project=project,
503
        person=user)
504

    
505
    if not membership.can_accept():
506
        m = _(astakos_messages.NOT_MEMBERSHIP_REQUEST)
507
        raise PermissionDenied(m)
508

    
509
    membership.accept()
510
    qh_sync_user(user)
511
    logger.info("User %s has been enrolled in %s." %
512
                (membership.person.log_display, project))
513

    
514
    membership_enroll_notify(project, membership.person)
515
    return membership
516

    
517

    
518
def leave_project_checks(project):
519
    checkAlive(project)
520

    
521
    leave_policy = project.application.member_leave_policy
522
    if leave_policy == CLOSED_POLICY:
523
        m = _(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED)
524
        raise PermissionDenied(m)
525

    
526

    
527
def can_leave_request(project, user):
528
    leave_policy = project.application.member_leave_policy
529
    if leave_policy == CLOSED_POLICY:
530
        return False
531
    m = user.get_membership(project)
532
    if m is None:
533
        return False
534
    if m.state != ProjectMembership.ACCEPTED:
535
        return False
536
    return True
537

    
538

    
539
def leave_project(project_id, request_user):
540
    project = get_project_for_update(project_id)
541
    leave_project_checks(project)
542
    membership = get_membership_for_update(project_id, request_user.id)
543
    if not membership.can_leave():
544
        m = _(astakos_messages.NOT_ACCEPTED_MEMBERSHIP)
545
        raise PermissionDenied(m)
546

    
547
    auto_accepted = False
548
    leave_policy = project.application.member_leave_policy
549
    if leave_policy == AUTO_ACCEPT_POLICY:
550
        membership.remove()
551
        qh_sync_user(request_user)
552
        logger.info("User %s has left %s." %
553
                    (membership.person.log_display, project))
554
        auto_accepted = True
555
    else:
556
        membership.leave_request()
557
        logger.info("User %s requested to leave %s." %
558
                    (membership.person.log_display, project))
559
        membership_leave_request_notify(project, membership.person)
560
    return auto_accepted
561

    
562

    
563
def join_project_checks(project):
564
    checkAlive(project)
565

    
566
    join_policy = project.application.member_join_policy
567
    if join_policy == CLOSED_POLICY:
568
        m = _(astakos_messages.MEMBER_JOIN_POLICY_CLOSED)
569
        raise PermissionDenied(m)
570

    
571

    
572
def can_join_request(project, user):
573
    join_policy = project.application.member_join_policy
574
    if join_policy == CLOSED_POLICY:
575
        return False
576
    m = user.get_membership(project)
577
    if m:
578
        return False
579
    return True
580

    
581

    
582
def join_project(project_id, request_user):
583
    project = get_project_for_update(project_id)
584
    join_project_checks(project)
585

    
586
    membership, created = ProjectMembership.objects.get_or_create(
587
        project=project,
588
        person=request_user)
589

    
590
    if not created:
591
        msg = _(astakos_messages.MEMBERSHIP_REQUEST_EXISTS)
592
        raise PermissionDenied(msg)
593

    
594
    auto_accepted = False
595
    join_policy = project.application.member_join_policy
596
    if (join_policy == AUTO_ACCEPT_POLICY and (
597
            not project.violates_members_limit(adding=1))):
598
        membership.accept()
599
        qh_sync_user(request_user)
600
        logger.info("User %s joined %s." %
601
                    (membership.person.log_display, project))
602
        auto_accepted = True
603
    else:
604
        membership_request_notify(project, membership.person)
605
        logger.info("User %s requested to join %s." %
606
                    (membership.person.log_display, project))
607
    return auto_accepted
608

    
609

    
610
def submit_application(owner=None,
611
                       name=None,
612
                       precursor_id=None,
613
                       homepage=None,
614
                       description=None,
615
                       start_date=None,
616
                       end_date=None,
617
                       member_join_policy=None,
618
                       member_leave_policy=None,
619
                       limit_on_members_number=None,
620
                       comments=None,
621
                       resource_policies=None,
622
                       request_user=None):
623

    
624
    precursor = None
625
    if precursor_id is not None:
626
        objs = ProjectApplication.objects
627
        precursor = objs.get_for_update(id=precursor_id)
628

    
629
        if (request_user and
630
            (not precursor.owner == request_user and
631
             not request_user.is_superuser
632
             and not request_user.is_project_admin())):
633
            m = _(astakos_messages.NOT_ALLOWED)
634
            raise PermissionDenied(m)
635

    
636
    force = request_user.is_project_admin()
637
    ok, limit = qh_add_pending_app(owner, precursor, force)
638
    if not ok:
639
        m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
640
        raise PermissionDenied(m)
641

    
642
    application = ProjectApplication(
643
        applicant=request_user,
644
        owner=owner,
645
        name=name,
646
        precursor_application_id=precursor_id,
647
        homepage=homepage,
648
        description=description,
649
        start_date=start_date,
650
        end_date=end_date,
651
        member_join_policy=member_join_policy,
652
        member_leave_policy=member_leave_policy,
653
        limit_on_members_number=limit_on_members_number,
654
        comments=comments)
655

    
656
    if precursor is None:
657
        application.chain = new_chain()
658
    else:
659
        chain = precursor.chain
660
        application.chain = chain
661
        objs = ProjectApplication.objects
662
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
663
        pending = q.select_for_update()
664
        for app in pending:
665
            app.state = ProjectApplication.REPLACED
666
            app.save()
667

    
668
    application.save()
669
    if resource_policies is not None:
670
        application.set_resource_policies(resource_policies)
671
    logger.info("User %s submitted %s." %
672
                (request_user.log_display, application.log_display))
673
    application_submit_notify(application)
674
    return application
675

    
676

    
677
def cancel_application(application_id, request_user=None, reason=""):
678
    application = get_application_for_update(application_id)
679
    checkAllowed(application, request_user)
680

    
681
    if not application.can_cancel():
682
        m = _(astakos_messages.APPLICATION_CANNOT_CANCEL %
683
              (application.id, application.state_display()))
684
        raise PermissionDenied(m)
685

    
686
    qh_release_pending_app(application.owner)
687

    
688
    application.cancel()
689
    logger.info("%s has been cancelled." % (application.log_display))
690

    
691

    
692
def dismiss_application(application_id, request_user=None, reason=""):
693
    application = get_application_for_update(application_id)
694
    checkAllowed(application, request_user)
695

    
696
    if not application.can_dismiss():
697
        m = _(astakos_messages.APPLICATION_CANNOT_DISMISS %
698
              (application.id, application.state_display()))
699
        raise PermissionDenied(m)
700

    
701
    application.dismiss()
702
    logger.info("%s has been dismissed." % (application.log_display))
703

    
704

    
705
def deny_application(application_id, request_user=None, reason=""):
706
    application = get_application_for_update(application_id)
707

    
708
    checkAllowed(application, request_user, admin_only=True)
709

    
710
    if not application.can_deny():
711
        m = _(astakos_messages.APPLICATION_CANNOT_DENY %
712
              (application.id, application.state_display()))
713
        raise PermissionDenied(m)
714

    
715
    qh_release_pending_app(application.owner)
716

    
717
    application.deny(reason)
718
    logger.info("%s has been denied with reason \"%s\"." %
719
                (application.log_display, reason))
720
    application_deny_notify(application)
721

    
722

    
723
def approve_application(app_id, request_user=None, reason=""):
724

    
725
    try:
726
        objects = ProjectApplication.objects
727
        application = objects.get_for_update(id=app_id)
728
    except ProjectApplication.DoesNotExist:
729
        m = _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID % (app_id,))
730
        raise PermissionDenied(m)
731

    
732
    checkAllowed(application, request_user, admin_only=True)
733

    
734
    if not application.can_approve():
735
        m = _(astakos_messages.APPLICATION_CANNOT_APPROVE %
736
              (application.id, application.state_display()))
737
        raise PermissionDenied(m)
738

    
739
    qh_release_pending_app(application.owner)
740
    project = application.approve(reason)
741
    qh_sync_project(project)
742
    logger.info("%s has been approved." % (application.log_display))
743
    application_approve_notify(application)
744

    
745

    
746
def check_expiration(execute=False):
747
    objects = Project.objects
748
    expired = objects.expired_projects()
749
    if execute:
750
        for project in expired:
751
            terminate(project.id)
752

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

    
755

    
756
def terminate(project_id, request_user=None):
757
    project = get_project_for_update(project_id)
758
    checkAllowed(project, request_user, admin_only=True)
759
    checkAlive(project)
760

    
761
    project.terminate()
762
    qh_sync_project(project)
763
    logger.info("%s has been terminated." % (project))
764

    
765
    project_termination_notify(project)
766

    
767

    
768
def suspend(project_id, request_user=None):
769
    project = get_project_for_update(project_id)
770
    checkAllowed(project, request_user, admin_only=True)
771
    checkAlive(project)
772

    
773
    project.suspend()
774
    qh_sync_project(project)
775
    logger.info("%s has been suspended." % (project))
776

    
777
    project_suspension_notify(project)
778

    
779

    
780
def resume(project_id, request_user=None):
781
    project = get_project_for_update(project_id)
782
    checkAllowed(project, request_user, admin_only=True)
783

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

    
788
    project.resume()
789
    qh_sync_project(project)
790
    logger.info("%s has been unsuspended." % (project))
791

    
792

    
793
def get_by_chain_or_404(chain_id):
794
    try:
795
        project = Project.objects.get(id=chain_id)
796
        application = project.application
797
        return project, application
798
    except:
799
        application = ProjectApplication.objects.latest_of_chain(chain_id)
800
        if application is None:
801
            raise Http404
802
        else:
803
            return None, application
804

    
805

    
806
def get_user_setting(user_id, key):
807
    try:
808
        setting = UserSetting.objects.get(
809
            user=user_id, setting=key)
810
        return setting.value
811
    except UserSetting.DoesNotExist:
812
        return getattr(settings, key)
813

    
814

    
815
def set_user_setting(user_id, key, value):
816
    try:
817
        setting = UserSetting.objects.get_for_update(
818
            user=user_id, setting=key)
819
    except UserSetting.DoesNotExist:
820
        setting = UserSetting(user_id=user_id, setting=key)
821
    setting.value = value
822
    setting.save()
823

    
824

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

    
828

    
829
def _partition_by(f, l):
830
    d = {}
831
    for x in l:
832
        group = f(x)
833
        group_l = d.get(group, [])
834
        group_l.append(x)
835
        d[group] = group_l
836
    return d
837

    
838

    
839
def count_pending_app(users):
840
    users = list(users)
841
    apps = ProjectApplication.objects.filter(state=ProjectApplication.PENDING,
842
                                             owner__in=users)
843
    apps_d = _partition_by(lambda a: a.owner.uuid, apps)
844

    
845
    usage = {}
846
    for user in users:
847
        uuid = user.uuid
848
        usage[uuid] = len(apps_d.get(uuid, []))
849
    return usage
850

    
851

    
852
def qh_add_pending_app(user, precursor=None, force=False, dry_run=False):
853
    if precursor is None:
854
        diff = 1
855
    else:
856
        chain = precursor.chain
857
        objs = ProjectApplication.objects
858
        q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
859
        count = q.count()
860
        diff = 1 - count
861

    
862
    return register_pending_apps(user, diff, force, dry_run)
863

    
864

    
865
def qh_release_pending_app(user):
866
    register_pending_apps(user, -1)
867

    
868

    
869
class SendMailError(Exception):
870
    pass
871

    
872

    
873
class SendFeedbackError(SendMailError):
874
    def __init__(self):
875
        self.message = _(astakos_messages.FEEDBACK_SEND_ERR)
876
        super(SendFeedbackError, self).__init__()
877

    
878

    
879
class ChangeEmailError(SendMailError):
880
    def __init__(self):
881
        self.message = _(astakos_messages.CHANGE_EMAIL_SEND_ERR)
882
        super(ChangeEmailError, self).__init__()