Commit 0905ccd2 authored by Sofia Papagiannaki's avatar Sofia Papagiannaki
Browse files

restructure app

- extend django user model
- pass template_name and extra_context in views
- incorporate context processors
- use different backends for invitations and simple workflow

Refs: #1823
parent 66396eca
......@@ -3,16 +3,16 @@
{% block tabs %}
<ul class="tabs">
<li{% ifequal tab "home" %} class="active"{% endifequal %}>
<a href="{% url astakos.im.views.admin %}">Home</a>
<a href="{% url astakos.im.admin.views.admin %}">Home</a>
</li>
<li{% ifequal tab "users" %} class="active"{% endifequal %}>
<a href="{% url astakos.im.views.users_list %}">Users</a>
<a href="{% url astakos.im.admin.views.users_list %}">Users</a>
</li>
<li{% ifequal tab "pending" %} class="active"{% endifequal %}>
<a href="{% url astakos.im.views.pending_users %}">Pending Users</a>
<a href="{% url astakos.im.admin.views.pending_users %}">Pending Users</a>
</li>
<li{% ifequal tab "invitations" %} class="active"{% endifequal %}>
<a href="{% url astakos.im.views.invitations_list %}">Invitations</a>
<a href="{% url astakos.im.admin.views.invitations_list %}">Invitations</a>
</li>
</ul>
{% endblock %}
......
......@@ -18,10 +18,10 @@
<thead>
<tr>
<th>ID</th>
<th>Uniq</th>
<th>Username</th>
<th>Real Name</th>
<th>Code</th>
<th>Inviter Uniq</th>
<th>Inviter username</th>
<th>Inviter Real Name</th>
<th>Is consumed</th>
<th>Created</th>
......@@ -32,10 +32,10 @@
{% for inv in invitations %}
<tr>
<td>{{ inv.id }}</td>
<td>{{ inv.uniq }}</td>
<td>{{ inv.username }}</td>
<td>{{ inv.realname }}</td>
<td>{{ inv.code }}</td>
<td>{{ inv.inviter.uniq }}</td>
<td>{{ inv.inviter.username }}</td>
<td>{{ inv.inviter.realname }}</td>
<td>{{ inv.is_consumed }}</td>
<td>{{ inv.created }}</td>
......@@ -67,7 +67,7 @@
</div>
{% endif %}
<a class="btn success" href="{% url astakos.im.views.invitations_export %}">Export</a>
<a class="btn success" href="{% url astakos.im.admin.views.invitations_export %}">Export</a>
<br /><br />
{% endblock body %}
......@@ -18,7 +18,7 @@
<thead>
<tr>
<th>ID</th>
<th>Uniq</th>
<th>Username</th>
<th>Real Name</th>
<th>Affiliation</th>
<th>Email</th>
......@@ -30,13 +30,13 @@
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.uniq }}</td>
<td>{{ user.username }}</td>
<td>{{ user.realname }}</td>
<td>{{ user.affiliation }}</td>
<td>{{ user.email }}</td>
<td>{{ user.inviter.realname }}</td>
<td>
<form action="{% url astakos.im.views.users_activate user.id %}" method="post">
<form action="{% url astakos.im.admin.views.users_activate user.id %}" method="post">
<input type="hidden" name="page" value="{{ page }}">
<button type="submit" class="btn primary">Activate</button>
</form>
......
......@@ -2,18 +2,32 @@
{% block body %}
<form action="{% url astakos.im.views.users_create %}" method="post">
<form action="{% url astakos.im.admin.views.users_create %}" method="post">
<div class="clearfix">
<label for="user-uniq">Uniq</label>
<label for="user-username">Username</label>
<div class="input">
<input class="span4" id="user-uniq" name="uniq" type="text" />
<input class="span4" id="user-username" name="username" type="text" />
</div>
</div>
<div class="clearfix">
<label for="user-email">Email</label>
<div class="input">
<input class="span4" id="user-email" name="email" type="text" />
</div>
</div>
<div class="clearfix">
<label for="user-realname">Real Name</label>
<label for="user-first-name">First Name</label>
<div class="input">
<input class="span4" id="user-first-name" name="first_name" type="text" />
</div>
</div>
<div class="clearfix">
<label for="user-last-name">Last Name</label>
<div class="input">
<input class="span4" id="user-realname" name="realname" type="text" />
<input class="span4" id="user-last-name" name="last_name" type="text" />
</div>
</div>
......
......@@ -4,7 +4,7 @@
{% block body %}
<form action="{% url astakos.im.views.users_modify user.id %}" method="post">
<form action="{% url astakos.im.admin.views.users_modify user.id %}" method="post">
<div class="clearfix">
<label for="user-id">ID</label>
<div class="input">
......@@ -13,16 +13,23 @@
</div>
<div class="clearfix">
<label for="user-uniq">Uniq</label>
<label for="user-username">Username</label>
<div class="input">
<input class="span4" id="user-uniq" name="uniq" value="{{ user.uniq }}" type="text" />
<input class="span4" id="user-username" name="username" value="{{ user.username }}" type="text" />
</div>
</div>
<div class="clearfix">
<label for="user-realname">Real Name</label>
<label for="user-first-name">First Name</label>
<div class="input">
<input class="span4" id="user-realname" name="realname" value="{{ user.realname }}" type="text" />
<input class="span4" id="user-first-name" name="first_name" value="{{ user.first_name }}" type="text" />
</div>
</div>
<div class="clearfix">
<label for="user-last-name">Last Name</label>
<div class="input">
<input class="span4" id="user-last-name" name="last_name" value="{{ user.last_name }}" type="text" />
</div>
</div>
......@@ -32,7 +39,7 @@
<ul class="inputs-list">
<li>
<label>
<input type="checkbox" id="user-admin" name="admin"{% if user.is_admin %} checked{% endif %}>
<input type="checkbox" id="user-admin" name="admin"{% if user.is_superuser %} checked{% endif %}>
</label>
</li>
</ul>
......@@ -47,13 +54,15 @@
</div>
<div class="clearfix">
<label for="user-state">State</label>
<label for="user-is-active">Is active?</label>
<div class="input">
<select class="medium" id="user-state" name="state">
{% for state in states %}
<option{% ifequal state user.state %} selected{% endifequal %}>{{ state }}</option>
{% endfor %}
</select>
<ul class="inputs-list">
<li>
<label>
<input type="checkbox" id="user-is-active" name="is_active"{% if user.is_active %} checked{% endif %}>
</label>
</li>
</ul>
</div>
</div>
......@@ -96,9 +105,9 @@
</div>
<div class="clearfix">
<label for="user-created">Created</label>
<label for="user-date-joined">Created</label>
<div class="input">
<span class="uneditable-input" id="user-created">{{ user.created }}</span>
<span class="uneditable-input" id="user-date-joined">{{ user.date_joined }}</span>
</div>
</div>
......@@ -113,13 +122,13 @@
<button type="submit" class="btn primary">Save Changes</button>
<button type="reset" class="btn">Reset</button>
&nbsp;&nbsp;
<a class="btn danger needs-confirm" href="{% url astakos.im.views.users_delete user.id %}">Delete User</a>
<a class="btn danger needs-confirm" href="{% url astakos.im.admin.views.users_delete user.id %}">Delete User</a>
</div>
<div class="alert-message block-message error">
<p><strong>WARNING:</strong> Are you sure you want to delete this user?</p>
<div class="alert-actions">
<a class="btn danger" href="{% url astakos.im.views.users_delete user.id %}">Delete</a>
<a class="btn danger" href="{% url astakos.im.admin.views.users_delete user.id %}">Delete</a>
<a class="btn alert-close">Cancel</a>
</div>
</div>
......
......@@ -18,11 +18,11 @@
<thead>
<tr>
<th>ID</th>
<th>Uniq</th>
<th>Username</th>
<th>Real Name</th>
<th>Admin</th>
<th>Affiliation</th>
<th>State</th>
<th>Is active?</th>
<th>Quota</th>
<th>Updated</th>
</tr>
......@@ -30,12 +30,12 @@
<tbody>
{% for user in users %}
<tr>
<td><a href="{% url astakos.im.views.users_info user.id %}">{{ user.id }}</a></td>
<td><a href="{% url astakos.im.views.users_info user.id %}">{{ user.uniq }}</a></td>
<td><a href="{% url astakos.im.admin.views.users_info user.id %}">{{ user.id }}</a></td>
<td><a href="{% url astakos.im.admin.views.users_info user.id %}">{{ user.username }}</a></td>
<td>{{ user.realname }}</td>
<td>{{ user.is_admin }}</td>
<td>{{ user.is_superuser }}</td>
<td>{{ user.affiliation }}</td>
<td>{{ user.state }}</td>
<td>{{ user.is_active }}</td>
<td>{{ user.quota|GiB }} GiB</td>
<td>{{ user.updated }}</td>
</tr>
......@@ -65,8 +65,8 @@
</div>
{% endif %}
<a class="btn success" href="{% url astakos.im.views.users_create %}">Create a user</a>
<a class="btn success" href="{% url astakos.im.views.users_export %}">Export</a>
<a class="btn success" href="{% url astakos.im.admin.views.users_create %}">Create a user</a>
<a class="btn success" href="{% url astakos.im.admin.views.users_export %}">Export</a>
<br /><br />
{% endblock body %}
# Copyright 2011 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from django.conf.urls.defaults import patterns
urlpatterns = patterns('astakos.im.admin.views',
(r'^$', 'admin'),
(r'^users/?$', 'users_list'),
(r'^users/(\d+)/?$', 'users_info'),
(r'^users/create$', 'users_create'),
(r'^users/(\d+)/modify/?$', 'users_modify'),
(r'^users/(\d+)/delete/?$', 'users_delete'),
(r'^users/export/?$', 'users_export'),
(r'^users/pending/?$', 'pending_users'),
(r'^users/activate/(\d+)/?$', 'users_activate'),
(r'^invitations/?$', 'invitations_list'),
(r'^invitations/export/?$', 'invitations_export'),
)
\ No newline at end of file
# Copyright 2011 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import json
import logging
import socket
import csv
import sys
from datetime import datetime
from functools import wraps
from math import ceil
from random import randint
from smtplib import SMTPException
from hashlib import new as newhasher
from urllib import quote
from django.conf import settings
from django.core.mail import send_mail
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.utils.http import urlencode
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
#from astakos.im.openid_store import PithosOpenIDStore
from astakos.im.models import AstakosUser, Invitation
from astakos.im.util import isoformat, get_or_create_user, get_context
from astakos.im.forms import *
from astakos.im.backends import get_backend
from astakos.im.views import render_response, index
def requires_admin(func):
@wraps(func)
def wrapper(request, *args):
if not settings.BYPASS_ADMIN_AUTH:
if not request.user:
next = urlencode({'next': request.build_absolute_uri()})
login_uri = reverse(index) + '?' + next
return HttpResponseRedirect(login_uri)
if not request.user.is_superuser:
return HttpResponse('Forbidden', status=403)
return func(request, *args)
return wrapper
@requires_admin
def admin(request, template_name='admin.html', extra_context={}):
stats = {}
stats['users'] = AstakosUser.objects.count()
stats['pending'] = AstakosUser.objects.filter(is_active = False).count()
invitations = Invitation.objects.all()
stats['invitations'] = invitations.count()
stats['invitations_consumed'] = invitations.filter(is_consumed=True).count()
kwargs = {'tab': 'home', 'stats': stats}
context = get_context(request, extra_context, **kwargs)
return render_response(template_name, context_instance = context)
@requires_admin
def users_list(request, template_name='users_list.html', extra_context={}):
users = AstakosUser.objects.order_by('id')
filter = request.GET.get('filter', '')
if filter:
if filter.startswith('-'):
users = users.exclude(username__icontains=filter[1:])
else:
users = users.filter(username__icontains=filter)
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
limit = offset + settings.ADMIN_PAGE_LIMIT
npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
prev = page - 1 if page > 1 else None
next = page + 1 if page < npages else None
kwargs = {'users':users[offset:limit],
'filter':filter,
'pages':range(1, npages + 1),
'prev':prev,
'next':next}
context = get_context(request, extra_context, **kwargs)
return render_response(template_name, context_instance = context)
@requires_admin
def users_info(request, user_id, template_name='users_info.html', extra_context={}):
if not extra_context:
extra_context = {}
kwargs = {'user':AstakosUser.objects.get(id=user_id)}
context = get_context(request, extra_context, **kwargs)
return render_response(template_name, context_instance = context)
@requires_admin
def users_modify(request, user_id):
user = AstakosUser.objects.get(id=user_id)
user.username = request.POST.get('username')
user.first_name = request.POST.get('first_name')
user.last_name = request.POST.get('last_name')
user.is_superuser = True if request.POST.get('admin') else False
user.affiliation = request.POST.get('affiliation')
user.is_active = True if request.POST.get('is_active') else False
user.invitations = int(request.POST.get('invitations') or 0)
#user.quota = int(request.POST.get('quota') or 0) * (1024 ** 3) # In GiB
user.auth_token = request.POST.get('auth_token')
try:
auth_token_expires = request.POST.get('auth_token_expires')
d = datetime.strptime(auth_token_expires, '%Y-%m-%dT%H:%MZ')
user.auth_token_expires = d
except ValueError:
pass
user.save()
return redirect(users_info, user.id)
@requires_admin
def users_delete(request, user_id):
user = AstakosUser.objects.get(id=user_id)
user.delete()
return redirect(users_list)
@requires_admin
def pending_users(request, template_name='pending_users.html', extra_context={}):
users = AstakosUser.objects.order_by('id')
users = users.filter(is_active = False)
filter = request.GET.get('filter', '')
if filter:
if filter.startswith('-'):
users = users.exclude(username__icontains=filter[1:])
else:
users = users.filter(username__icontains=filter)
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
limit = offset + settings.ADMIN_PAGE_LIMIT
npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
prev = page - 1 if page > 1 else None
next = page + 1 if page < npages else None
kwargs = {'users':users[offset:limit],
'filter':filter,
'pages':range(1, npages + 1),
'page':page,
'prev':prev,
'next':next}
return render_response(template_name,
context_instance = get_context(request, extra_context, **kwargs))
def _send_greeting(baseurl, user):
url = reverse('astakos.im.views.index')
subject = _('Welcome to %s' %settings.SERVICE_NAME)
message = render_to_string('welcome.txt', {
'user': user,
'url': url,
'baseurl': baseurl,
'service': settings.SERVICE_NAME,
'support': settings.DEFAULT_CONTACT_EMAIL})
sender = settings.DEFAULT_FROM_EMAIL
send_mail(subject, message, sender, [user.email])
logging.info('Sent greeting %s', user)
@requires_admin
def users_activate(request, user_id, template_name='pending_users.html', extra_context={}):
user = AstakosUser.objects.get(id=user_id)
user.is_active = True
status = 'success'
try:
_send_greeting(request.build_absolute_uri('/').rstrip('/'), user)
message = _('Greeting sent to %s' % user.email)
user.save()
except (SMTPException, socket.error) as e:
status = 'error'
name = 'strerror'
message = getattr(e, name) if hasattr(e, name) else e
users = AstakosUser.objects.order_by('id')
users = users.filter(is_active = False)
try:
page = int(request.POST.get('page', 1))
except ValueError:
page = 1
offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
limit = offset + settings.ADMIN_PAGE_LIMIT
npages = int(ceil(1.0 * users.count() / settings.ADMIN_PAGE_LIMIT))
prev = page - 1 if page > 1 else None
next = page + 1 if page < npages else None
kwargs = {'users':users[offset:limit],
'filter':'',
'pages':range(1, npages + 1),
'page':page,
'prev':prev,
'next':next,
'message':message}
return render_response(template_name,
context_instance = get_context(request, extra_context, **kwargs))
@requires_admin
def invitations_list(request, template_name='invitations_list.html', extra_context={}):
invitations = Invitation.objects.order_by('id')
filter = request.GET.get('filter', '')
if filter:
if filter.startswith('-'):
invitations = invitations.exclude(username__icontains=filter[1:])
else:
invitations = invitations.filter(username__icontains=filter)
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
offset = max(0, page - 1) * settings.ADMIN_PAGE_LIMIT
limit = offset + settings.ADMIN_PAGE_LIMIT
npages = int(ceil(1.0 * invitations.count() / settings.ADMIN_PAGE_LIMIT))
prev = page - 1 if page > 1 else None
next = page + 1 if page < npages else None
kwargs = {'invitations':invitations[offset:limit],
'filter':filter,
'pages':range(1, npages + 1),
'page':page,
'prev':prev,
'next':next}
return render_response(template_name,
context_instance = get_context(request, extra_context, **kwargs))
@requires_admin
def invitations_export(request):
# Create the HttpResponse object with the appropriate CSV header.
response = HttpResponse(mimetype='text/csv')
response['Content-Disposition'] = 'attachment; filename=invitations.csv'
writer = csv.writer(response)
writer.writerow(['ID',
'Username',
'Real Name',
'Code',
'Inviter username',
'Inviter Real Name',
'Is_accepted',
'Created',
'Accepted',])
invitations = Invitation.objects.order_by('id')
for inv in invitations:
writer.writerow([inv.id,
inv.username.encode("utf-8"),
inv.realname.encode("utf-8"),
inv.code,
inv.inviter.username.encode("utf-8"),
inv.inviter.realname.encode("utf-8"),
inv.is_accepted,
inv.created,
inv.accepted])