Fix invite error handling.
[pithos] / pithos / im / 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
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 pithos.im.models import User, Invitation
54 from pithos.im.util import isoformat
55
56
57 def render_response(template, tab=None, status=200, **kwargs):
58     if tab is None:
59         tab = template.partition('_')[0]
60     kwargs.setdefault('tab', tab)
61     html = render_to_string(template, kwargs)
62     return HttpResponse(html, status=status)
63
64
65 def requires_login(func):
66     @wraps(func)
67     def wrapper(request, *args):
68         if not settings.BYPASS_ADMIN_AUTH:
69             if not request.user:
70                 next = urlencode({'next': request.build_absolute_uri()})
71                 login_uri = reverse(index) + '?' + next
72                 return HttpResponseRedirect(login_uri)
73         return func(request, *args)
74     return wrapper
75
76
77 def requires_admin(func):
78     @wraps(func)
79     def wrapper(request, *args):
80         if not settings.BYPASS_ADMIN_AUTH:
81             if not request.user:
82                 next = urlencode({'next': request.build_absolute_uri()})
83                 login_uri = reverse(index) + '?' + next
84                 return HttpResponseRedirect(login_uri)
85             if not request.user.is_admin:
86                 return HttpResponse('Forbidden', status=403)
87         return func(request, *args)
88     return wrapper
89
90
91 def index(request):
92     return render_response('index.html', next=request.GET.get('next', ''))
93
94
95 @requires_admin
96 def admin(request):
97     stats = {}
98     stats['users'] = User.objects.count()
99     
100     invitations = Invitation.objects.all()
101     stats['invitations'] = invitations.count()
102     stats['invitations_accepted'] = invitations.filter(is_accepted=True).count()
103     
104     return render_response('admin.html', tab='home', stats=stats)
105
106
107 @requires_admin
108 def users_list(request):
109     users = User.objects.order_by('id')
110     
111     filter = request.GET.get('filter', '')
112     if filter:
113         if filter.startswith('-'):
114             users = users.exclude(uniq__icontains=filter[1:])
115         else:
116             users = users.filter(uniq__icontains=filter)
117     
118     try:
119         page = int(request.GET.get('page', 1))
120     except ValueError:
121         page = 1
122     offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
123     limit = offset + settings.ADMIN_PAGE_LIMIT
124     
125     npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
126     prev = page - 1 if page > 1 else None
127     next = page + 1 if page < npages else None
128     return render_response('users_list.html',
129                             users=users[offset:limit],
130                             filter=filter,
131                             pages=range(1, npages + 1),
132                             page=page,
133                             prev=prev,
134                             next=next)
135
136
137 @requires_admin
138 def users_create(request):
139     if request.method == 'GET':
140         return render_response('users_create.html')
141     if request.method == 'POST':
142         user = User()
143         user.uniq = request.POST.get('uniq')
144         user.realname = request.POST.get('realname')
145         user.is_admin = True if request.POST.get('admin') else False
146         user.affiliation = request.POST.get('affiliation')
147         user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3)  # In GiB
148         user.renew_token()
149         user.save()
150         return redirect(users_info, user.id)
151
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(inv):
201     url = settings.INVITATION_LOGIN_TARGET % inv.code
202     subject = _('Invitation to Pithos')
203     message = render_to_string('invitation.txt', {
204                 'invitation': inv,
205                 'url': url})
206     sender = settings.DEFAULT_FROM_EMAIL
207     send_mail(subject, message, sender, [inv.uniq])
208     logging.info('Sent invitation %s', inv)
209
210
211 @requires_login
212 def invite(request):
213     status = None
214     message = None
215     inviter = request.user
216
217     if request.method == 'POST':
218         uniq = request.POST.get('uniq')
219         realname = request.POST.get('realname')
220         
221         if inviter.invitations > 0:
222             code = generate_invitation_code()
223             invitation, created = Invitation.objects.get_or_create(
224                 inviter=inviter,
225                 uniq=uniq,
226                 defaults={'code': code, 'realname': realname})
227             
228             try:
229                 send_invitation(invitation)
230                 if created:
231                     inviter.invitations = max(0, inviter.invitations - 1)
232                     inviter.save()
233                 status = 'success'
234                 message = _('Invitation sent to %s' % uniq)
235             except (SMTPException, socket.error) as e:
236                 status = 'error'
237                 message = getattr(e, 'strerror', '')
238         else:
239             status = 'error'
240             message = _('No invitations left')
241
242     if request.GET.get('format') == 'json':
243         sent = [{'email': inv.uniq,
244                  'realname': inv.realname,
245                  'is_accepted': inv.is_accepted}
246                     for inv in inviter.invitations_sent.all()]
247         rep = {'invitations': inviter.invitations, 'sent': sent}
248         return HttpResponse(json.dumps(rep))
249     
250     html = render_to_string('invitations.html', {
251             'user': inviter,
252             'status': status,
253             'message': message})
254     return HttpResponse(html)