Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / functions.py @ 7dd3047d

History | View | Annotate | Download (21 kB)

1
# Copyright 2011 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 django.template.loader import render_to_string
39
from django.core.mail import send_mail
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.conf import settings
46
from django.contrib.auth.models import AnonymousUser
47
from django.core.exceptions import PermissionDenied
48
from django.db import IntegrityError
49

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

    
56
from astakos.im.settings import (
57
    DEFAULT_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, ProjectMembership, ProjectApplication, Project,
68
    trigger_sync, get_closed_join, get_auto_accept_join,
69
    get_auto_accept_leave, get_closed_leave)
70

    
71
import astakos.im.messages as astakos_messages
72

    
73
logger = logging.getLogger(__name__)
74

    
75

    
76
def logged(func, msg):
77
    @wraps(func)
78
    def with_logging(*args, **kwargs):
79
        email = ''
80
        user = None
81
        try:
82
            request = args[0]
83
            email = request.user.email
84
        except (KeyError, AttributeError), e:
85
            email = ''
86
        r = func(*args, **kwargs)
87
        if LOGGING_LEVEL:
88
            logger.log(LOGGING_LEVEL, msg % email)
89
        return r
90
    return with_logging
91

    
92

    
93
def login(request, user):
94
    auth_login(request, user)
95
    from astakos.im.models import SessionCatalog
96
    SessionCatalog(
97
        session_key=request.session.session_key,
98
        user=user
99
    ).save()
100

    
101
login = logged(login, '%s logged in.')
102
logout = logged(auth_logout, '%s logged out.')
103

    
104

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

109
    Raises SendVerificationError
110
    """
111
    url = '%s?auth=%s&next=%s' % (urljoin(BASEURL, reverse('activate')),
112
                                  quote(user.auth_token),
113
                                  quote(urljoin(BASEURL, reverse('index'))))
114
    message = render_to_string(template_name, {
115
                               'user': user,
116
                               'url': url,
117
                               'baseurl': BASEURL,
118
                               'site_name': SITENAME,
119
                               'support': DEFAULT_CONTACT_EMAIL})
120
    sender = settings.SERVER_EMAIL
121
    try:
122
        send_mail(_(VERIFICATION_EMAIL_SUBJECT), message, sender, [user.email])
123
    except (SMTPException, socket.error) as e:
124
        logger.exception(e)
125
        raise SendVerificationError()
126
    else:
127
        msg = 'Sent activation %s' % user.email
128
        logger.log(LOGGING_LEVEL, msg)
129

    
130

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

    
136

    
137
def _send_admin_notification(template_name,
138
                             dictionary=None,
139
                             subject='alpha2 testing notification',):
140
    """
141
    Send notification email to settings.ADMINS.
142

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

    
161

    
162
def send_account_creation_notification(template_name, dictionary=None):
163
    user = dictionary.get('user', AnonymousUser())
164
    subject = _(ACCOUNT_CREATION_SUBJECT) % {'user':user.get('email', '')}
165
    return _send_admin_notification(template_name, dictionary, subject=subject)
166

    
167

    
168
def send_group_creation_notification(template_name, dictionary=None):
169
    group = dictionary.get('group')
170
    if not group:
171
        return
172
    subject = _(GROUP_CREATION_SUBJECT) % {'group':group.get('name', '')}
173
    return _send_admin_notification(template_name, dictionary, subject=subject)
174

    
175

    
176
def send_helpdesk_notification(user, template_name='im/helpdesk_notification.txt'):
177
    """
178
    Send email to DEFAULT_CONTACT_EMAIL to notify for a new user activation.
179

180
    Raises SendNotificationError
181
    """
182
    if not DEFAULT_CONTACT_EMAIL:
183
        return
184
    message = render_to_string(
185
        template_name,
186
        {'user': user}
187
    )
188
    sender = settings.SERVER_EMAIL
189
    try:
190
        send_mail(
191
            _(HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % {'user': user.email},
192
            message, sender, [DEFAULT_CONTACT_EMAIL])
193
    except (SMTPException, socket.error) as e:
194
        logger.exception(e)
195
        raise SendNotificationError()
196
    else:
197
        msg = 'Sent helpdesk admin notification for %s' % user.email
198
        logger.log(LOGGING_LEVEL, msg)
199

    
200

    
201
def send_invitation(invitation, template_name='im/invitation.txt'):
202
    """
203
    Send invitation email.
204

205
    Raises SendInvitationError
206
    """
207
    subject = _(INVITATION_EMAIL_SUBJECT)
208
    url = '%s?code=%d' % (urljoin(BASEURL, reverse('index')), invitation.code)
209
    message = render_to_string(template_name, {
210
                               'invitation': invitation,
211
                               'url': url,
212
                               'baseurl': BASEURL,
213
                               'site_name': SITENAME,
214
                               'support': DEFAULT_CONTACT_EMAIL})
215
    sender = settings.SERVER_EMAIL
216
    try:
217
        send_mail(subject, message, sender, [invitation.username])
218
    except (SMTPException, socket.error) as e:
219
        logger.exception(e)
220
        raise SendInvitationError()
221
    else:
222
        msg = 'Sent invitation %s' % invitation
223
        logger.log(LOGGING_LEVEL, msg)
224
        invitation.inviter.invitations = max(0, invitation.inviter.invitations - 1)
225
        invitation.inviter.save()
226

    
227

    
228
def send_greeting(user, email_template_name='im/welcome_email.txt'):
229
    """
230
    Send welcome email.
231

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

    
251

    
252
def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'):
253
    subject = _(FEEDBACK_EMAIL_SUBJECT)
254
    from_email = user.email
255
    recipient_list = [DEFAULT_CONTACT_EMAIL]
256
    content = render_to_string(email_template_name, {
257
        'message': msg,
258
        'data': data,
259
        'user': user})
260
    try:
261
        send_mail(subject, content, from_email, recipient_list)
262
    except (SMTPException, socket.error) as e:
263
        logger.exception(e)
264
        raise SendFeedbackError()
265
    else:
266
        msg = 'Sent feedback from %s' % user.email
267
        logger.log(LOGGING_LEVEL, msg)
268

    
269

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

    
287

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

296
    Raises SendGreetingError, ValidationError
297
    """
298
    user.is_active = True
299
    user.email_verified = True
300
    if not user.activation_sent:
301
        user.activation_sent = datetime.now()
302
    user.save()
303
    send_helpdesk_notification(user, helpdesk_email_template_name)
304
    send_greeting(user, email_template_name)
305

    
306
def invite(inviter, email, realname):
307
    inv = Invitation(inviter=inviter, username=email, realname=realname)
308
    inv.save()
309
    send_invitation(inv)
310
    inviter.invitations = max(0, self.invitations - 1)
311
    inviter.save()
312

    
313
def switch_account_to_shibboleth(user, local_user,
314
                                 greeting_template_name='im/welcome_email.txt'):
315
    try:
316
        provider = user.provider
317
    except AttributeError:
318
        return
319
    else:
320
        if not provider == 'shibboleth':
321
            return
322
        user.delete()
323
        local_user.provider = 'shibboleth'
324
        local_user.third_party_identifier = user.third_party_identifier
325
        local_user.save()
326
        send_greeting(local_user, greeting_template_name)
327
        return local_user
328

    
329

    
330
class SendMailError(Exception):
331
    pass
332

    
333

    
334
class SendAdminNotificationError(SendMailError):
335
    def __init__(self):
336
        self.message = _(astakos_messages.ADMIN_NOTIFICATION_SEND_ERR)
337
        super(SendAdminNotificationError, self).__init__()
338

    
339

    
340
class SendVerificationError(SendMailError):
341
    def __init__(self):
342
        self.message = _(astakos_messages.VERIFICATION_SEND_ERR)
343
        super(SendVerificationError, self).__init__()
344

    
345

    
346
class SendInvitationError(SendMailError):
347
    def __init__(self):
348
        self.message = _(astakos_messages.INVITATION_SEND_ERR)
349
        super(SendInvitationError, self).__init__()
350

    
351

    
352
class SendGreetingError(SendMailError):
353
    def __init__(self):
354
        self.message = _(astakos_messages.GREETING_SEND_ERR)
355
        super(SendGreetingError, self).__init__()
356

    
357

    
358
class SendFeedbackError(SendMailError):
359
    def __init__(self):
360
        self.message = _(astakos_messages.FEEDBACK_SEND_ERR)
361
        super(SendFeedbackError, self).__init__()
362

    
363

    
364
class ChangeEmailError(SendMailError):
365
    def __init__(self):
366
        self.message = _(astakos_messages.CHANGE_EMAIL_SEND_ERR)
367
        super(ChangeEmailError, self).__init__()
368

    
369

    
370
class SendNotificationError(SendMailError):
371
    def __init__(self):
372
        self.message = _(astakos_messages.NOTIFICATION_SEND_ERR)
373
        super(SendNotificationError, self).__init__()
374

    
375

    
376
### PROJECT VIEWS ###
377
def get_join_policy(str_policy):
378
    try:
379
        return MemberJoinPolicy.objects.get(policy=str_policy)
380
    except:
381
        return None
382

    
383
def get_leave_policy(str_policy):
384
    try:
385
        return MemberLeavePolicy.objects.get(policy=str_policy)
386
    except:
387
        return None
388
    
389
_auto_accept_join = False
390
def get_auto_accept_join_policy():
391
    global _auto_accept_join
392
    if _auto_accept_join is not False:
393
        return _auto_accept_join
394
    _auto_accept = get_join_policy('auto_accept')
395
    return _auto_accept
396

    
397
_closed_join = False
398
def get_closed_join_policy():
399
    global _closed_join
400
    if _closed_join is not False:
401
        return _closed_join
402
    _closed_join = get_join_policy('closed')
403
    return _closed_join
404

    
405
_auto_accept_leave = False
406
def get_auto_accept_leave_policy():
407
    global _auto_accept_leave
408
    if _auto_accept_leave is not False:
409
        return _auto_accept_leave
410
    _auto_accept_leave = get_leave_policy('auto_accept')
411
    return _auto_accept_leave
412

    
413
_closed_leave = False
414
def get_closed_leave_policy():
415
    global _closed_leave
416
    if _closed_leave is not False:
417
        return _closed_leave
418
    _closed_leave = get_leave_policy('closed')
419
    return _closed_leave
420

    
421
def get_project_by_application_id(project_application_id):
422
    try:
423
        return Project.objects.get(application__id=project_application_id)
424
    except Project.DoesNotExist:
425
        raise IOError(
426
            _(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) % project_application_id)
427

    
428
def get_user_by_id(user_id):
429
    try:
430
        return AstakosUser.objects.get(id=user_id)
431
    except AstakosUser.DoesNotExist:
432
        raise IOError(_(astakos_messages.UNKNOWN_USER_ID) % user_id)
433

    
434
def create_membership(project, user):
435
    if isinstance(project, int):
436
        project = get_project_by_application_id(project)
437
    if isinstance(user, int):
438
        user = get_user_by_id(user)
439
    m = ProjectMembership(
440
        project=project,
441
        person=user,
442
        request_date=datetime.now())
443
    try:
444
        m.save()
445
    except IntegrityError, e:
446
        raise IOError(_(astakos_messages.MEMBERSHIP_REQUEST_EXISTS))
447
    else:
448
        return m
449

    
450
def get_membership(project, user):
451
    if isinstance(project, int):
452
        project = get_project_by_application_id(project)
453
    if isinstance(user, int):
454
        user = get_user_by_id(user)
455
    try:
456
        return ProjectMembership.objects.select_related().get(
457
            project=project,
458
            person=user)
459
    except ProjectMembership.DoesNotExist:
460
        raise IOError(_(astakos_messages.NOT_MEMBERSHIP_REQUEST))
461

    
462
def accept_membership(project, user, request_user=None):
463
    """
464
        Raises:
465
            django.core.exceptions.PermissionDenied
466
            IOError
467
    """
468
    membership = get_membership(project, user)
469
    if request_user and \
470
        (not membership.project.application.owner == request_user and \
471
            not request_user.is_superuser):
472
        raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
473
    if not membership.project.is_alive:
474
        raise PermissionDenied(
475
            _(astakos_messages.NOT_ALIVE_PROJECT) % membership.project.__dict__)
476
    if len(membership.project.approved_members) + 1 > \
477
        membership.project.application.limit_on_members_number:
478
        raise PermissionDenied(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
479

    
480
    membership.accept()
481
    trigger_sync()
482

    
483
    try:
484
        notification = build_notification(
485
            settings.SERVER_EMAIL,
486
            [membership.person.email],
487
            _(PROJECT_MEMBERSHIP_CHANGE_SUBJECT) % membership.project.__dict__,
488
            template='im/projects/project_membership_change_notification.txt',
489
            dictionary={'object':membership.project.application, 'action':'accepted'})
490
        notification.send()
491
    except NotificationError, e:
492
        logger.error(e.message)
493
    return membership
494

    
495
def reject_membership(project, user, request_user=None):
496
    """
497
        Raises:
498
            django.core.exceptions.PermissionDenied
499
            IOError
500
    """
501
    membership = get_membership(project, user)
502
    if request_user and \
503
        (not membership.project.application.owner == request_user and \
504
            not request_user.is_superuser):
505
        raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
506
    if not membership.project.is_alive:
507
        raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % membership.project.__dict__)
508

    
509
    membership.reject()
510

    
511
    try:
512
        notification = build_notification(
513
            settings.SERVER_EMAIL,
514
            [membership.person.email],
515
            _(PROJECT_MEMBERSHIP_CHANGE_SUBJECT) % membership.project.__dict__,
516
            template='im/projects/project_membership_change_notification.txt',
517
            dictionary={'object':membership.project.application, 'action':'rejected'})
518
        notification.send()
519
    except NotificationError, e:
520
        logger.error(e.message)
521
    return membership
522

    
523
def remove_membership(project, user, request_user=None):
524
    """
525
        Raises:
526
            django.core.exceptions.PermissionDenied
527
            IOError
528
    """
529
    membership = get_membership(project, user)
530
    if request_user and \
531
        (not membership.project.application.owner == request_user and \
532
            not request_user.is_superuser):
533
        raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
534
    if not membership.project.is_alive:
535
        raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % membership.project.__dict__)
536

    
537
    membership.remove()
538
    trigger_sync()
539

    
540
    try:
541
        notification = build_notification(
542
            settings.SERVER_EMAIL,
543
            [membership.person.email],
544
            _(PROJECT_MEMBERSHIP_CHANGE_SUBJECT) % membership.project.__dict__,
545
            template='im/projects/project_membership_change_notification.txt',
546
            dictionary={'object':membership.project.application, 'action':'removed'})
547
        notification.send()
548
    except NotificationError, e:
549
        logger.error(e.message)
550
    return membership
551

    
552
def enroll_member(project, user, request_user=None):
553
    membership = create_membership(project, user)
554
    accept_membership(project, user, request_user)
555
    
556
def leave_project(project_application_id, user_id):
557
    """
558
        Raises:
559
            django.core.exceptions.PermissionDenied
560
            IOError
561
    """
562
    project = get_project_by_application_id(project_application_id)
563
    leave_policy = project.application.member_join_policy
564
    if leave_policy == get_closed_leave():
565
        raise PermissionDenied(_(astakos_messages.MEMBER_LEAVE_POLICY_CLOSED))
566

    
567
    membership = get_membership(project_application_id, user_id)
568
    if leave_policy == get_auto_accept_leave():
569
        membership.remove()
570
        trigger_sync()
571
    else:
572
        membership.leave_request_date = datetime.now()
573
        membership.save()
574
    return membership
575

    
576
def join_project(project_application_id, user_id):
577
    """
578
        Raises:
579
            django.core.exceptions.PermissionDenied
580
            IOError
581
    """
582
    project = get_project_by_application_id(project_application_id)
583
    join_policy = project.application.member_join_policy
584
    if join_policy == get_closed_join():
585
        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
586

    
587
    membership = create_membership(project_application_id, user_id)
588

    
589
    if join_policy == get_auto_accept_join():
590
        membership.accept()
591
        trigger_sync()
592
    return membership
593
    
594
def submit_application(
595
    application, resource_policies, applicant, comments, precursor_application=None):
596

    
597
    application.submit(
598
        resource_policies, applicant, comments, precursor_application)
599
    
600
    try:
601
        notification = build_notification(
602
            settings.SERVER_EMAIL,
603
            [i[1] for i in settings.ADMINS],
604
            _(PROJECT_CREATION_SUBJECT) % application.__dict__,
605
            template='im/projects/project_creation_notification.txt',
606
            dictionary={'object':application})
607
        notification.send()
608
    except NotificationError, e:
609
        logger.error(e)
610
    return application
611

    
612
def approve_application(application):
613
    application.approve()
614
    trigger_sync()
615
    
616
    try:
617
        notification = build_notification(
618
            settings.SERVER_EMAIL,
619
            [application.owner.email],
620
            _(PROJECT_APPROVED_SUBJECT) % application.__dict__,
621
            template='im/projects/project_approval_notification.txt',
622
            dictionary={'object':application})
623
        notification.send()
624
    except NotificationError, e:
625
        logger.error(e.message)