fix admin users_modify view: upon success render user_info
[astakos] / astakos / im / admin / views.py
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 import sys
39
40 from datetime import datetime
41 from functools import wraps
42 from math import ceil
43 from random import randint
44 from smtplib import SMTPException
45 from hashlib import new as newhasher
46 from urllib import quote
47
48 from django.conf import settings
49 from django.core.mail import send_mail
50 from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
51 from django.shortcuts import redirect
52 from django.template.loader import render_to_string
53 from django.utils.http import urlencode
54 from django.utils.translation import ugettext as _
55 from django.core.urlresolvers import reverse
56 from django.contrib import messages
57 from django.db import transaction
58 from django.contrib.auth.models import AnonymousUser
59 from django.contrib.sites.models import Site
60
61 from astakos.im.models import AstakosUser, Invitation
62 from astakos.im.util import isoformat, get_or_create_user, get_context
63 from astakos.im.forms import *
64 from astakos.im.backends import get_backend
65 from astakos.im.views import render_response, index
66 from astakos.im.admin.forms import AdminProfileForm
67
68 def requires_admin(func):
69     """
70     Decorator checkes whether the request.user is a superuser and if not
71     redirects to login page.
72     """
73     @wraps(func)
74     def wrapper(request, *args):
75         if not settings.BYPASS_ADMIN_AUTH:
76             if request.user.is_anonymous():
77                 next = urlencode({'next': request.build_absolute_uri()})
78                 login_uri = reverse(index) + '?' + next
79                 return HttpResponseRedirect(login_uri)
80             if not request.user.is_superuser:
81                 return HttpResponse('Forbidden', status=403)
82         return func(request, *args)
83     return wrapper
84
85 @requires_admin
86 def admin(request, template_name='admin.html', extra_context={}):
87     """
88     Renders the admin page
89     
90     If the ``request.user`` is not a superuser redirects to login page.
91     
92    **Arguments**
93     
94     ``template_name``
95         A custom template to use. This is optional; if not specified,
96         this will default to ``admin.html``.
97     
98     ``extra_context``
99         An dictionary of variables to add to the template context.
100     
101    **Template:**
102     
103     admin.html or ``template_name`` keyword argument.
104     
105    **Template Context:**
106     
107     The template context is extended by:
108     
109     * tab: the name of the active tab
110     * stats: dictionary containing the number of all and prending users
111     """
112     stats = {}
113     stats['users'] = AstakosUser.objects.count()
114     stats['pending'] = AstakosUser.objects.filter(is_active = False).count()
115     
116     invitations = Invitation.objects.all()
117     stats['invitations'] = invitations.count()
118     stats['invitations_consumed'] = invitations.filter(is_consumed=True).count()
119     
120     kwargs = {'tab': 'home', 'stats': stats}
121     context = get_context(request, extra_context,**kwargs)
122     return render_response(template_name, context_instance = context)
123
124 @requires_admin
125 def users_list(request, template_name='users_list.html', extra_context={}):
126     """
127     Displays the list of all users.
128     
129     If the ``request.user`` is not a superuser redirects to login page.
130     
131    **Arguments**
132     
133     ``template_name``
134         A custom template to use. This is optional; if not specified,
135         this will default to ``users_list.html``.
136     
137     ``extra_context``
138         An dictionary of variables to add to the template context.
139     
140    **Template:**
141     
142     users_list.html or ``template_name`` keyword argument.
143     
144    **Template Context:**
145     
146     The template context is extended by:
147     
148     * users: list of users fitting in current page
149     * filter: search key
150     * pages: the number of pages
151     * prev: the previous page
152     * next: the current page
153     
154    **Settings:**
155     
156     * ADMIN_PAGE_LIMIT: Show these many users per page in admin interface
157     """
158     users = AstakosUser.objects.order_by('id')
159     
160     filter = request.GET.get('filter', '')
161     if filter:
162         if filter.startswith('-'):
163             users = users.exclude(username__icontains=filter[1:])
164         else:
165             users = users.filter(username__icontains=filter)
166     
167     try:
168         page = int(request.GET.get('page', 1))
169     except ValueError:
170         page = 1
171     offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
172     limit = offset + settings.ADMIN_PAGE_LIMIT
173     
174     npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
175     prev = page - 1 if page > 1 else None
176     next = page + 1 if page < npages else None
177     
178     kwargs = {'users':users[offset:limit],
179               'filter':filter,
180               'pages':range(1, npages + 1),
181               'prev':prev,
182               'next':next}
183     context = get_context(request, extra_context,**kwargs)
184     return render_response(template_name, context_instance = context)
185
186 @requires_admin
187 def users_info(request, user_id, template_name='users_info.html', extra_context={}):
188     """
189     Displays the specific user profile.
190     
191     If the ``request.user`` is not a superuser redirects to login page.
192     
193    **Arguments**
194     
195     ``template_name``
196         A custom template to use. This is optional; if not specified,
197         this will default to ``users_info.html``.
198     
199     ``extra_context``
200         An dictionary of variables to add to the template context.
201     
202    **Template:**
203     
204     users_info.html or ``template_name`` keyword argument.
205     
206    **Template Context:**
207     
208     The template context is extended by:
209     
210     * user: the user instance identified by ``user_id`` keyword argument
211     """
212     if not extra_context:
213         extra_context = {}
214     user = AstakosUser.objects.get(id=user_id)
215     return render_response(template_name,
216                            form = AdminProfileForm(instance=user),
217                            context_instance = get_context(request, extra_context))
218
219 @requires_admin
220 def users_modify(request, user_id, template_name='users_info.html', extra_context={}):
221     """
222     Update the specific user information. Upon success redirects to ``user_info`` view.
223     
224     If the ``request.user`` is not a superuser redirects to login page.
225     """
226     user = AstakosUser.objects.get(id = user_id)
227     form = AdminProfileForm(request.POST, instance=user)
228     if form.is_valid():
229         form.save()
230         return users_info(request, user.id, template_name, extra_context)
231     return render_response(template_name,
232                            form = form,
233                            context_instance = get_context(request, extra_context))
234
235 @requires_admin
236 def users_delete(request, user_id):
237     """
238     Deletes the specified user
239     
240     If the ``request.user`` is not a superuser redirects to login page.
241     """
242     user = AstakosUser.objects.get(id=user_id)
243     user.delete()
244     return redirect(users_list)
245
246 @requires_admin
247 def pending_users(request, template_name='pending_users.html', extra_context={}):
248     """
249     Displays the list of the pending users.
250     
251     If the ``request.user`` is not a superuser redirects to login page.
252     
253    **Arguments**
254     
255     ``template_name``
256         A custom template to use. This is optional; if not specified,
257         this will default to ``users_list.html``.
258     
259     ``extra_context``
260         An dictionary of variables to add to the template context.
261     
262    **Template:**
263     
264     pending_users.html or ``template_name`` keyword argument.
265     
266    **Template Context:**
267     
268     The template context is extended by:
269     
270     * users: list of pending users fitting in current page
271     * filter: search key
272     * pages: the number of pages
273     * prev: the previous page
274     * next: the current page
275     
276    **Settings:**
277     
278     * ADMIN_PAGE_LIMIT: Show these many users per page in admin interface
279     """
280     users = AstakosUser.objects.order_by('id')
281     
282     users = users.filter(is_active = False)
283     
284     filter = request.GET.get('filter', '')
285     if filter:
286         if filter.startswith('-'):
287             users = users.exclude(username__icontains=filter[1:])
288         else:
289             users = users.filter(username__icontains=filter)
290     
291     try:
292         page = int(request.GET.get('page', 1))
293     except ValueError:
294         page = 1
295     offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
296     limit = offset + settings.ADMIN_PAGE_LIMIT
297     
298     npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
299     prev = page - 1 if page > 1 else None
300     next = page + 1 if page < npages else None
301     kwargs = {'users':users[offset:limit],
302               'filter':filter,
303               'pages':range(1, npages + 1),
304               'page':page,
305               'prev':prev,
306               'next':next}
307     return render_response(template_name,
308                             context_instance = get_context(request, extra_context,**kwargs))
309
310 def _send_greeting(request, user, template_name):
311     subject = _('Welcome to %s' %settings.SERVICE_NAME)
312     site = Site.objects.get_current()
313     baseurl = request.build_absolute_uri('/').rstrip('/')
314     message = render_to_string(template_name, {
315                 'user': user,
316                 'url': site.domain,
317                 'baseurl': baseurl,
318                 'site_name': site.name,
319                 'support': settings.DEFAULT_CONTACT_EMAIL})
320     sender = settings.DEFAULT_FROM_EMAIL
321     send_mail(subject, message, sender, [user.email])
322     logging.info('Sent greeting %s', user)
323
324 @requires_admin
325 @transaction.commit_manually
326 def users_activate(request, user_id, template_name='pending_users.html', extra_context={}, email_template_name='welcome_email.txt'):
327     """
328     Activates the specific user and sends an email. Upon success renders the
329     ``template_name`` keyword argument if exists else renders ``pending_users.html``.
330     
331     If the ``request.user`` is not a superuser redirects to login page.
332     
333    **Arguments**
334     
335     ``template_name``
336         A custom template to use. This is optional; if not specified,
337         this will default to ``users_list.html``.
338     
339     ``extra_context``
340         An dictionary of variables to add to the template context.
341     
342    **Templates:**
343     
344     pending_users.html or ``template_name`` keyword argument.
345     welcome_email.txt or ``email_template_name`` keyword argument.
346     
347    **Template Context:**
348     
349     The template context is extended by:
350     
351     * users: list of pending users fitting in current page
352     * filter: search key
353     * pages: the number of pages
354     * prev: the previous page
355     * next: the current page
356     """
357     user = AstakosUser.objects.get(id=user_id)
358     user.is_active = True
359     user.save()
360     status = messages.SUCCESS
361     try:
362         _send_greeting(request, user, email_template_name)
363         message = _('Greeting sent to %s' % user.email)
364         transaction.commit()
365     except (SMTPException, socket.error) as e:
366         status = messages.ERROR
367         name = 'strerror'
368         message = getattr(e, name) if hasattr(e, name) else e
369         transaction.rollback()
370     messages.add_message(request, status, message)
371     
372     users = AstakosUser.objects.order_by('id')
373     users = users.filter(is_active = False)
374     
375     try:
376         page = int(request.POST.get('page', 1))
377     except ValueError:
378         page = 1
379     offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
380     limit = offset + settings.ADMIN_PAGE_LIMIT
381     
382     npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
383     prev = page - 1 if page > 1 else None
384     next = page + 1 if page < npages else None
385     kwargs = {'users':users[offset:limit],
386               'filter':'',
387               'pages':range(1, npages + 1),
388               'page':page,
389               'prev':prev,
390               'next':next}
391     return render_response(template_name,
392                            context_instance = get_context(request, extra_context,**kwargs))
393
394 @requires_admin
395 def invitations_list(request, template_name='invitations_list.html', extra_context={}):
396     """
397     Displays a list with the Invitations.
398     
399     If the ``request.user`` is not a superuser redirects to login page.
400     
401    **Arguments**
402     
403     ``template_name``
404         A custom template to use. This is optional; if not specified,
405         this will default to ``invitations_list.html``.
406     
407     ``extra_context``
408         An dictionary of variables to add to the template context.
409     
410    **Templates:**
411     
412     invitations_list.html or ``template_name`` keyword argument.
413     
414    **Template Context:**
415     
416     The template context is extended by:
417     
418     * invitations: list of invitations fitting in current page
419     * filter: search key
420     * pages: the number of pages
421     * prev: the previous page
422     * next: the current page
423     """
424     invitations = Invitation.objects.order_by('id')
425     
426     filter = request.GET.get('filter', '')
427     if filter:
428         if filter.startswith('-'):
429             invitations = invitations.exclude(username__icontains=filter[1:])
430         else:
431             invitations = invitations.filter(username__icontains=filter)
432     
433     try:
434         page = int(request.GET.get('page', 1))
435     except ValueError:
436         page = 1
437     offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
438     limit = offset + settings.ADMIN_PAGE_LIMIT
439     
440     npages = int(ceil(1.0 * invitations.count() / settings.ADMIN_PAGE_LIMIT))
441     prev = page - 1 if page > 1 else None
442     next = page + 1 if page < npages else None
443     kwargs = {'invitations':invitations[offset:limit],
444               'filter':filter,
445               'pages':range(1, npages + 1),
446               'page':page,
447               'prev':prev,
448               'next':next}
449     return render_response(template_name,
450                            context_instance = get_context(request, extra_context,**kwargs))
451
452 @requires_admin
453 def invitations_export(request):
454     """
455     Exports the invitation list in csv file.
456     """
457     # Create the HttpResponse object with the appropriate CSV header.
458     response = HttpResponse(mimetype='text/csv')
459     response['Content-Disposition'] = 'attachment; filename=invitations.csv'
460
461     writer = csv.writer(response)
462     writer.writerow(['ID',
463                      'Username',
464                      'Real Name',
465                      'Code',
466                      'Inviter username',
467                      'Inviter Real Name',
468                      'Is_accepted',
469                      'Created',
470                      'Accepted',])
471     invitations = Invitation.objects.order_by('id')
472     for inv in invitations:
473         
474         writer.writerow([inv.id,
475                          inv.username.encode("utf-8"),
476                          inv.realname.encode("utf-8"),
477                          inv.code,
478                          inv.inviter.username.encode("utf-8"),
479                          inv.inviter.realname.encode("utf-8"),
480                          inv.is_accepted,
481                          inv.created,
482                          inv.accepted])
483
484     return response
485
486
487 @requires_admin
488 def users_export(request):
489     """
490     Exports the user list in csv file.
491     """
492     # Create the HttpResponse object with the appropriate CSV header.
493     response = HttpResponse(mimetype='text/csv')
494     response['Content-Disposition'] = 'attachment; filename=users.csv'
495
496     writer = csv.writer(response)
497     writer.writerow(['ID',
498                      'Username',
499                      'Real Name',
500                      'Admin',
501                      'Affiliation',
502                      'Is active?',
503                      'Quota (GiB)',
504                      'Updated',])
505     users = AstakosUser.objects.order_by('id')
506     for u in users:
507         writer.writerow([u.id,
508                          u.username.encode("utf-8"),
509                          u.realname.encode("utf-8"),
510                          u.is_superuser,
511                          u.affiliation.encode("utf-8"),
512                          u.is_active,
513                          u.quota,
514                          u.updated])
515
516     return response
517
518 @requires_admin
519 def users_create(request, template_name='users_create.html', extra_context={}):
520     """
521     Creates a user. Upon success redirect to ``users_info`` view.
522     
523    **Arguments**
524     
525     ``template_name``
526         A custom template to use. This is optional; if not specified,
527         this will default to ``users_create.html``.
528     
529     ``extra_context``
530         An dictionary of variables to add to the template context.
531     
532    **Templates:**
533     
534     users_create.html or ``template_name`` keyword argument.
535     """
536     if request.method == 'GET':
537         return render_response(template_name,
538                                context_instance=get_context(request, extra_context))
539     if request.method == 'POST':
540         user = AstakosUser()
541         user.username = request.POST.get('username')
542         user.email = request.POST.get('email')
543         user.first_name = request.POST.get('first_name')
544         user.last_name = request.POST.get('last_name')
545         user.is_superuser = True if request.POST.get('admin') else False
546         user.affiliation = request.POST.get('affiliation')
547         user.quota = int(request.POST.get('quota') or 0) * (1024**3)  # In GiB
548         user.renew_token()
549         user.provider = 'local'
550         user.save()
551         return redirect(users_info, user.id)