root / pithos / im / views.py @ 618d7e05
History | View | Annotate | Download (17.1 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 |
import csv |
38 |
|
39 |
from datetime import datetime |
40 |
from functools import wraps |
41 |
from math import ceil |
42 |
from random import randint |
43 |
from smtplib import SMTPException |
44 |
|
45 |
from django.conf import settings |
46 |
from django.core.mail import send_mail |
47 |
from django.http import HttpResponse, HttpResponseRedirect |
48 |
from django.shortcuts import redirect |
49 |
from django.template.loader import render_to_string |
50 |
from django.utils.http import urlencode |
51 |
from django.utils.translation import ugettext as _ |
52 |
from django.core.urlresolvers import reverse |
53 |
|
54 |
from urllib import quote |
55 |
|
56 |
from pithos.im.models import User, Invitation |
57 |
from pithos.im.util import isoformat |
58 |
|
59 |
|
60 |
def render_response(template, tab=None, status=200, **kwargs): |
61 |
if tab is None: |
62 |
tab = template.partition('_')[0] |
63 |
kwargs.setdefault('tab', tab)
|
64 |
html = render_to_string(template, kwargs) |
65 |
return HttpResponse(html, status=status)
|
66 |
|
67 |
|
68 |
def requires_login(func): |
69 |
@wraps(func)
|
70 |
def wrapper(request, *args): |
71 |
if not settings.BYPASS_ADMIN_AUTH: |
72 |
if not request.user: |
73 |
next = urlencode({'next': request.build_absolute_uri()})
|
74 |
login_uri = reverse(index) + '?' + next |
75 |
return HttpResponseRedirect(login_uri)
|
76 |
return func(request, *args)
|
77 |
return wrapper
|
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 |
kwargs = {'standard_modules':settings.IM_STANDARD_MODULES,
|
95 |
'other_modules':settings.IM_OTHER_MODULES}
|
96 |
return render_response('index.html', |
97 |
next=request.GET.get('next', ''), |
98 |
**kwargs) |
99 |
|
100 |
|
101 |
@requires_admin
|
102 |
def admin(request): |
103 |
stats = {} |
104 |
stats['users'] = User.objects.count()
|
105 |
|
106 |
invitations = Invitation.objects.all() |
107 |
stats['invitations'] = invitations.count()
|
108 |
stats['invitations_accepted'] = invitations.filter(is_accepted=True).count() |
109 |
|
110 |
return render_response('admin.html', tab='home', stats=stats) |
111 |
|
112 |
|
113 |
@requires_admin
|
114 |
def users_list(request): |
115 |
users = User.objects.order_by('id')
|
116 |
|
117 |
filter = request.GET.get('filter', '') |
118 |
if filter: |
119 |
if filter.startswith('-'): |
120 |
users = users.exclude(uniq__icontains=filter[1:]) |
121 |
else:
|
122 |
users = users.filter(uniq__icontains=filter)
|
123 |
|
124 |
try:
|
125 |
page = int(request.GET.get('page', 1)) |
126 |
except ValueError: |
127 |
page = 1
|
128 |
offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT |
129 |
limit = offset + settings.ADMIN_PAGE_LIMIT |
130 |
|
131 |
npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT)) |
132 |
prev = page - 1 if page > 1 else None |
133 |
next = page + 1 if page < npages else None |
134 |
return render_response('users_list.html', |
135 |
users=users[offset:limit], |
136 |
filter=filter,
|
137 |
pages=range(1, npages + 1), |
138 |
page=page, |
139 |
prev=prev, |
140 |
next=next)
|
141 |
|
142 |
@requires_admin
|
143 |
def users_info(request, user_id): |
144 |
user = User.objects.get(id=user_id) |
145 |
states = [x[0] for x in User.ACCOUNT_STATE] |
146 |
return render_response('users_info.html', |
147 |
user=user, |
148 |
states=states) |
149 |
|
150 |
|
151 |
@requires_admin
|
152 |
def users_modify(request, user_id): |
153 |
user = User.objects.get(id=user_id) |
154 |
user.uniq = request.POST.get('uniq')
|
155 |
user.realname = request.POST.get('realname')
|
156 |
user.is_admin = True if request.POST.get('admin') else False |
157 |
user.affiliation = request.POST.get('affiliation')
|
158 |
user.state = request.POST.get('state')
|
159 |
user.invitations = int(request.POST.get('invitations') or 0) |
160 |
user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3) # In GiB |
161 |
user.auth_token = request.POST.get('auth_token')
|
162 |
try:
|
163 |
auth_token_expires = request.POST.get('auth_token_expires')
|
164 |
d = datetime.strptime(auth_token_expires, '%Y-%m-%dT%H:%MZ')
|
165 |
user.auth_token_expires = d |
166 |
except ValueError: |
167 |
pass
|
168 |
user.save() |
169 |
return redirect(users_info, user.id)
|
170 |
|
171 |
|
172 |
@requires_admin
|
173 |
def users_delete(request, user_id): |
174 |
user = User.objects.get(id=user_id) |
175 |
user.delete() |
176 |
return redirect(users_list)
|
177 |
|
178 |
|
179 |
def generate_invitation_code(): |
180 |
while True: |
181 |
code = randint(1, 2L**63 - 1) |
182 |
try:
|
183 |
Invitation.objects.get(code=code) |
184 |
# An invitation with this code already exists, try again
|
185 |
except Invitation.DoesNotExist:
|
186 |
return code
|
187 |
|
188 |
|
189 |
def send_invitation(baseurl, inv): |
190 |
url = settings.INVITATION_LOGIN_TARGET % (baseurl, inv.code, quote(baseurl)) |
191 |
subject = _('Invitation to Pithos')
|
192 |
message = render_to_string('invitation.txt', {
|
193 |
'invitation': inv,
|
194 |
'url': url,
|
195 |
'baseurl': baseurl,
|
196 |
'service': settings.SERVICE_NAME,
|
197 |
'support': settings.DEFAULT_CONTACT_EMAIL})
|
198 |
sender = settings.DEFAULT_FROM_EMAIL |
199 |
send_mail(subject, message, sender, [inv.uniq]) |
200 |
logging.info('Sent invitation %s', inv)
|
201 |
|
202 |
|
203 |
@requires_login
|
204 |
def invite(request): |
205 |
status = None
|
206 |
message = None
|
207 |
inviter = request.user |
208 |
|
209 |
if request.method == 'POST': |
210 |
uniq = request.POST.get('uniq')
|
211 |
realname = request.POST.get('realname')
|
212 |
|
213 |
if inviter.invitations > 0: |
214 |
code = generate_invitation_code() |
215 |
invitation, created = Invitation.objects.get_or_create( |
216 |
inviter=inviter, |
217 |
uniq=uniq, |
218 |
defaults={'code': code, 'realname': realname}) |
219 |
|
220 |
try:
|
221 |
send_invitation(request.build_absolute_uri('/').rstrip('/'), invitation) |
222 |
if created:
|
223 |
inviter.invitations = max(0, inviter.invitations - 1) |
224 |
inviter.save() |
225 |
status = 'success'
|
226 |
message = _('Invitation sent to %s' % uniq)
|
227 |
except (SMTPException, socket.error) as e: |
228 |
status = 'error'
|
229 |
message = getattr(e, 'strerror', '') |
230 |
else:
|
231 |
status = 'error'
|
232 |
message = _('No invitations left')
|
233 |
|
234 |
if request.GET.get('format') == 'json': |
235 |
sent = [{'email': inv.uniq,
|
236 |
'realname': inv.realname,
|
237 |
'is_accepted': inv.is_accepted}
|
238 |
for inv in inviter.invitations_sent.all()] |
239 |
rep = {'invitations': inviter.invitations, 'sent': sent} |
240 |
return HttpResponse(json.dumps(rep))
|
241 |
|
242 |
html = render_to_string('invitations.html', {
|
243 |
'user': inviter,
|
244 |
'status': status,
|
245 |
'message': message})
|
246 |
return HttpResponse(html)
|
247 |
|
248 |
def send_verification(baseurl, user): |
249 |
url = settings.ACTIVATION_LOGIN_TARGET % (baseurl, |
250 |
quote(user.auth_token), |
251 |
quote(baseurl)) |
252 |
message = render_to_string('activation.txt', {
|
253 |
'user': user,
|
254 |
'url': url,
|
255 |
'baseurl': baseurl,
|
256 |
'service': settings.SERVICE_NAME,
|
257 |
'support': settings.DEFAULT_CONTACT_EMAIL})
|
258 |
sender = settings.DEFAULT_FROM_EMAIL |
259 |
send_mail('Pithos account activation', message, sender, [user.email])
|
260 |
logging.info('Sent activation %s', user)
|
261 |
|
262 |
def local_create(request): |
263 |
if request.method == 'GET': |
264 |
return render_response('local_create.html') |
265 |
elif request.method == 'POST': |
266 |
username = request.POST.get('uniq')
|
267 |
realname = request.POST.get('realname')
|
268 |
email = request.POST.get('email')
|
269 |
password = request.POST.get('password')
|
270 |
status = 'success'
|
271 |
cookie_value = None
|
272 |
if not username: |
273 |
status = 'error'
|
274 |
message = 'No username provided'
|
275 |
elif not password: |
276 |
status = 'error'
|
277 |
message = 'No password provided'
|
278 |
elif not email: |
279 |
status = 'error'
|
280 |
message = 'No email provided'
|
281 |
|
282 |
if status == 'success': |
283 |
username = '%s@local' % username
|
284 |
try:
|
285 |
user = User.objects.get(uniq=username) |
286 |
status = 'error'
|
287 |
message = 'Username is not available'
|
288 |
except User.DoesNotExist:
|
289 |
user = User() |
290 |
user.uniq = username |
291 |
user.realname = realname |
292 |
user.email = request.POST.get('email')
|
293 |
user.password = request.POST.get('password')
|
294 |
user.is_admin = False
|
295 |
user.quota = 0
|
296 |
user.state = 'UNVERIFIED'
|
297 |
user.level = 1
|
298 |
user.renew_token() |
299 |
try:
|
300 |
send_verification(request.build_absolute_uri('/').rstrip('/'), user) |
301 |
message = _('Verification sent to %s' % user.email)
|
302 |
user.save() |
303 |
except (SMTPException, socket.error) as e: |
304 |
status = 'error'
|
305 |
name = 'strerror'
|
306 |
message = getattr(e, name) if hasattr(e, name) else e |
307 |
|
308 |
html = render_to_string('local_create.html', {
|
309 |
'status': status,
|
310 |
'message': message})
|
311 |
response = HttpResponse(html) |
312 |
return response
|
313 |
|
314 |
def send_password(baseurl, user): |
315 |
url = settings.PASSWORD_RESET_TARGET % (baseurl, |
316 |
quote(user.uniq), |
317 |
quote(baseurl)) |
318 |
message = render_to_string('password.txt', {
|
319 |
'user': user,
|
320 |
'url': url,
|
321 |
'baseurl': baseurl,
|
322 |
'service': settings.SERVICE_NAME,
|
323 |
'support': settings.DEFAULT_CONTACT_EMAIL})
|
324 |
sender = settings.DEFAULT_FROM_EMAIL |
325 |
send_mail('Pithos password recovering', message, sender, [user.email])
|
326 |
logging.info('Sent password %s', user)
|
327 |
|
328 |
def reclaim_password(request): |
329 |
if request.method == 'GET': |
330 |
return render_response('reclaim.html') |
331 |
elif request.method == 'POST': |
332 |
username = request.POST.get('uniq')
|
333 |
username = '%s@local' % username
|
334 |
try:
|
335 |
user = User.objects.get(uniq=username) |
336 |
try:
|
337 |
send_password(request.build_absolute_uri('/').rstrip('/'), user) |
338 |
status = 'success'
|
339 |
message = _('Password reset sent to %s' % user.email)
|
340 |
user.status = 'UNVERIFIED'
|
341 |
user.save() |
342 |
except (SMTPException, socket.error) as e: |
343 |
status = 'error'
|
344 |
name = 'strerror'
|
345 |
message = getattr(e, name) if hasattr(e, name) else e |
346 |
except User.DoesNotExist:
|
347 |
status = 'error'
|
348 |
message = 'Username does not exist'
|
349 |
|
350 |
html = render_to_string('reclaim.html', {
|
351 |
'status': status,
|
352 |
'message': message})
|
353 |
return HttpResponse(html)
|
354 |
|
355 |
@requires_admin
|
356 |
def invitations_list(request): |
357 |
invitations = Invitation.objects.order_by('id')
|
358 |
|
359 |
filter = request.GET.get('filter', '') |
360 |
if filter: |
361 |
if filter.startswith('-'): |
362 |
invitations = invitations.exclude(uniq__icontains=filter[1:]) |
363 |
else:
|
364 |
invitations = invitations.filter(uniq__icontains=filter)
|
365 |
|
366 |
try:
|
367 |
page = int(request.GET.get('page', 1)) |
368 |
except ValueError: |
369 |
page = 1
|
370 |
offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT |
371 |
limit = offset + settings.ADMIN_PAGE_LIMIT |
372 |
|
373 |
npages = int(ceil(1.0 * invitations.count() / settings.ADMIN_PAGE_LIMIT)) |
374 |
prev = page - 1 if page > 1 else None |
375 |
next = page + 1 if page < npages else None |
376 |
return render_response('invitations_list.html', |
377 |
invitations=invitations[offset:limit], |
378 |
filter=filter,
|
379 |
pages=range(1, npages + 1), |
380 |
page=page, |
381 |
prev=prev, |
382 |
next=next)
|
383 |
|
384 |
@requires_admin
|
385 |
def invitations_export(request): |
386 |
# Create the HttpResponse object with the appropriate CSV header.
|
387 |
response = HttpResponse(mimetype='text/csv')
|
388 |
response['Content-Disposition'] = 'attachment; filename=invitations.csv' |
389 |
|
390 |
writer = csv.writer(response) |
391 |
writer.writerow(['ID',
|
392 |
'Uniq',
|
393 |
'Real Name',
|
394 |
'Code',
|
395 |
'Inviter Uniq',
|
396 |
'Inviter Real Name',
|
397 |
'Is_accepted',
|
398 |
'Created',
|
399 |
'Accepted',])
|
400 |
invitations = Invitation.objects.order_by('id')
|
401 |
for inv in invitations: |
402 |
writer.writerow([inv.id, |
403 |
inv.uniq.encode("utf-8"),
|
404 |
inv.realname.encode("utf-8"),
|
405 |
inv.code, |
406 |
inv.inviter.uniq.encode("utf-8"),
|
407 |
inv.inviter.realname.encode("utf-8"),
|
408 |
inv.is_accepted, |
409 |
inv.created, |
410 |
inv.accepted]) |
411 |
|
412 |
return response
|
413 |
|
414 |
|
415 |
@requires_admin
|
416 |
def users_export(request): |
417 |
# Create the HttpResponse object with the appropriate CSV header.
|
418 |
response = HttpResponse(mimetype='text/csv')
|
419 |
response['Content-Disposition'] = 'attachment; filename=users.csv' |
420 |
|
421 |
writer = csv.writer(response) |
422 |
writer.writerow(['ID',
|
423 |
'Uniq',
|
424 |
'Real Name',
|
425 |
'Admin',
|
426 |
'Affiliation',
|
427 |
'State',
|
428 |
'Quota (GiB)',
|
429 |
'Updated',])
|
430 |
users = User.objects.order_by('id')
|
431 |
for u in users: |
432 |
writer.writerow([u.id, |
433 |
u.uniq.encode("utf-8"),
|
434 |
u.realname.encode("utf-8"),
|
435 |
u.is_admin, |
436 |
u.affiliation.encode("utf-8"),
|
437 |
u.state.encode("utf-8"),
|
438 |
u.quota, |
439 |
u.updated]) |
440 |
|
441 |
return response
|
442 |
|
443 |
@requires_admin
|
444 |
def users_create(request): |
445 |
if request.method == 'GET': |
446 |
return render_response('users_create.html') |
447 |
if request.method == 'POST': |
448 |
user = User() |
449 |
user.uniq = request.POST.get('uniq')
|
450 |
user.realname = request.POST.get('realname')
|
451 |
user.is_admin = True if request.POST.get('admin') else False |
452 |
user.affiliation = request.POST.get('affiliation')
|
453 |
user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3) # In GiB |
454 |
user.renew_token() |
455 |
user.save() |
456 |
return redirect(users_info, user.id)
|
457 |
|
458 |
@requires_login
|
459 |
def users_profile(request): |
460 |
next = request.GET.get('next')
|
461 |
user = User.objects.get(uniq=request.user) |
462 |
states = [x[0] for x in User.ACCOUNT_STATE] |
463 |
return render_response('users_profile.html', |
464 |
user=user, |
465 |
states=states, |
466 |
next=next)
|
467 |
|
468 |
@requires_login
|
469 |
def users_edit(request): |
470 |
user = User.objects.get(uniq=request.user) |
471 |
user.realname = request.POST.get('realname')
|
472 |
user.affiliation = request.POST.get('affiliation')
|
473 |
user.is_verified = True
|
474 |
user.save() |
475 |
next = request.POST.get('next')
|
476 |
if next: |
477 |
return redirect(next) |
478 |
|
479 |
status = 'success'
|
480 |
message = _('Profile has been updated')
|
481 |
html = render_to_string('users_profile.html', {
|
482 |
'user': user,
|
483 |
'status': status,
|
484 |
'message': message})
|
485 |
return HttpResponse(html)
|
486 |
|