Statistics
| Branch: | Tag: | Revision:

root / invitations / invitations.py @ 9f8ad4c5

History | View | Annotate | Download (9.4 kB)

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

    
31

    
32
from datetime import timedelta
33
import base64
34
import time
35
import urllib
36

    
37
from django.conf import settings
38
from django.core.exceptions import ValidationError
39
from django.db import transaction
40
from django.http import HttpResponse, HttpResponseRedirect
41
from django.template.context import RequestContext
42
from django.template.loader import render_to_string
43
from django.core.validators import validate_email
44
from django.views.decorators.csrf import csrf_protect
45
from synnefo.logic.email_send import send_async
46

    
47
from synnefo.api.common import method_not_allowed
48
from synnefo.db.models import Invitations, SynnefoUser
49
from synnefo.logic import users
50

    
51
from Crypto.Cipher import AES
52

    
53

    
54
def process_form(request):
55
    errors = []
56
    valid_inv = filter(lambda x: x.startswith("name_"), request.POST.keys())
57

    
58
    for inv in valid_inv:
59
        (name, inv_id) = inv.split('_')
60

    
61
        email = ""
62
        name = ""
63
        try:
64
            email = request.POST['email_' + inv_id]
65
            name = request.POST[inv]
66

    
67
            validate_name(name)
68
            validate_email(email)
69

    
70
            inv = add_invitation(request.user, name, email)
71
            send_invitation(inv)
72

    
73
        except Exception as e:
74
            try :
75
                errors += ["Invitation to %s <%s> not sent. Reason: %s" %
76
                           (name, email, e.messages[0])]
77
            except:
78
                errors += ["Invitation to %s <%s> not sent. Reason: %s" %
79
                           (name, email, e.message)]
80

    
81
    respose = None
82
    if errors:
83
        data = render_to_string('invitations.html',
84
                                {'invitations': invitations_for_user(request),
85
                                 'errors': errors},
86
                                context_instance=RequestContext(request))
87
        response =  HttpResponse(data)
88
    else:
89
        response = HttpResponseRedirect("/invitations/")
90

    
91
    return response
92

    
93

    
94
def validate_name(name):
95
    if name is None or name.strip() == '' :
96
        raise ValidationError("Name is empty")
97

    
98
    if name.find(' ') is -1:
99
        raise ValidationError("Name must contain at least one space")
100

    
101
    return True
102

    
103

    
104
def invitations_for_user(request):
105
    invitations = []
106

    
107
    for inv in Invitations.objects.filter(source = request.user):
108
        invitation = {}
109

    
110
        invitation['sourcename'] = inv.source.realname
111
        invitation['source'] = inv.source.uniq
112
        invitation['targetname'] = inv.target.realname
113
        invitation['target'] = inv.target.uniq
114
        invitation['accepted'] = inv.accepted
115
        invitation['sent'] = inv.created
116

    
117
        invitations.append(invitation)
118

    
119
    return invitations
120

    
121

    
122
@csrf_protect
123
def inv_demux(request):
124

    
125
    if request.method == 'GET':
126
        data = render_to_string('invitations.html',
127
                                {'invitations': invitations_for_user(request)},
128
                                context_instance=RequestContext(request))
129
        return  HttpResponse(data)
130
    elif request.method == 'POST':
131
        return process_form(request)
132
    else:
133
        method_not_allowed(request)
134

    
135

    
136
def login(request):
137

    
138
    if not request.method == 'GET':
139
        method_not_allowed(request)
140

    
141
    key = request.GET['key']
142

    
143
    if key is None:
144
        return render_login_error("10", "Required key is missing")
145

    
146
    PADDING = '{'
147

    
148
    try :
149
        DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).rstrip(PADDING)
150
        cipher = AES.new(settings.INVITATION_ENCR_KEY)
151
        decoded = DecodeAES(cipher, key)
152
    except Exception:
153
        return render_login_error("20", "Required key is invalid")
154

    
155
    users = SynnefoUser.objects.filter(auth_token = decoded)
156

    
157
    if users.count() is 0:
158
        return render_login_error("20", "Required key is invalid")
159

    
160
    user = users[0]
161
    invitations = Invitations.objects.filter(target = user)
162

    
163
    if invitations.count() is 0:
164
        return render_login_error("30", "Non-existent invitation")
165

    
166
    inv = invitations[0]
167

    
168
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
169
    valid_until = inv.created + valid
170

    
171
    if (time.time() -
172
        time.mktime(inv.created.timetuple()) -
173
        settings.INVITATION_VALID_DAYS * 3600) > 0:
174
        return render_login_error("40",
175
                                  "Invitation expired (was valid until %s)"%
176
                                  valid_until.strftime('%A, %d %B %Y'))
177
    #if inv.accepted == False:
178
    #    return render_login_error("60", "Invitation already accepted")
179

    
180
    inv.accepted = True
181
    inv.save()
182

    
183
    data = dict()
184
    data['user'] = user.realname
185
    data['url'] = settings.APP_INSTALL_URL
186

    
187
    welcome = render_to_string('welcome.html', {'data': data})
188

    
189
    response = HttpResponse(welcome)
190

    
191
    response.set_cookie('X-Auth-Token', value=user.auth_token,
192
                        expires = valid_until.strftime('%a, %d-%b-%Y %H:%M:%S %Z'),
193
                        path='/')
194
    response['X-Auth-Token'] = user.auth_token
195
    return response
196

    
197

    
198
def render_login_error(code, text):
199
    error = dict()
200
    error['id'] = code
201
    error['text'] = text
202

    
203
    data = render_to_string('error.html', {'error': error})
204

    
205
    response = HttpResponse(data)
206
    return response
207

    
208

    
209
def send_invitation(invitation):
210
    email = {}
211
    email['invitee'] = invitation.target.realname
212
    email['inviter'] = invitation.source.realname
213

    
214
    valid = timedelta(days = settings.INVITATION_VALID_DAYS)
215
    valid_until = invitation.created + valid
216
    email['valid_until'] = valid_until.strftime('%A, %d %B %Y')
217

    
218
    PADDING = '{'
219
    pad = lambda s: s + (32 - len(s) % 32) * PADDING
220
    EncodeAES = lambda c, s: base64.b64encode(c.encrypt(pad(s)))
221

    
222
    cipher = AES.new(settings.INVITATION_ENCR_KEY)
223
    encoded = EncodeAES(cipher, invitation.target.auth_token)
224

    
225
    url_safe = urllib.urlencode({'key': encoded})
226

    
227
    email['url'] = settings.APP_INSTALL_URL + "/invitations/login?" + url_safe
228

    
229
    data = render_to_string('invitation.txt', {'email': email})
230

    
231
    print data
232

    
233
    send_async(
234
        frm = "%s <%s>"%(invitation.source.realname,invitation.source.uniq),
235
        to = "%s <%s>"%(invitation.target.realname,invitation.target.uniq),
236
        subject = u'Πρόσκληση για την υπηρεσία Ωκεανός',
237
        body = data
238
    )
239

    
240

    
241
def get_invitee_level(source):
242
    return get_user_inv_level(source) + 1
243

    
244

    
245
def get_user_inv_level(u):
246
    inv = Invitations.objects.filter(target = u)
247

    
248
    if inv is None:
249
        raise Exception("User without invitation", u)
250

    
251
    return inv[0].level
252

    
253

    
254
@transaction.commit_on_success
255
def add_invitation(source, name, email):
256
    """
257
        Adds an invitation, if the source user has not gone over his/her
258
        invitation limit or the target user has not been invited already
259
    """
260
    num_inv = Invitations.objects.filter(source = source).count()
261

    
262
    if num_inv >= source.max_invitations:
263
        raise TooManyInvitations("User invitation limit (%d) exhausted" %
264
                                 source.max_invitations)
265

    
266
    target = SynnefoUser.objects.filter(uniq = email)
267

    
268
    if target.count() is not 0:
269
        raise AlreadyInvited("User with email %s already invited" % (email))
270

    
271
    users.register_user(name, email)
272

    
273
    target = SynnefoUser.objects.filter(uniq = email)
274

    
275
    r = list(target[:1])
276
    if not r:
277
        raise Exception("Invited user cannot be added")
278

    
279
    u = target[0]
280
    invitee_level = get_invitee_level(source)
281

    
282
    u.max_invitations = settings.INVITATIONS_PER_LEVEL[invitee_level]
283
    u.save()
284

    
285
    inv = Invitations()
286
    inv.source = source
287
    inv.target = u
288
    inv.level = invitee_level
289
    inv.save()
290
    return inv
291

    
292

    
293
@transaction.commit_on_success
294
def invitation_accepted(invitation):
295
    """
296
        Mark an invitation as accepted
297
    """
298
    invitation.accepted = True
299
    invitation.save()
300

    
301

    
302
class TooManyInvitations(Exception):
303
    messages = []
304

    
305
    def __init__(self, msg):
306
        self.messages.append(msg)
307

    
308

    
309
class AlreadyInvited(Exception):
310
    messages = []
311

    
312
    def __init__(self, msg):
313
        self.messages.append(msg)