root / pithos / im / views.py @ a7bdef13
History | View | Annotate | Download (13 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 json |
35 |
import logging |
36 |
import socket |
37 |
|
38 |
from datetime import datetime |
39 |
from functools import wraps |
40 |
from math import ceil |
41 |
from random import randint |
42 |
from smtplib import SMTPException |
43 |
|
44 |
from django.conf import settings |
45 |
from django.core.mail import send_mail |
46 |
from django.http import HttpResponse, HttpResponseRedirect |
47 |
from django.shortcuts import redirect |
48 |
from django.template.loader import render_to_string |
49 |
from django.utils.http import urlencode |
50 |
from django.utils.translation import ugettext as _ |
51 |
from django.core.urlresolvers import reverse |
52 |
|
53 |
from urllib import quote |
54 |
|
55 |
from pithos.im.models import User, Invitation |
56 |
from pithos.im.util import isoformat |
57 |
|
58 |
|
59 |
def render_response(template, tab=None, status=200, **kwargs): |
60 |
if tab is None: |
61 |
tab = template.partition('_')[0] |
62 |
kwargs.setdefault('tab', tab)
|
63 |
html = render_to_string(template, kwargs) |
64 |
return HttpResponse(html, status=status)
|
65 |
|
66 |
|
67 |
def requires_login(func): |
68 |
@wraps(func)
|
69 |
def wrapper(request, *args): |
70 |
if not settings.BYPASS_ADMIN_AUTH: |
71 |
if not request.user: |
72 |
next = urlencode({'next': request.build_absolute_uri()})
|
73 |
login_uri = reverse(index) + '?' + next |
74 |
return HttpResponseRedirect(login_uri)
|
75 |
return func(request, *args)
|
76 |
return wrapper
|
77 |
|
78 |
|
79 |
def requires_admin(func): |
80 |
@wraps(func)
|
81 |
def wrapper(request, *args): |
82 |
if not settings.BYPASS_ADMIN_AUTH: |
83 |
if not request.user: |
84 |
next = urlencode({'next': request.build_absolute_uri()})
|
85 |
login_uri = reverse(index) + '?' + next |
86 |
return HttpResponseRedirect(login_uri)
|
87 |
if not request.user.is_admin: |
88 |
return HttpResponse('Forbidden', status=403) |
89 |
return func(request, *args)
|
90 |
return wrapper
|
91 |
|
92 |
|
93 |
def index(request): |
94 |
return render_response('index.html', next=request.GET.get('next', '')) |
95 |
|
96 |
|
97 |
@requires_admin
|
98 |
def admin(request): |
99 |
stats = {} |
100 |
stats['users'] = User.objects.count()
|
101 |
|
102 |
invitations = Invitation.objects.all() |
103 |
stats['invitations'] = invitations.count()
|
104 |
stats['invitations_accepted'] = invitations.filter(is_accepted=True).count() |
105 |
|
106 |
return render_response('admin.html', tab='home', stats=stats) |
107 |
|
108 |
|
109 |
@requires_admin
|
110 |
def users_list(request): |
111 |
users = User.objects.order_by('id')
|
112 |
|
113 |
filter = request.GET.get('filter', '') |
114 |
if filter: |
115 |
if filter.startswith('-'): |
116 |
users = users.exclude(uniq__icontains=filter[1:]) |
117 |
else:
|
118 |
users = users.filter(uniq__icontains=filter)
|
119 |
|
120 |
try:
|
121 |
page = int(request.GET.get('page', 1)) |
122 |
except ValueError: |
123 |
page = 1
|
124 |
offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT |
125 |
limit = offset + settings.ADMIN_PAGE_LIMIT |
126 |
|
127 |
npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT)) |
128 |
prev = page - 1 if page > 1 else None |
129 |
next = page + 1 if page < npages else None |
130 |
return render_response('users_list.html', |
131 |
users=users[offset:limit], |
132 |
filter=filter,
|
133 |
pages=range(1, npages + 1), |
134 |
page=page, |
135 |
prev=prev, |
136 |
next=next)
|
137 |
|
138 |
@requires_admin
|
139 |
def users_create(request): |
140 |
if request.method == 'GET': |
141 |
return render_response('users_create.html') |
142 |
if request.method == 'POST': |
143 |
user = User() |
144 |
user.uniq = request.POST.get('uniq')
|
145 |
user.realname = request.POST.get('realname')
|
146 |
user.is_admin = True if request.POST.get('admin') else False |
147 |
user.affiliation = request.POST.get('affiliation')
|
148 |
user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3) # In GiB |
149 |
user.renew_token() |
150 |
user.save() |
151 |
return redirect(users_info, user.id)
|
152 |
|
153 |
@requires_admin
|
154 |
def users_info(request, user_id): |
155 |
user = User.objects.get(id=user_id) |
156 |
states = [x[0] for x in User.ACCOUNT_STATE] |
157 |
return render_response('users_info.html', |
158 |
user=user, |
159 |
states=states) |
160 |
|
161 |
|
162 |
@requires_admin
|
163 |
def users_modify(request, user_id): |
164 |
user = User.objects.get(id=user_id) |
165 |
user.uniq = request.POST.get('uniq')
|
166 |
user.realname = request.POST.get('realname')
|
167 |
user.is_admin = True if request.POST.get('admin') else False |
168 |
user.affiliation = request.POST.get('affiliation')
|
169 |
user.state = request.POST.get('state')
|
170 |
user.invitations = int(request.POST.get('invitations') or 0) |
171 |
user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3) # In GiB |
172 |
user.auth_token = request.POST.get('auth_token')
|
173 |
try:
|
174 |
auth_token_expires = request.POST.get('auth_token_expires')
|
175 |
d = datetime.strptime(auth_token_expires, '%Y-%m-%dT%H:%MZ')
|
176 |
user.auth_token_expires = d |
177 |
except ValueError: |
178 |
pass
|
179 |
user.save() |
180 |
return redirect(users_info, user.id)
|
181 |
|
182 |
|
183 |
@requires_admin
|
184 |
def users_delete(request, user_id): |
185 |
user = User.objects.get(id=user_id) |
186 |
user.delete() |
187 |
return redirect(users_list)
|
188 |
|
189 |
|
190 |
def generate_invitation_code(): |
191 |
while True: |
192 |
code = randint(1, 2L**63 - 1) |
193 |
try:
|
194 |
Invitation.objects.get(code=code) |
195 |
# An invitation with this code already exists, try again
|
196 |
except Invitation.DoesNotExist:
|
197 |
return code
|
198 |
|
199 |
|
200 |
def send_invitation(baseurl, inv): |
201 |
url = settings.INVITATION_LOGIN_TARGET % (baseurl, inv.code, quote(baseurl)) |
202 |
subject = _('Invitation to Pithos')
|
203 |
message = render_to_string('invitation.txt', {
|
204 |
'invitation': inv,
|
205 |
'url': url,
|
206 |
'baseurl': baseurl,
|
207 |
'service': settings.SERVICE_NAME,
|
208 |
'support': settings.DEFAULT_CONTACT_EMAIL})
|
209 |
sender = settings.DEFAULT_FROM_EMAIL |
210 |
send_mail(subject, message, sender, [inv.uniq]) |
211 |
logging.info('Sent invitation %s', inv)
|
212 |
|
213 |
|
214 |
@requires_login
|
215 |
def invite(request): |
216 |
status = None
|
217 |
message = None
|
218 |
inviter = request.user |
219 |
|
220 |
if request.method == 'POST': |
221 |
uniq = request.POST.get('uniq')
|
222 |
realname = request.POST.get('realname')
|
223 |
|
224 |
if inviter.invitations > 0: |
225 |
code = generate_invitation_code() |
226 |
invitation, created = Invitation.objects.get_or_create( |
227 |
inviter=inviter, |
228 |
uniq=uniq, |
229 |
defaults={'code': code, 'realname': realname}) |
230 |
|
231 |
try:
|
232 |
send_invitation(request.get_host(), invitation) |
233 |
if created:
|
234 |
inviter.invitations = max(0, inviter.invitations - 1) |
235 |
inviter.save() |
236 |
status = 'success'
|
237 |
message = _('Invitation sent to %s' % uniq)
|
238 |
except (SMTPException, socket.error) as e: |
239 |
status = 'error'
|
240 |
message = getattr(e, 'strerror', '') |
241 |
else:
|
242 |
status = 'error'
|
243 |
message = _('No invitations left')
|
244 |
|
245 |
if request.GET.get('format') == 'json': |
246 |
sent = [{'email': inv.uniq,
|
247 |
'realname': inv.realname,
|
248 |
'is_accepted': inv.is_accepted}
|
249 |
for inv in inviter.invitations_sent.all()] |
250 |
rep = {'invitations': inviter.invitations, 'sent': sent} |
251 |
return HttpResponse(json.dumps(rep))
|
252 |
|
253 |
html = render_to_string('invitations.html', {
|
254 |
'user': inviter,
|
255 |
'status': status,
|
256 |
'message': message})
|
257 |
return HttpResponse(html)
|
258 |
|
259 |
def send_verification(baseurl, user): |
260 |
next = quote('http://%s' % baseurl)
|
261 |
url = settings.ACTIVATION_LOGIN_TARGET % (baseurl, |
262 |
quote(user.auth_token), |
263 |
next)
|
264 |
message = render_to_string('activation.txt', {
|
265 |
'user': user,
|
266 |
'url': url,
|
267 |
'baseurl': baseurl,
|
268 |
'service': settings.SERVICE_NAME,
|
269 |
'support': settings.DEFAULT_CONTACT_EMAIL})
|
270 |
sender = settings.DEFAULT_FROM_EMAIL |
271 |
send_mail('Pithos account activation', message, sender, [user.email])
|
272 |
logging.info('Sent activation %s', user)
|
273 |
|
274 |
def local_create(request): |
275 |
if request.method == 'GET': |
276 |
return render_response('local_create.html') |
277 |
elif request.method == 'POST': |
278 |
username = request.POST.get('uniq')
|
279 |
realname = request.POST.get('realname')
|
280 |
email = request.POST.get('email')
|
281 |
password = request.POST.get('password')
|
282 |
status = 'success'
|
283 |
cookie_value = None
|
284 |
if not username: |
285 |
status = 'error'
|
286 |
message = 'No username provided'
|
287 |
elif not password: |
288 |
status = 'error'
|
289 |
message = 'No password provided'
|
290 |
elif not email: |
291 |
status = 'error'
|
292 |
message = 'No email provided'
|
293 |
|
294 |
if status == 'success': |
295 |
username = '%s@local' % username
|
296 |
try:
|
297 |
user = User.objects.get(uniq=username) |
298 |
status = 'error'
|
299 |
message = 'Username is not available'
|
300 |
except User.DoesNotExist:
|
301 |
user = User() |
302 |
user.uniq = username |
303 |
user.realname = realname |
304 |
user.email = request.POST.get('email')
|
305 |
user.password = request.POST.get('password')
|
306 |
user.is_admin = False
|
307 |
user.quota = 0
|
308 |
user.state = 'UNVERIFIED'
|
309 |
user.level = 1
|
310 |
user.renew_token() |
311 |
try:
|
312 |
send_verification(request.get_host(), user) |
313 |
message = _('Verification sent to %s' % user.email)
|
314 |
user.save() |
315 |
except (SMTPException, socket.error) as e: |
316 |
status = 'error'
|
317 |
name = 'strerror'
|
318 |
message = getattr(e, name) if hasattr(e, name) else e |
319 |
|
320 |
html = render_to_string('local_create.html', {
|
321 |
'status': status,
|
322 |
'message': message})
|
323 |
response = HttpResponse(html) |
324 |
return response
|
325 |
|
326 |
def send_password(baseurl, user): |
327 |
next = quote('http://%s' % baseurl)
|
328 |
url = settings.PASSWORD_RESET_TARGET % (baseurl, |
329 |
quote(user.uniq), |
330 |
next)
|
331 |
message = render_to_string('password.txt', {
|
332 |
'user': user,
|
333 |
'url': url,
|
334 |
'baseurl': baseurl,
|
335 |
'service': settings.SERVICE_NAME,
|
336 |
'support': settings.DEFAULT_CONTACT_EMAIL})
|
337 |
sender = settings.DEFAULT_FROM_EMAIL |
338 |
send_mail('Pithos password recovering', message, sender, [user.email])
|
339 |
logging.info('Sent password %s', user)
|
340 |
|
341 |
def reclaim_password(request): |
342 |
if request.method == 'GET': |
343 |
return render_response('reclaim.html') |
344 |
elif request.method == 'POST': |
345 |
username = request.POST.get('uniq')
|
346 |
username = '%s@local' % username
|
347 |
try:
|
348 |
user = User.objects.get(uniq=username) |
349 |
try:
|
350 |
send_password(request.get_host(), user) |
351 |
status = 'success'
|
352 |
message = _('Password reset sent to %s' % user.email)
|
353 |
user.status = 'UNVERIFIED'
|
354 |
user.save() |
355 |
except (SMTPException, socket.error) as e: |
356 |
status = 'error'
|
357 |
name = 'strerror'
|
358 |
message = getattr(e, name) if hasattr(e, name) else e |
359 |
except User.DoesNotExist:
|
360 |
status = 'error'
|
361 |
message = 'Username does not exist'
|
362 |
|
363 |
html = render_to_string('reclaim.html', {
|
364 |
'status': status,
|
365 |
'message': message})
|
366 |
return HttpResponse(html)
|