Statistics
| Branch: | Tag: | Revision:

root / pithos / im / views.py @ 0778f7e1

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