--- /dev/null
+# 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 import forms
+from django.utils.translation import ugettext as _
+from django.contrib.auth.forms import UserCreationForm
+from django.conf import settings
+from hashlib import new as newhasher
+
+from astakos.im.models import AstakosUser
+from astakos.im.util import get_or_create_user
+
+class AdminProfileForm(forms.ModelForm):
+ """
+ Subclass of ``ModelForm`` for permiting user to edit his/her profile.
+ Most of the fields are readonly since the user is not allowed to change them.
+
+ The class defines a save method which sets ``is_verified`` to True so as the user
+ during the next login will not to be redirected to profile page.
+ """
+ class Meta:
+ model = AstakosUser
+
+ def __init__(self, *args, **kwargs):
+ super(AdminProfileForm, self).__init__(*args, **kwargs)
+ instance = getattr(self, 'instance', None)
+ ro_fields = ('username','date_joined', 'auth_token', 'last_login', 'email')
+ if instance and instance.id:
+ for field in ro_fields:
+ if isinstance(self.fields[field].widget, forms.CheckboxInput):
+ self.fields[field].widget.attrs['disabled'] = True
+ self.fields[field].widget.attrs['readonly'] = True
\ No newline at end of file
<td>{{ user.email }}</td>
<td>{{ user.inviter.realname }}</td>
<td>
- <form action="{% url astakos.im.admin.views.users_activate user.id %}" method="post">
+ <form action="{% url astakos.im.admin.views.users_activate user.id %}" method="post">{% csrf_token %}
<input type="hidden" name="page" value="{{ page }}">
<button type="submit" class="btn primary">Activate</button>
</form>
{% block body %}
-<form action="{% url astakos.im.admin.views.users_create %}" method="post">
+<form action="{% url astakos.im.admin.views.users_create %}" method="post">{% csrf_token %}
<div class="clearfix">
<label for="user-username">Username</label>
<div class="input">
{% block body %}
-<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">
- <span class="uneditable-input" id="user-id">{{ user.id }}</span>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-username">Username</label>
- <div class="input">
- <input class="span4" id="user-username" name="username" value="{{ user.username }}" type="text" />
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-first-name">First Name</label>
- <div class="input">
- <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>
-
- <div class="clearfix">
- <label for="user-admin">Admin</label>
- <div class="input">
- <ul class="inputs-list">
- <li>
- <label>
- <input type="checkbox" id="user-admin" name="admin"{% if user.is_superuser %} checked{% endif %}>
- </label>
- </li>
- </ul>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-affiliation">Affiliation</label>
- <div class="input">
- <input class="span4" id="user-affiliation" name="affiliation" value="{{ user.affiliation }}" type="text" />
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-is-active">Is active?</label>
- <div class="input">
- <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>
-
- <div class="clearfix">
- <label for="user-invitations">Invitations</label>
- <div class="input">
- <input class="span2" id="user-invitations" name="invitations" value="{{ user.invitations }}" type="text" />
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-quota">Quota</label>
- <div class="input">
- <div class="input-append">
- <input class="span2" id="user-quota" name="quota" value="{{ user.quota|GiB }}" type="text" />
- <span class="add-on">GiB</span>
- </div>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-token">Token</label>
- <div class="input">
- <input class="span4" id="user-token" name="auth_token" value="{{ user.auth_token }}" type="text" />
- </div>
- </div>
-
- <div class="clearfix">
- <label for="token-created">Token Created</label>
- <div class="input">
- <span class="uneditable-input" id="token-created">{{ user.auth_token_created }}</span>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="token-expires">Token Expires</label>
- <div class="input">
- <input type="datetime" class="span4" id="token-expires" name="auth_token_expires" value="{{ user.auth_token_expires|isoformat }}" />
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-date-joined">Created</label>
- <div class="input">
- <span class="uneditable-input" id="user-date-joined">{{ user.date_joined }}</span>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-updated">Updated</label>
- <div class="input">
- <span class="uneditable-input" id="user-updated">{{ user.updated }}</span>
- </div>
- </div>
+<form action="{% url astakos.im.admin.views.users_modify user.id %}" method="post">{% csrf_token %}
+ {{ form.as_p }}
<div class="actions">
<button type="submit" class="btn primary">Save Changes</button>
Αγαπητέ/η {{ user.realname }},
-Ο λογαρισμός σας για την υπηρεσία {{ service }} της ΕΔΕΤκατά την Alpha (πιλοτική)
+Ο λογαρισμός σας για την υπηρεσία {{ site_name }} της ΕΔΕΤκατά την Alpha (πιλοτική)
φάση λειτουργίας της έχει ενεργοποιηθεί.
Για να συνδεθείτε, χρησιμοποιήστε τον παρακάτω σύνδεσμο:
υπηρεσίας, δεν αποκλείεται να εμφανιστούν προβλήματα στο λογισμικό
διαχείρισης ή η υπηρεσία να μην είναι διαθέσιμη κατά διαστήματα. Για
αυτό το λόγο, σας παρακαλούμε να μη μεταφέρετε ακόμη σημαντικά κομμάτια
-της δουλειάς σας στην υπηρεσία {{ service }}. Επίσης, παρακαλούμε να έχετε
+της δουλειάς σας στην υπηρεσία {{ site_name }}. Επίσης, παρακαλούμε να έχετε
υπόψη σας ότι όλα τα δεδομένα, θα διαγραφούν κατά τη μετάβαση από την
έκδοση Alpha στην έκδοση Beta. Θα υπάρξει έγκαιρη ειδοποίησή σας πριν
από τη μετάβαση αυτή.
Για όποιες παρατηρήσεις ή προβλήματα στη λειτουργεία της υπηρεσίας μπορείτε να
απευθυνθείτε στο {{ support }}.
-Σας ευχαριστούμε πολύ για τη συμμετοχή στην Alpha λειτουργία του {{ service }}.
+Σας ευχαριστούμε πολύ για τη συμμετοχή στην Alpha λειτουργία του {{ site_name }}.
Με εκτίμηση,
Dear {{ user.realname }},
-Your account for GRNET's {{ service }} service for its Alpha test phase has been
+Your account for GRNET's {{ site_name }} service for its Alpha test phase has been
activated.
To login, please use the following link:
taken to ensure high quality, it is possible there may still be software
bugs, or periods of limited service availability. For this reason, we
kindly request you do not transfer important parts of your work to
-{{ service }}, yet. Also, please bear in mind that all data, will be deleted
+{{ site_name }}, yet. Also, please bear in mind that all data, will be deleted
when the service moves to Beta test. Before the transition, you will be
notified in time.
For any remarks or problems you can contact {{ support }}.
-Thank you for participating in the Alpha test of {{ service }}.
+Thank you for participating in the Alpha test of {{ site_name }}.
Greek Research and Technonogy Network - GRNET
from django.utils.http import urlencode
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
+from django.contrib import messages
+from django.db import transaction
+from django.contrib.auth.models import AnonymousUser
+from django.contrib.sites.models import get_current_site
-#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
+from astakos.im.admin.forms import AdminProfileForm
def requires_admin(func):
+ """
+ Decorator checkes whether the request.user is a superuser and if not
+ redirects to login page.
+ """
@wraps(func)
def wrapper(request, *args):
if not settings.BYPASS_ADMIN_AUTH:
- if not request.user:
+ if isinstance(request.user, AnonymousUser):
next = urlencode({'next': request.build_absolute_uri()})
login_uri = reverse(index) + '?' + next
return HttpResponseRedirect(login_uri)
@requires_admin
def admin(request, template_name='admin.html', extra_context={}):
+ """
+ Renders the admin page
+
+ If the ``request.user`` is not a superuser redirects to login page.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``admin.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Template:**
+
+ admin.html or ``template_name`` keyword argument.
+
+ **Template Context:**
+
+ The template context is extended by:
+
+ * tab: the name of the active tab
+ * stats: dictionary containing the number of all and prending users
+ """
stats = {}
stats['users'] = AstakosUser.objects.count()
stats['pending'] = AstakosUser.objects.filter(is_active = False).count()
stats['invitations_consumed'] = invitations.filter(is_consumed=True).count()
kwargs = {'tab': 'home', 'stats': stats}
- context = get_context(request, extra_context, **kwargs)
+ 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={}):
+ """
+ Displays the list of all users.
+
+ If the ``request.user`` is not a superuser redirects to login page.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``users_list.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Template:**
+
+ users_list.html or ``template_name`` keyword argument.
+
+ **Template Context:**
+
+ The template context is extended by:
+
+ * users: list of users fitting in current page
+ * filter: search key
+ * pages: the number of pages
+ * prev: the previous page
+ * next: the current page
+
+ **Settings:**
+
+ * ADMIN_PAGE_LIMIT: Show these many users per page in admin interface
+ """
users = AstakosUser.objects.order_by('id')
filter = request.GET.get('filter', '')
'pages':range(1, npages + 1),
'prev':prev,
'next':next}
- context = get_context(request, extra_context, **kwargs)
+ 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={}):
+ """
+ Displays the specific user profile.
+
+ If the ``request.user`` is not a superuser redirects to login page.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``users_info.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Template:**
+
+ users_info.html or ``template_name`` keyword argument.
+
+ **Template Context:**
+
+ The template context is extended by:
+
+ * user: the user instance identified by ``user_id`` keyword argument
+ """
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)
+ user = AstakosUser.objects.get(id=user_id)
+ return render_response(template_name,
+ form = AdminProfileForm(instance=user),
+ context_instance = get_context(request, extra_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)
+def users_modify(request, user_id, template_name='users_info.html', extra_context={}):
+ """
+ Update the specific user information. Upon success redirects to ``user_info`` view.
+
+ If the ``request.user`` is not a superuser redirects to login page.
+ """
+ form = AdminProfileForm(request.POST)
+ if form.is_valid():
+ form.save()
+ return redirect(users_info, user.id, template_name, extra_context)
+ return render_response(template_name,
+ form = form,
+ context_instance = get_context(request, extra_context))
@requires_admin
def users_delete(request, user_id):
+ """
+ Deletes the specified user
+
+ If the ``request.user`` is not a superuser redirects to login page.
+ """
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={}):
+ """
+ Displays the list of the pending users.
+
+ If the ``request.user`` is not a superuser redirects to login page.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``users_list.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Template:**
+
+ pending_users.html or ``template_name`` keyword argument.
+
+ **Template Context:**
+
+ The template context is extended by:
+
+ * users: list of pending users fitting in current page
+ * filter: search key
+ * pages: the number of pages
+ * prev: the previous page
+ * next: the current page
+
+ **Settings:**
+
+ * ADMIN_PAGE_LIMIT: Show these many users per page in admin interface
+ """
users = AstakosUser.objects.order_by('id')
users = users.filter(is_active = False)
'prev':prev,
'next':next}
return render_response(template_name,
- context_instance = get_context(request, extra_context, **kwargs))
+ context_instance = get_context(request, extra_context,**kwargs))
-def _send_greeting(baseurl, user):
+def _send_greeting(request, user, template_name):
url = reverse('astakos.im.views.index')
subject = _('Welcome to %s' %settings.SERVICE_NAME)
- message = render_to_string('welcome.txt', {
+ site = get_current_site(request)
+ baseurl = request.build_absolute_uri('/').rstrip('/')
+ message = render_to_string(template_name, {
'user': user,
'url': url,
'baseurl': baseurl,
- 'service': settings.SERVICE_NAME,
+ 'site_name': site.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={}):
+@transaction.commit_manually
+def users_activate(request, user_id, template_name='pending_users.html', extra_context={}, email_template_name='welcome_email.txt'):
+ """
+ Activates the specific user and sends an email. Upon success renders the
+ ``template_name`` keyword argument if exists else renders ``pending_users.html``.
+
+ If the ``request.user`` is not a superuser redirects to login page.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``users_list.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Templates:**
+
+ pending_users.html or ``template_name`` keyword argument.
+ welcome_email.txt or ``email_template_name`` keyword argument.
+
+ **Template Context:**
+
+ The template context is extended by:
+
+ * users: list of pending users fitting in current page
+ * filter: search key
+ * pages: the number of pages
+ * prev: the previous page
+ * next: the current page
+ """
user = AstakosUser.objects.get(id=user_id)
user.is_active = True
- status = 'success'
+ user.save()
+ status = messages.SUCCESS
try:
- _send_greeting(request.build_absolute_uri('/').rstrip('/'), user)
+ _send_greeting(request, user, email_template_name)
message = _('Greeting sent to %s' % user.email)
- user.save()
+ transaction.commit()
except (SMTPException, socket.error) as e:
- status = 'error'
+ status = messages.ERROR
name = 'strerror'
message = getattr(e, name) if hasattr(e, name) else e
+ transaction.rollback()
+ messages.add_message(request, status, message)
users = AstakosUser.objects.order_by('id')
users = users.filter(is_active = False)
'pages':range(1, npages + 1),
'page':page,
'prev':prev,
- 'next':next,
- 'message':message}
+ 'next':next}
return render_response(template_name,
- context_instance = get_context(request, extra_context, **kwargs))
+ context_instance = get_context(request, extra_context,**kwargs))
@requires_admin
def invitations_list(request, template_name='invitations_list.html', extra_context={}):
+ """
+ Displays a list with the Invitations.
+
+ If the ``request.user`` is not a superuser redirects to login page.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``invitations_list.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Templates:**
+
+ invitations_list.html or ``template_name`` keyword argument.
+
+ **Template Context:**
+
+ The template context is extended by:
+
+ * invitations: list of invitations fitting in current page
+ * filter: search key
+ * pages: the number of pages
+ * prev: the previous page
+ * next: the current page
+ """
invitations = Invitation.objects.order_by('id')
filter = request.GET.get('filter', '')
'prev':prev,
'next':next}
return render_response(template_name,
- context_instance = get_context(request, extra_context, **kwargs))
+ context_instance = get_context(request, extra_context,**kwargs))
@requires_admin
def invitations_export(request):
+ """
+ Exports the invitation list in csv file.
+ """
# Create the HttpResponse object with the appropriate CSV header.
response = HttpResponse(mimetype='text/csv')
response['Content-Disposition'] = 'attachment; filename=invitations.csv'
@requires_admin
def users_export(request):
+ """
+ Exports the user list in csv file.
+ """
# Create the HttpResponse object with the appropriate CSV header.
response = HttpResponse(mimetype='text/csv')
response['Content-Disposition'] = 'attachment; filename=users.csv'
@requires_admin
def users_create(request, template_name='users_create.html', extra_context={}):
+ """
+ Creates a user. Upon success redirect to ``users_info`` view.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``users_create.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Templates:**
+
+ users_create.html or ``template_name`` keyword argument.
+ """
if request.method == 'GET':
return render_response(template_name,
context_instance=get_context(request, extra_context))
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.quota = int(request.POST.get('quota') or 0) * (1024 ** 3) # In GiB
+ user.quota = int(request.POST.get('quota') or 0) * (1024**3) # In GiB
user.renew_token()
user.provider = 'local'
user.save()
response = HttpResponse()
response.status=204
- user_info = user.__dict__
- for k,v in user_info.items():
- if isinstance(v, datetime.datetime):
- user_info[k] = v.strftime('%a, %d-%b-%Y %H:%M:%S %Z')
- user_info.pop('_state')
+ user_info = {'uniq':user.username,
+ 'auth_token':user.auth_token,
+ 'auth_token_created':user.auth_token_created,
+ 'auth_token_expires':user.auth_token_expires}
response.content = json.dumps(user_info)
update_response_headers(response)
return response
from astakos.im.models import AstakosUser
-class AstakosUserModelBackend(ModelBackend):
+class AstakosUserModelCredentialsBackend(ModelBackend):
def authenticate(self, username=None, password=None):
try:
user = AstakosUser.objects.get(username=username)
return AstakosUser.objects.get(pk=user_id)
except AstakosUser.DoesNotExist:
return None
-
+
#@property
#def user_class(self):
# if not hasattr(self, '_user_class'):
# print '#', self._user_class
# if not self._user_class:
# raise ImproperlyConfigured('Could not get custom user model')
- # return self._user_class
\ No newline at end of file
+ # return self._user_class
+
+class AstakosUserModelTokenBackend(ModelBackend):
+ def authenticate(self, username=None, auth_token=None):
+ try:
+ user = AstakosUser.objects.get(username=username)
+ if user.auth_token == auth_token:
+ return user
+ except AstakosUser.DoesNotExist:
+ return None
+
+ def get_user(self, user_id):
+ try:
+ return AstakosUser.objects.get(pk=user_id)
+ except AstakosUser.DoesNotExist:
+ return None
\ No newline at end of file
from django.conf import settings
from django.utils.importlib import import_module
+from django.core.exceptions import ImproperlyConfigured
+from django.core.mail import send_mail
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext as _
+from django.contrib.auth.forms import UserCreationForm
+from django.contrib import messages
+
+from smtplib import SMTPException
+from urllib import quote
+
+from astakos.im.util import get_or_create_user
+from astakos.im.models import AstakosUser, Invitation
+from astakos.im.forms import ExtendedUserCreationForm, InvitedExtendedUserCreationForm
def get_backend():
"""
Return an instance of a registration backend,
- according to the INVITATIONS_ENABLED setting.
-
+ according to the INVITATIONS_ENABLED setting
+ (if True returns ``astakos.im.backends.InvitationsBackend`` and if False
+ returns ``astakos.im.backends.SimpleBackend``).
+
+ If the backend cannot be located ``django.core.exceptions.ImproperlyConfigured``
+ is raised.
"""
- module = 'invitations' if settings.INVITATIONS_ENABLED else 'simple'
- module = 'astakos.im.backends.%s' %module
- backend_class_name = 'Backend'
+ module = 'astakos.im.backends'
+ prefix = 'Invitations' if settings.INVITATIONS_ENABLED else 'Simple'
+ backend_class_name = '%sBackend' %prefix
try:
mod = import_module(module)
except ImportError, e:
backend_class = getattr(mod, backend_class_name)
except AttributeError:
raise ImproperlyConfigured('Module "%s" does not define a registration backend named "%s"' % (module, attr))
- return backend_class()
\ No newline at end of file
+ return backend_class()
+
+class InvitationsBackend(object):
+ """
+ A registration backend which implements the following workflow: a user
+ supplies the necessary registation information, if the request contains a valid
+ inivation code the user is automatically activated otherwise an inactive user
+ account is created and the user is going to receive an email as soon as an
+ administrator activates his/her account.
+ """
+ def get_signup_form(self, request):
+ """
+ Returns the necassary registration form depending the user is invited or not
+
+ Throws Invitation.DoesNotExist in case ``code`` is not valid.
+ """
+ code = request.GET.get('code', '')
+ formclass = 'ExtendedUserCreationForm'
+ if request.method == 'GET':
+ initial_data = None
+ if code:
+ formclass = 'Invited%s' %formclass
+ self.invitation = Invitation.objects.get(code=code)
+ if self.invitation.is_consumed:
+ return HttpResponseBadRequest('Invitation has beeen used')
+ initial_data.update({'username':self.invitation.username,
+ 'email':self.invitation.username,
+ 'realname':self.invitation.realname})
+ inviter = AstakosUser.objects.get(username=self.invitation.inviter)
+ initial_data['inviter'] = inviter.realname
+ else:
+ initial_data = request.POST
+ self.form = globals()[formclass](initial_data)
+ return self.form
+
+ def _is_preaccepted(self, user):
+ """
+ If there is a valid, not-consumed invitation code for the specific user
+ returns True else returns False.
+
+ It should be called after ``get_signup_form`` which sets invitation if exists.
+ """
+ invitation = getattr(self, 'invitation') if hasattr(self, 'invitation') else None
+ if not invitation:
+ return False
+ if invitation.username == user.username and not invitation.is_consumed:
+ return True
+ return False
+
+ def signup(self, request):
+ """
+ Creates a incative user account. If the user is preaccepted (has a valid
+ invitation code) the user is activated and if the request param ``next``
+ is present redirects to it.
+ In any other case the method returns the action status and a message.
+ """
+ kwargs = {}
+ form = self.form
+ user = form.save(commit=False)
+
+ try:
+ if self._is_preaccepted(user):
+ user.is_active = True
+ message = _('Registration completed. You can now login.')
+ next = request.POST.get('next')
+ if next:
+ return redirect(next)
+ else:
+ message = _('Registration completed. You will receive an email upon your account\'s activation')
+ status = messages.SUCCESS
+ except Invitation.DoesNotExist, e:
+ status = messages.ERROR
+ message = _('Invalid invitation code')
+ return status, message
+
+class SimpleBackend(object):
+ """
+ A registration backend which implements the following workflow: a user
+ supplies the necessary registation information, an incative user account is
+ created and receives an email in order to activate his/her account.
+ """
+ def get_signup_form(self, request):
+ """
+ Returns the UserCreationForm
+ """
+ initial_data = request.POST if request.method == 'POST' else None
+ return UserCreationForm(initial_data)
+
+ def signup(self, request, email_template_name='activation_email.txt'):
+ """
+ Creates an inactive user account and sends a verification email.
+
+ ** Arguments **
+
+ ``email_template_name``
+ A custom template for the verification email body to use. This is
+ optional; if not specified, this will default to
+ ``activation_email.txt``.
+
+ ** Templates **
+ activation_email.txt or ``email_template_name`` keyword argument
+
+ ** Settings **
+
+ * ACTIVATION_LOGIN_TARGET: Where users should activate their local account
+ * DEFAULT_CONTACT_EMAIL: service support email
+ * DEFAULT_FROM_EMAIL: from email
+ """
+ kwargs = {}
+ form = self.form
+ user = form.save(commit=False)
+ status = messages.SUCCESS
+ try:
+ _send_verification(request, user, email_template_name)
+ message = _('Verification sent to %s' % user.email)
+ except (SMTPException, socket.error) as e:
+ status = messages.ERROR
+ name = 'strerror'
+ message = getattr(e, name) if hasattr(e, name) else e
+ return status, message
+
+ def _send_verification(request, user, template_name):
+ site = get_current_site(request)
+ baseurl = request.build_absolute_uri('/').rstrip('/')
+ url = settings.ACTIVATION_LOGIN_TARGET % (baseurl,
+ quote(user.auth_token),
+ quote(baseurl))
+ message = render_to_string(template_name, {
+ 'user': user,
+ 'url': url,
+ 'baseurl': baseurl,
+ 'site_name': site.name,
+ 'support': settings.DEFAULT_CONTACT_EMAIL})
+ sender = settings.DEFAULT_FROM_EMAIL
+ send_mail('Pithos account activation', message, sender, [user.email])
+ logging.info('Sent activation %s', user)
\ No newline at end of file
+++ /dev/null
-# 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 astakos.im.forms import InvitedLocalRegisterForm, LocalRegisterForm
-from astakos.im.models import AstakosUser, Invitation
-
-class Backend(object):
- def get_signup_form(self, request):
- code = request.GET.get('code', '')
- formclass = 'LocalRegisterForm'
- if request.method == 'GET':
- initial_data = None
- if code:
- formclass = 'InvitedLocalRegiterForm'
- invitation = Invitation.objects.get(code=code)
- if invitation.is_consumed:
- return HttpResponseBadRequest('Invitation has beeen used')
- initial_data.update({'username':invitation.username,
- 'email':invitation.username,
- 'realname':invitation.realname})
- inviter = AstakosUser.objects.get(username=invitation.inviter)
- initial_data['inviter'] = inviter.realname
- else:
- initial_data = request.POST
- return globals()[formclass](initial_data)
-
- def is_preaccepted(user, code):
- invitation = self.invitation
- if invitation and not invitation.is_consumed and invitation.code == code:
- return True
- return False
-
- def signup(self, request, form):
- kwargs = {}
- for field in form.fields:
- if hasattr(AstakosUser(), field):
- kwargs[field] = form.cleaned_data[field]
- user = get_or_create_user(**kwargs)
-
- code = request.POST.get('code')
- if is_preaccepted(user, code):
- user.is_active = True
- user.save()
- message = _('Registration completed. You can now login.')
- next = request.POST.get('next')
- if next:
- return redirect(next)
- else:
- message = _('Registration completed. You will receive an email upon your account\'s activation')
- status = 'success'
- return status, message
\ No newline at end of file
+++ /dev/null
-# 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 socket
-import logging
-
-from django.core.mail import send_mail
-from django.conf import settings
-from django.template.loader import render_to_string
-from django.utils.translation import ugettext as _
-from smtplib import SMTPException
-from urllib import quote
-
-from astakos.im.forms import LocalRegisterForm
-from astakos.im.util import get_or_create_user
-from astakos.im.models import AstakosUser
-
-class Backend(object):
- def get_signup_form(self, request):
- initial_data = request.POST if request.method == 'POST' else None
- return LocalRegisterForm(initial_data)
-
- def signup(self, request, form, success_url):
- kwargs = {}
- for field in form.fields:
- if hasattr(AstakosUser(), field):
- kwargs[field] = form.cleaned_data[field]
- user = get_or_create_user(**kwargs)
-
- status = 'success'
- try:
- send_verification(request.build_absolute_uri('/').rstrip('/'), user)
- message = _('Verification sent to %s' % user.email)
- except (SMTPException, socket.error) as e:
- status = 'error'
- name = 'strerror'
- message = getattr(e, name) if hasattr(e, name) else e
-
- if user and status == 'error':
- #delete created user
- user.delete()
- return status, message
-
-def send_verification(baseurl, user):
- url = settings.ACTIVATION_LOGIN_TARGET % (baseurl,
- quote(user.auth_token),
- quote(baseurl))
- message = render_to_string('activation.txt', {
- 'user': user,
- 'url': url,
- 'baseurl': baseurl,
- 'service': settings.SERVICE_NAME,
- 'support': settings.DEFAULT_CONTACT_EMAIL})
- sender = settings.DEFAULT_FROM_EMAIL
- send_mail('Pithos account activation', message, sender, [user.email])
- logging.info('Sent activation %s', user)
\ No newline at end of file
def code(request):
return {'code' : request.GET.get('code', '')}
+
+def invitations(request):
+ return {'invitations_enabled' :settings.INVITATIONS_ENABLED}
\ No newline at end of file
[
{
"model": "im.AstakosUser",
- "pk": 1,
+ "pk": 2,
"fields": {
"username": "test",
"level": 0,
},
{
"model": "im.AstakosUser",
- "pk": 2,
+ "pk": 3,
"fields": {
"username": "verigak",
"level": 1,
},
{
"model": "im.AstakosUser",
- "pk": 3,
+ "pk": 4,
"fields": {
"username": "chazapis",
"level": 1,
},
{
"model": "im.AstakosUser",
- "pk": 4,
+ "pk": 5,
"fields": {
"username": "gtsouk",
"level": 1,
},
{
"model": "im.AstakosUser",
- "pk": 5,
+ "pk": 6,
"fields": {
"username": "papagian",
"level": 1,
},
{
"model": "im.AstakosUser",
- "pk": 6,
+ "pk": 7,
"fields": {
"username": "louridas",
"level": 1,
},
{
"model": "im.AstakosUser",
- "pk": 7,
+ "pk": 8,
"fields": {
"username": "chstath",
"level": 1,
},
{
"model": "im.AstakosUser",
- "pk": 8,
+ "pk": 9,
"fields": {
"username": "pkanavos",
"level": 1,
},
{
"model": "im.AstakosUser",
- "pk": 9,
+ "pk": 10,
"fields": {
"username": "mvasilak",
"level": 1,
},
{
"model": "im.AstakosUser",
- "pk": 10,
+ "pk": 11,
"fields": {
"username": "διογένης",
"level": 2,
from django import forms
from django.utils.translation import ugettext as _
+from django.contrib.auth.forms import UserCreationForm
from django.conf import settings
from hashlib import new as newhasher
from astakos.im.models import AstakosUser
+from astakos.im.util import get_or_create_user
-class RegisterForm(forms.Form):
- username = forms.CharField(widget=forms.widgets.TextInput())
- email = forms.EmailField(widget=forms.TextInput(),
- label=_('Email address'))
- first_name = forms.CharField(widget=forms.TextInput(),
- label=u'First Name', required=False)
- last_name = forms.CharField(widget=forms.TextInput(),
- label=u'Last Name', required=False)
-
- def __init__(self, *args, **kwargs):
- super(forms.Form, self).__init__(*args, **kwargs)
-
- def clean_username(self):
- """
- Validate that the username is alphanumeric and is not already
- in use.
-
- """
+class UniqueUserEmailField(forms.EmailField):
+ """
+ An EmailField which only is valid if no User has that email.
+ """
+ def validate(self, value):
+ super(forms.EmailField, self).validate(value)
try:
- user = AstakosUser.objects.get(username__iexact=self.cleaned_data['username'])
+ AstakosUser.objects.get(email = value)
+ raise forms.ValidationError("Email already exists")
+ except AstakosUser.MultipleObjectsReturned:
+ raise forms.ValidationError("Email already exists")
except AstakosUser.DoesNotExist:
- return self.cleaned_data['username']
- raise forms.ValidationError(_("A user with that username already exists."))
+ pass
-class LocalRegisterForm(RegisterForm):
- """ local signup form"""
- password = forms.CharField(widget=forms.PasswordInput(render_value=False),
- label=_('Password'))
- password2 = forms.CharField(widget=forms.PasswordInput(render_value=False),
- label=_('Confirm Password'))
+class ExtendedUserCreationForm(UserCreationForm):
+ """
+ Extends the built in UserCreationForm in several ways:
- def __init__(self, *args, **kwargs):
- super(LocalRegisterForm, self).__init__(*args, **kwargs)
+ * Adds an email field, which uses the custom UniqueUserEmailField,
+ that is, the form does not validate if the email address already exists
+ in the User table.
+ * The username field is generated based on the email, and isn't visible.
+ * first_name and last_name fields are added.
+ * Data not saved by the default behavior of UserCreationForm is saved.
+ """
+
+ username = forms.CharField(required = False, max_length = 30)
+ email = UniqueUserEmailField(required = True, label = 'Email address')
+ first_name = forms.CharField(required = False, max_length = 30)
+ last_name = forms.CharField(required = False, max_length = 30)
- def clean_username(self):
+ def __init__(self, *args, **kwargs):
"""
- Validate that the username is alphanumeric and is not already
- in use.
-
+ Changes the order of fields, and removes the username field.
"""
- try:
- user = AstakosUser.objects.get(username__iexact=self.cleaned_data['username'])
- except AstakosUser.DoesNotExist:
- return self.cleaned_data['username']
- raise forms.ValidationError(_("A user with that username already exists."))
+ super(UserCreationForm, self).__init__(*args, **kwargs)
+ self.fields.keyOrder = ['email', 'first_name', 'last_name',
+ 'password1', 'password2']
- def clean(self):
+ def clean(self, *args, **kwargs):
+ """
+ Normal cleanup + username generation.
"""
- Verifiy that the values entered into the two password fields
- match. Note that an error here will end up in
- ``non_field_errors()`` because it doesn't apply to a single
- field.
+ cleaned_data = super(UserCreationForm, self).clean(*args, **kwargs)
+ if cleaned_data.has_key('email'):
+ #cleaned_data['username'] = self.__generate_username(
+ # cleaned_data['email'])
+ cleaned_data['username'] = cleaned_data['email']
+ return cleaned_data
+ def save(self, commit=True):
"""
- if 'password' in self.cleaned_data and 'password2' in self.cleaned_data:
- if self.cleaned_data['password'] != self.cleaned_data['password2']:
- raise forms.ValidationError(_("The two password fields didn't match."))
- return self.cleaned_data
+ Saves the email, first_name and last_name properties, after the normal
+ save behavior is complete.
+ """
+ user = super(UserCreationForm, self).save(commit)
+ if user:
+ kwargs = {}
+ for field in self.fields:
+ if hasattr(AstakosUser(), field):
+ kwargs[field] = self.cleaned_data[field]
+ user = get_or_create_user(username=self.cleaned_data['email'], **kwargs)
+ return user
-class InvitedRegisterForm(RegisterForm):
+class InvitedExtendedUserCreationForm(ExtendedUserCreationForm):
+ """
+ Subclass of ``RegistrationForm`` for registring a invited user. Adds a
+ readonly field for inviter's name. The email is also readonly since
+ it will be the invitation username.
+ """
inviter = forms.CharField(widget=forms.TextInput(),
label=_('Inviter Real Name'))
super(RegisterForm, self).__init__(*args, **kwargs)
#set readonly form fields
- self.fields['username'].widget.attrs['readonly'] = True
self.fields['inviter'].widget.attrs['readonly'] = True
-class InvitedLocalRegisterForm(LocalRegisterForm, InvitedRegisterForm):
- pass
+class ProfileForm(forms.ModelForm):
+ """
+ Subclass of ``ModelForm`` for permiting user to edit his/her profile.
+ Most of the fields are readonly since the user is not allowed to change them.
+
+ The class defines a save method which sets ``is_verified`` to True so as the user
+ during the next login will not to be redirected to profile page.
+ """
+ class Meta:
+ model = AstakosUser
+ exclude = ('groups', 'user_permissions')
+
+ def __init__(self, *args, **kwargs):
+ super(ProfileForm, self).__init__(*args, **kwargs)
+ instance = getattr(self, 'instance', None)
+ ro_fields = ('username','date_joined', 'updated', 'auth_token',
+ 'auth_token_created', 'auth_token_expires', 'invitations',
+ 'level', 'last_login', 'email', 'is_active', 'is_superuser',
+ 'is_staff')
+ if instance and instance.id:
+ for field in ro_fields:
+ if isinstance(self.fields[field].widget, forms.CheckboxInput):
+ self.fields[field].widget.attrs['disabled'] = True
+ self.fields[field].widget.attrs['readonly'] = True
+
+ def save(self, commit=True):
+ user = super(ProfileForm, self).save(commit=False)
+ user.is_verified = True
+ if commit:
+ user.save()
+ return user
+
-class LoginForm(forms.Form):
- username = forms.CharField(widget=forms.widgets.TextInput())
- password = forms.CharField(widget=forms.PasswordInput(render_value=False),
- label=_('Password'))
\ No newline at end of file
+class FeedbackForm(forms.Form):
+ """
+ Form for writing feedback.
+ """
+ feedback_msg = forms.CharField(widget=forms.Textarea(),
+ label=u'Message', required=False)
+ feedback_data = forms.CharField(widget=forms.Textarea(),
+ label=u'Data', required=False)
+
\ No newline at end of file
from astakos.im.interface import get_quota, set_quota
class AstakosUser(User):
+ """
+ Extends ``django.contrib.auth.models.User`` by defining additional fields.
+ """
# Use UserManager to get the create_user method, etc.
objects = UserManager()
auth_token_expires = models.DateTimeField('Token expiration date', null=True)
updated = models.DateTimeField('Update date')
+ is_verified = models.BooleanField('Is verified?', default=False)
@property
def realname(self):
return self.username
class Invitation(models.Model):
+ """
+ Model for registring invitations
+ """
inviter = models.ForeignKey(AstakosUser, related_name='invitations_sent',
null=True)
realname = models.CharField('Real name', max_length=255)
from django.conf import settings
from django.http import HttpResponseBadRequest
+from django.contrib.auth import authenticate
from astakos.im.models import Invitation
from astakos.im.target.util import prepare_response
logging.info('Accepted invitation %s', invitation)
user = get_or_create_user(invitation.uniq,
- invitation.realname,
- 'Invitation',
- invitation.inviter.level + 1)
+ invitation.realname,
+ 'Invitation',
+ invitation.inviter.level + 1)
+ # in order to login the user we must call authenticate first
+ authenticate(username=user.username, auth_token=user.auth_token)
next = request.GET.get('next')
return prepare_response(request, user, next, 'renew' in request.GET)
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.contrib.auth import authenticate
+from django.contrib.auth.forms import AuthenticationForm
+from django.contrib import messages
from django.utils.translation import ugettext as _
from astakos.im.target.util import prepare_response
from astakos.im.models import AstakosUser
-from astakos.im.forms import LoginForm
from urllib import unquote
"""
on_failure: whatever redirect accepts as to
"""
- form = LoginForm(request.POST)
-
+ form = AuthenticationForm(data=request.POST)
if not form.is_valid():
return render_to_response(on_failure,
{'form':form},
context_instance=RequestContext(request))
+ # get the user from the cash
+ user = form.user_cache
- user = authenticate(**form.cleaned_data)
- status = 'success'
+ message = None
if not user:
- status = 'error'
message = _('Cannot authenticate account')
elif not user.is_active:
- status = 'error'
message = _('Inactive account')
-
- if status == 'error':
+ if message:
+ messages.add_message(request, message.ERROR, message)
return render_to_response(on_failure,
- {'form':form,
- 'message': _('Unverified account')},
+ {'form':form},
context_instance=RequestContext(request))
next = request.POST.get('next')
user.is_active = True
user.save()
return prepare_response(request, user, next, renew=True)
-
-def reset_password(request):
- if request.method == 'GET':
- cookie_value = unquote(request.COOKIES.get('_pithos2_a', ''))
- if cookie_value and '|' in cookie_value:
- token = cookie_value.split('|', 1)[1]
- else:
- token = request.GET.get('auth')
- next = request.GET.get('next')
- username = request.GET.get('username')
- kwargs = {'auth': token,
- 'next': next,
- 'username' : username}
- if not token:
- kwargs.update({'status': 'error',
- 'message': 'Missing token'})
- html = render_to_string('reset.html', kwargs)
- return HttpResponse(html)
- elif request.method == 'POST':
- token = request.POST.get('auth')
- username = request.POST.get('username')
- password = request.POST.get('password')
- next = request.POST.get('next')
- if not token:
- status = 'error'
- message = 'Bad Request: missing token'
- try:
- user = AstakosUser.objects.get(auth_token=token)
- if username != user.username:
- status = 'error'
- message = 'Bad Request: username mismatch'
- else:
- user.password = password
- user.status = 'NORMAL'
- user.save()
- return prepare_response(request, user, next, renew=True)
- except AstakosUser.DoesNotExist:
- status = 'error'
- message = 'Bad Request: invalid token'
-
- html = render_to_string('reset.html', {
- 'status': status,
- 'message': message})
- return HttpResponse(html)
from django.http import HttpResponseBadRequest
from django.core.urlresolvers import reverse
+from django.contrib.auth import authenticate
from astakos.im.target.util import prepare_response
from astakos.im.util import get_or_create_user
affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '')
+ user = get_or_create_user(username, realname=realname, affiliation=affiliation, level=0, email=eppn)
+ # in order to login the user we must call authenticate first
+ user = authenticate(username=user.username, auth_token=user.auth_token)
return prepare_response(request,
- get_or_create_user(eppn, realname, affiliation, 0),
+ user,
request.GET.get('next'),
'renew' in request.GET)
from django.conf import settings
from django.http import HttpResponse
from django.utils import simplejson as json
+from django.contrib.auth import authenticate
from astakos.im.target.util import prepare_response
from astakos.im.util import get_or_create_user
# can prompt them for their email here. Either way, the password
# should never be used.
username = '%s@twitter.com' % access_token['screen_name']
- realname = access_token['user_id']
+ realname = access_token['screen_name']
+ user = get_or_create_user(username, realname=realname, affiliation='Twitter', level=0, email=username)
+ # in order to login the user we must call authenticate first
+ user = authenticate(username=user.username, auth_token=user.auth_token)
return prepare_response(request,
- get_or_create_user(username, realname, 'Twitter', 0),
+ user,
request_token.get('next'))
from django.utils.http import urlencode
from django.core.urlresolvers import reverse
from django.conf import settings
+from django.contrib.auth import login
def prepare_response(request, user, next='', renew=False):
"""Return the unique username and the token
params = ''
if next:
params = '?' + urlencode({'next': next})
- next = reverse('astakos.im.views.users_profile') + params
+ next = reverse('astakos.im.views.edit_profile') + params
+
+ # user login
+ login(request, user)
response = HttpResponse()
- expire_fmt = auth_token_expires.strftime('%a, %d-%b-%Y %H:%M:%S %Z')
- cookie_value = quote(user.username + '|' + auth_token)
- response.set_cookie('_pithos2_a', value=cookie_value, expires=expire_fmt, path='/')
-
if not next:
response['X-Auth-User'] = user.username
response['X-Auth-Token'] = auth_token
--- /dev/null
+{% extends "base.html" %}
+
+{% block tabs %}
+<ul class="tabs">
+ <li{% ifequal tab "profile" %} class="active"{% endifequal %}>
+ <a href="{% url astakos.im.views.edit_profile %}">Profile</a>
+ </li>
+ <li{% ifequal tab "passwordchange" %} class="active"{% endifequal %}>
+ <a href="{% url django.contrib.auth.views.password_change %}">Password Change</a>
+ </li>
+ {% if invitations_enabled %}
+ <li{% ifequal tab "invite" %} class="active"{% endifequal %}>
+ <a href="{% url astakos.im.views.invite %}">Invite</a>
+ </li>
+ {% endif %}
+ <li{% ifequal tab "feedback" %} class="active"{% endifequal %}>
+ <a href="{% url astakos.im.views.send_feedback %}">Send Feedback</a>
+ </li>
+ <li{% ifequal tab "logout" %} class="active"{% endifequal %}>
+ <a href="{% url django.contrib.auth.views.logout %}">Logout</a>
+ </li>
+</ul>
+{% endblock %}
+
+{% block body %}{% endblock %}
Αγαπητέ/η {{ user.realname }},
Έπειτα από αίτημα σας έχει δημιουργηθεί ο λογαρισμός σας
-για την υπηρεσία {{ service }} της ΕΔΕΤ κατά την Alpha (πιλοτική) φάση λειτουργίας της.
+για την υπηρεσία {{ site_name }} της ΕΔΕΤ κατά την Alpha (πιλοτική) φάση λειτουργίας της.
Για να τον ενεργοποιήσετε, χρησιμοποιήστε τον παρακάτω σύνδεσμο:
υπηρεσίας, δεν αποκλείεται να εμφανιστούν προβλήματα στο λογισμικό
διαχείρισης ή η υπηρεσία να μην είναι διαθέσιμη κατά διαστήματα. Για
αυτό το λόγο, σας παρακαλούμε να μη μεταφέρετε ακόμη σημαντικά κομμάτια
-της δουλειάς σας στην υπηρεσία {{ service }}. Επίσης, παρακαλούμε να έχετε
+της δουλειάς σας στην υπηρεσία {{ site_name }}. Επίσης, παρακαλούμε να έχετε
υπόψη σας ότι όλα τα δεδομένα, θα διαγραφούν κατά τη μετάβαση από την
έκδοση Alpha στην έκδοση Beta. Θα υπάρξει έγκαιρη ειδοποίησή σας πριν
από τη μετάβαση αυτή.
Για όποιες παρατηρήσεις ή προβλήματα στη λειτουργεία της υπηρεσίας μπορείτε να
απευθυνθείτε στο {{ support }}.
-Σας ευχαριστούμε πολύ για τη συμμετοχή στην Alpha λειτουργία του {{ service }}.
+Σας ευχαριστούμε πολύ για τη συμμετοχή στην Alpha λειτουργία του {{ site_name }}.
Με εκτίμηση,
Dear {{ user.realname }},
-After your request a new account for GRNET's {{ service }} service has been created
+After your request a new account for GRNET's {{ site_name }} service has been created
for its Alpha test phase.
To activate the account, please use the following link:
taken to ensure high quality, it is possible there may still be software
bugs, or periods of limited service availability. For this reason, we
kindly request you do not transfer important parts of your work to
-{{ service }}, yet. Also, please bear in mind that all data, will be deleted
+{{ site_name }}, yet. Also, please bear in mind that all data, will be deleted
when the service moves to Beta test. Before the transition, you will be
notified in time.
For any remarks or problems you can contact {{ support }}.
-Thank you for participating in the Alpha test of {{ service }}.
+Thank you for participating in the Alpha test of {{ site_name }}.
Greek Research and Technonogy Network - GRNET
<html>
<head>
<meta charset="utf-8" />
- <title>{{ title|default:"User Admin" }}</title>
+ <title>{{ title|default:"Astakos Login" }}</title>
<link rel="stylesheet" href="/im/static/bootstrap.css">
<script src="/im/static/jquery.js"></script>
<script src="/im/static/jquery.tablesorter.js"></script>
</div>
{% block title %}{% endblock %}
- {% if message %}
- <br />
- <div class="alert-message.{{ status }}">
- <p>{{ message }}</p>
- </div>
+ {% if messages %}
+ <ul class="messages">
+ {% for message in messages %}
+ <li{% if message.tags %} class="alert-message.{{ message.tags }}"{% endif %}>{{ message }}</li>
+ {% endfor %}
+ </ul>
{% endif %}
{% block tabs %}{% endblock %}
--- /dev/null
+{% extends "account_base.html" %}
+
+{% load formatters %}
+
+{% block body %}
+
+<form method="post">{% csrf_token %}
+ {{ form.as_p }}
+
+ <div class="actions">
+ <input type="hidden" name="next" value="{{ next }}">
+ <button type="submit" class="btn primary">Send Feedback</button>
+ </div>
+
+</form>
+{% endblock body %}
--- /dev/null
+Feedback message:
+{{ message }}
+
+User info:
+ID: {{ request.user.id }}
+Email: {{ request.user.uniq }}
+
+User application data:
+{{ data|safe }}
+
{% if "local" in im_modules %}
<div class="span4">
<h4>Local account</h4>
- <form action="{% url astakos.im.target.local.login %}" method="post" class="form-stacked">
+ <form action="{% url astakos.im.target.local.login %}" method="post" class="form-stacked">{% csrf_token %}
{{ form.as_p }}
<div>
- <a href="{% url astakos.im.views.reclaim_password %}">Forgot your password?</a>
+ <a href="{% url django.contrib.auth.views.password_reset %}">Forgot your password?</a>
</div>
<br>
<div class="">
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="utf-8" />
- <title>Invitations</title>
- <link rel="stylesheet" href="/im/static/bootstrap.css">
-</head>
-<body>
+{% extends "account_base.html" %}
+
+{% load formatters %}
+
+{% block body %}
<div class="container">
<h3>You have {{ user.invitations }} invitation{{ user.invitations|pluralize }} left.</h3>
- {% if message %}
+ <!--{% if message %}
<br />
<div class="alert-message {{ status }}">
<p>{{ message }}</p>
</div>
- {% endif %}
+ {% endif %}-->
{% if user.invitations %}
<br />
<h4>Invite someone else:</h4>
- <form method="post">
+ <form method="post">{% csrf_token %}
<div class="clearfix">
<label for="user-realname">Name</label>
<div class="input">
</div>
</form>
{% endif %}
-</div>
-</body>
-</html>
+{% endblock body %}
--- /dev/null
+{% extends "account_base.html" %}
+
+{% load formatters %}
+
+{% block body %}
+
+<form method="post">{% csrf_token %}
+ {{ form.as_p }}
+
+ <div class="actions">
+ <input type="hidden" name="next" value="{{ next }}">
+ <input type="hidden" name="auth" value="{{ user.auth_token }}">
+ <button type="submit" class="btn primary">Update</button>
+ </div>
+
+</form>
+{% endblock body %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title%}
- <h2>Reclaim password</h2>
-{% endblock title%}
-
-{% block body %}
-<form action="{% url astakos.im.views.reclaim_password %}" method="post">
- <div class="clearfix">
- <label for="user-uniq">Username</label>
- <div class="input">
- <input class="span4" id="user-username" name="username" type="text" />
- </div>
- </div>
-
- <div class="actions">
- <button type="submit" class="btn primary">Go</button>
- </div>
-</form>
-{% endblock body %}
{% endblock title%}
{% block body %}
-<form action={%url astakos.im.views.register%} method="post">
+<form action={%url astakos.im.views.register%} method="post">{% csrf_token %}
{{ form.as_p }}
<div>
<button type="submit" class="btn primary">Register</button>
--- /dev/null
+{% extends 'base.html'%}
+
+{% block title%}
+ <h2>Logout</h2>
+ <p>You have successfully logged out. <a href="{% url astakos.im.views.index %}">Login</a>.</p>
+{% endblock title%}
-{% extends "base.html" %}
-
-{% block title%}
- <h2>Reset password</h2>
-{% endblock title%}
+{% extends "account_base.html" %}
{% block body %}
-<form action="{% url astakos.im.target.local.reset_password %}" method="post">
+<form action="{% url django.contrib.auth.views.password_change %}" method="post">{% csrf_token %}
<div class="clearfix">
- <label for="user-password">password</label>
- <div class="input">
- <input class="span4" id="user-password" name="password" type="password" />
- </div>
+ {{ form.as_p }}
</div>
<div class="actions">
--- A translation in English follows ---
-
-Αγαπητέ/η {{ user.realname }},
-
Για να ανανεώσετε τον κωδικό πρόσβασης σας
-για την υπηρεσία {{ service }} της ΕΔΕΤ κατά την Alpha (πιλοτική) φάση λειτουργίας της,
+για την υπηρεσία {{ site_name }} της ΕΔΕΤ κατά την Alpha (πιλοτική) φάση λειτουργίας της,
χρησιμοποιήστε τον παρακάτω σύνδεσμο:
-{{ url }}
+{{ protocol }}://{{ domain }}/local/reset/confirm/{{ uid }}-{{ token }}/
Σημείωση:
υπηρεσίας, δεν αποκλείεται να εμφανιστούν προβλήματα στο λογισμικό
διαχείρισης ή η υπηρεσία να μην είναι διαθέσιμη κατά διαστήματα. Για
αυτό το λόγο, σας παρακαλούμε να μη μεταφέρετε ακόμη σημαντικά κομμάτια
-της δουλειάς σας στην υπηρεσία {{ service }}. Επίσης, παρακαλούμε να έχετε
+της δουλειάς σας στην υπηρεσία {{ site_name }}. Επίσης, παρακαλούμε να έχετε
υπόψη σας ότι όλα τα δεδομένα, θα διαγραφούν κατά τη μετάβαση από την
έκδοση Alpha στην έκδοση Beta. Θα υπάρξει έγκαιρη ειδοποίησή σας πριν
από τη μετάβαση αυτή.
-Περισσότερα για την υπηρεσία θα βρείτε στο {{ baseurl }}, αφού
+Περισσότερα για την υπηρεσία θα βρείτε στο {{ protocol }}://{{ domain }}/, αφού
έχετε ενεργοποιήσει την πρόσκλησή σας.
Για όποιες παρατηρήσεις ή προβλήματα στη λειτουργεία της υπηρεσίας μπορείτε να
λειτουργικότητα και την αξιοπιστία της καινοτομικής αυτής υπηρεσίας.
-Σας ευχαριστούμε πολύ για τη συμμετοχή στην Alpha λειτουργία του {{ service }}.
+Σας ευχαριστούμε πολύ για τη συμμετοχή στην Alpha λειτουργία του {{ site_name }}.
Με εκτίμηση,
--
-Dear {{ user.realname }},
+You can use the following link:
-Yuu can use the following link:
+{{ protocol }}://{{ domain }}/local/reset/confirm/{{ uid }}-{{ token }}/
-{{ url }}
-
-to reset your password for GRNET's {{ service }} service has been created
+to reset your password for GRNET's {{ site_name }} service has been created
for its Alpha test phase.
To activate the account, please use the following link:
taken to ensure high quality, it is possible there may still be software
bugs, or periods of limited service availability. For this reason, we
kindly request you do not transfer important parts of your work to
-{{ service }}, yet. Also, please bear in mind that all data, will be deleted
+{{ site_name }}, yet. Also, please bear in mind that all data, will be deleted
when the service moves to Beta test. Before the transition, you will be
notified in time.
-For more information, please visit {{ baseurl }}, after
+For more information, please visit {{ protocol }}://{{ domain }}/, after
activating your invitation.
We look forward to your feedback, to improve the functionality and
reliability of this innovative service.
-For any remarks or problems you can contact {{ support }}.
-
-Thank you for participating in the Alpha test of {{ service }}.
+Thank you for participating in the Alpha test of {{ site_name }}.
Greek Research and Technonogy Network - GRNET
--- /dev/null
+{% extends 'base.html'%}
+
+{% block title%}
+ <h2>Password reset</h2>
+ <p>Password reset successfully</p>
+{% endblock title%}
--- /dev/null
+{% extends 'base.html'%}
+
+{% block title%}
+ <h2>Please enter your new password</h2>
+{% endblock title%}
+
+{% block body %}
+ {% if validlink %}
+ <form action="" method="post">{% csrf_token %}
+ {{ form.as_p }}
+ <div class="actions">
+ <button type="submit" class="btn primary">Go</button>
+ </div>
+ </form>
+ {% else %}
+ The password reset link was invalid
+ {% endif %}
+{% endblock body %}
--- /dev/null
+{% extends 'base.html'%}
+
+{% block title%}
+ <h2>Password reset</h2>
+ <p>E-mail sent</p>
+{% endblock title%}
--- /dev/null
+{% extends "base.html" %}
+
+{% block title%}
+ <h2>Reset password</h2>
+{% endblock title%}
+
+{% block body %}
+<form action="{% url django.contrib.auth.views.password_reset %}" method="post">{% csrf_token %}
+ {{form.as_p}}
+ <div class="actions">
+ <button type="submit" class="btn primary">Go</button>
+ </div>
+</form>
+{% endblock body %}
{% if "local" in im_modules %}
<div class="span4">
<h4>Local account</h4>
- <form action="" method="post" class="form-stacked">
+ <form action="" method="post" class="form-stacked">{% csrf_token %}
{{ form.as_p }}
<br>
<div class="">
+++ /dev/null
-{% extends "base.html" %}
-
-{% load formatters %}
-
-{% block title%}
- <h2>User Profile</h2>
-{% endblock title%}
-
-{% block body %}
-
-<form action="{% url astakos.im.views.users_edit%}" method="post">
- <div class="clearfix">
- <label for="user-id">ID</label>
- <div class="input">
- <span class="uneditable-input" id="user-id">{{ user.id }}</span>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-username">Username</label>
- <div class="input">
- <span class="uneditable-input" id="user-username">{{ user.username }}</span>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-first-name">Real Name</label>
- <div class="input">
- <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">Real Name</label>
- <div class="input">
- <input class="span4" id="user-last-name" name="last_name" value="{{ user.last_name }}" type="text" />
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-admin">Admin</label>
- <div class="input">
- <ul class="inputs-list">
- <li>
- <label>
- <input type="checkbox" id="user-admin" name="admin"{% if user.is_superuser %} checked{% endif %} disabled="disabled">
- </label>
- </li>
- </ul>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-affiliation">Affiliation</label>
- <div class="input">
- <input class="span4" id="user-affiliation" name="affiliation" value="{{ user.affiliation }}" type="text" />
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-is-active">Is active?</label>
- <div class="input">
- <ul class="inputs-list">
- <li>
- <label>
- <input type="checkbox" id="user-is-active" name="is_active"{% if user.is_active %} checked{% endif %} disabled="disabled">
- </label>
- </li>
- </ul>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-invitations">Invitations</label>
- <div class="input">
- <span class="uneditable-input" id="user-invitations">{{ user.invitations }}</span>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-quota">Quota</label>
- <div class="input">
- <div class="input-append">
- <input class="span2" id="user-quota" name="quota" value="{{ user.quota|GiB }}" type="text" readonly="readonly"/>
- <span class="add-on">GiB</span>
- </div>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-token">Token</label>
- <div class="input">
- <span class="uneditable-input" id="user-token">{{ user.auth_token }}</span>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="token-created">Token Created</label>
- <div class="input">
- <span class="uneditable-input" id="token-created">{{ user.auth_token_created }}</span>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="token-expires">Token Expires</label>
- <div class="input">
- <span class="uneditable-input" id="token-expires">{{ user.auth_token_expires }}</span>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-created">Created</label>
- <div class="input">
- <span class="uneditable-input" id="user-date-joined">{{ user.date_joined }}</span>
- </div>
- </div>
-
- <div class="clearfix">
- <label for="user-updated">Updated</label>
- <div class="input">
- <span class="uneditable-input" id="user-updated">{{ user.updated }}</span>
- </div>
- </div>
-
- <div class="actions">
- <input type="hidden" name="next" value="{{ next }}">
- <input type="hidden" name="auth" value="{{ user.auth_token }}">
- <button type="submit" class="btn primary">Verify</button>
- </div>
-
-</form>
-{% endblock body %}
from django.conf import settings
from django.conf.urls.defaults import patterns, include
+from django.core.urlresolvers import reverse
urlpatterns = patterns('astakos.im.views',
(r'^$', 'index'),
(r'^login/?$', 'index'),
-
- (r'^admin/', include('astakos.im.admin.urls')),
-
- (r'^profile/?$', 'users_profile'),
- (r'^profile/edit/?$', 'users_edit'),
-
+ (r'^profile/?$', 'edit_profile'),
+ (r'^feedback/?$', 'send_feedback'),
(r'^signup/?$', 'signup'),
- #(r'^register/(\w+)?$', 'register'),
- #(r'^signup/complete/?$', 'signup_complete'),
- #(r'^local/create/?$', 'local_create'),
+ (r'^admin/', include('astakos.im.admin.urls')),
+)
+
+urlpatterns += patterns('django.contrib.auth.views',
+ (r'^logout/?$', 'logout'),
+ (r'^password/?$', 'password_change', {'post_change_redirect':'admin'}),
)
urlpatterns += patterns('astakos.im.target',
)
if 'local' in settings.IM_MODULES:
- urlpatterns += patterns('astakos.im.views',
-# (r'^local/create/?$', 'local_create'),
- (r'^local/reclaim/?$', 'reclaim_password')
- )
urlpatterns += patterns('astakos.im.target',
(r'^local/?$', 'local.login'),
(r'^local/activate/?$', 'local.activate'),
- (r'^local/reset/?$', 'local.reset_password')
+ )
+ urlpatterns += patterns('django.contrib.auth.views',
+ (r'^local/password_reset/?$', 'password_reset',
+ {'email_template_name':'registration/password_email.txt'}),
+ (r'^local/password_reset_done/?$', 'password_reset_done'),
+ (r'^local/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
+ 'password_reset_confirm'),
+ (r'^local/password/reset/complete/$', 'password_reset_complete')
)
if settings.INVITATIONS_ENABLED:
return d.replace(tzinfo=UTC()).isoformat()
-def get_or_create_user(username, realname=None, first_name=None, last_name=None, affiliation=None, level=0, provider='local', password=None, email=None):
+def get_or_create_user(username, realname='', first_name='', last_name='', affiliation='', level=0, provider='local', password='', email=''):
"""Find or register a user into the internal database
and issue a token for subsequent requests.
"""
-
user, created = AstakosUser.objects.get_or_create(username=username,
defaults={
'is_active': False,
from django.utils.http import urlencode
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
+from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth.models import AnonymousUser
+from django.contrib.auth.decorators import login_required
+from django.contrib.sites.models import get_current_site
+from django.contrib import messages
+from django.db import transaction
+from django.contrib.auth.forms import UserCreationForm
#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.forms import ProfileForm, FeedbackForm
def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
+ """
+ Calls ``django.template.loader.render_to_string`` with an additional ``tab``
+ keyword argument and returns an ``django.http.HttpResponse`` with the
+ specified ``status``.
+ """
if tab is None:
tab = template.partition('_')[0]
kwargs.setdefault('tab', tab)
html = render_to_string(template, kwargs, context_instance=context_instance)
return HttpResponse(html, status=status)
-def requires_login(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)
- return func(request, *args)
- return wrapper
-
def index(request, template_name='index.html', extra_context={}):
- print '#', get_context(request, extra_context)
+ """
+ Renders the index (login) page
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``index.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Template:**
+
+ index.html or ``template_name`` keyword argument.
+
+ """
return render_response(template_name,
- form = LoginForm(),
+ form = AuthenticationForm(),
context_instance = get_context(request, extra_context))
def _generate_invitation_code():
def _send_invitation(baseurl, inv):
url = settings.SIGNUP_TARGET % (baseurl, inv.code, quote(baseurl))
subject = _('Invitation to Pithos')
+ site = get_current_site(request)
message = render_to_string('invitation.txt', {
'invitation': inv,
'url': url,
'baseurl': baseurl,
- 'service': settings.SERVICE_NAME,
+ 'service': site_name,
'support': settings.DEFAULT_CONTACT_EMAIL})
sender = settings.DEFAULT_FROM_EMAIL
send_mail(subject, message, sender, [inv.username])
logging.info('Sent invitation %s', inv)
-@requires_login
+@login_required
+@transaction.commit_manually
def invite(request, template_name='invitations.html', extra_context={}):
+ """
+ Allows a user to invite somebody else.
+
+ In case of GET request renders a form for providing the invitee information.
+ In case of POST checks whether the user has not run out of invitations and then
+ sends an invitation email to singup to the service.
+
+ The view uses commit_manually decorator in order to ensure the number of the
+ user invitations is going to be updated only if the email has been successfully sent.
+
+ If the user isn't logged in, redirects to settings.LOGIN_URL.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``invitations.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Template:**
+
+ invitations.html or ``template_name`` keyword argument.
+
+ **Settings:**
+
+ The view expectes the following settings are defined:
+
+ * LOGIN_URL: login uri
+ * SIGNUP_TARGET: Where users should signup with their invitation code
+ * DEFAULT_CONTACT_EMAIL: service support email
+ * DEFAULT_FROM_EMAIL: from email
+ """
status = None
message = None
inviter = request.user
if created:
inviter.invitations = max(0, inviter.invitations - 1)
inviter.save()
- status = 'success'
+ status = messages.SUCCESS
message = _('Invitation sent to %s' % username)
+ transaction.commit()
except (SMTPException, socket.error) as e:
- status = 'error'
+ status = messages.ERROR
message = getattr(e, 'strerror', '')
+ transaction.rollback()
else:
- status = 'error'
+ status = messages.ERROR
message = _('No invitations left')
-
+ messages.add_message(request, status, message)
+
if request.GET.get('format') == 'json':
sent = [{'email': inv.username,
'realname': inv.realname,
rep = {'invitations': inviter.invitations, 'sent': sent}
return HttpResponse(json.dumps(rep))
- kwargs = {'user': inviter, 'status': status, 'message': message}
+ kwargs = {'user': inviter}
context = get_context(request, extra_context, **kwargs)
return render_response(template_name,
context_instance = context)
-def _send_password(baseurl, user):
- url = settings.PASSWORD_RESET_TARGET % (baseurl,
- quote(user.username),
- quote(baseurl))
- message = render_to_string('password.txt', {
- 'user': user,
- 'url': url,
- 'baseurl': baseurl,
- 'service': settings.SERVICE_NAME,
- 'support': settings.DEFAULT_CONTACT_EMAIL})
- sender = settings.DEFAULT_FROM_EMAIL
- send_mail('Pithos password recovering', message, sender, [user.email])
- logging.info('Sent password %s', user)
-
-def reclaim_password(request, template_name='reclaim.html', extra_context={}):
- if request.method == 'GET':
- return render_response(template_name,
- context_instance = get_context(request, extra_context))
- elif request.method == 'POST':
- username = request.POST.get('username')
- try:
- user = AstakosUser.objects.get(username=username)
- try:
- _send_password(request.build_absolute_uri('/').rstrip('/'), user)
- status = 'success'
- message = _('Password reset sent to %s' % user.email)
- user.is_active = False
- user.save()
- except (SMTPException, socket.error) as e:
- status = 'error'
- name = 'strerror'
- message = getattr(e, name) if hasattr(e, name) else e
- except AstakosUser.DoesNotExist:
- status = 'error'
- message = 'Username does not exist'
-
- kwargs = {'status': status, 'message': message}
- return render_response(template_name,
- context_instance = get_context(request, extra_context, **kwargs))
-
-@requires_login
-def users_profile(request, template_name='users_profile.html', extra_context={}):
+@login_required
+def edit_profile(request, template_name='profile.html', extra_context={}):
+ """
+ Allows a user to edit his/her profile.
+
+ In case of GET request renders a form for displaying the user information.
+ In case of POST updates the user informantion.
+
+ If the user isn't logged in, redirects to settings.LOGIN_URL.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``profile.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Template:**
+
+ profile.html or ``template_name`` keyword argument.
+ """
try:
user = AstakosUser.objects.get(username=request.user)
+ form = ProfileForm(instance=user)
except AstakosUser.DoesNotExist:
token = request.GET.get('auth', None)
user = AstakosUser.objects.get(auth_token=token)
+ if request.method == 'POST':
+ form = ProfileForm(request.POST, instance=user)
+ if form.is_valid():
+ try:
+ form.save()
+ msg = _('Profile has been updated successfully')
+ messages.add_message(request, messages.SUCCESS, msg)
+ except ValueError, ve:
+ messages.add_message(request, messages.ERROR, ve)
return render_response(template_name,
+ form = form,
context_instance = get_context(request,
extra_context,
user=user))
-@requires_login
-def users_edit(request, template_name='users_profile.html', extra_context={}):
- try:
- user = AstakosUser.objects.get(username=request.user)
- except AstakosUser.DoesNotExist:
- token = request.POST.get('auth', None)
- #users = AstakosUser.objects.all()
- user = AstakosUser.objects.get(auth_token=token)
- user.first_name = request.POST.get('first_name')
- user.last_name = request.POST.get('last_name')
- user.affiliation = request.POST.get('affiliation')
- user.is_verified = True
- user.save()
- next = request.POST.get('next')
- if next:
- return redirect(next)
-
- status = 'success'
- message = _('Profile has been updated')
- return render_response(template_name,
- context_instance = get_context(request, extra_context, **kwargs))
+@transaction.commit_manually
+def signup(request, template_name='signup.html', extra_context={}, backend=None):
+ """
+ Allows a user to create a local account.
+
+ In case of GET request renders a form for providing the user information.
+ In case of POST handles the signup.
+
+ The user activation will be delegated to the backend specified by the ``backend`` keyword argument
+ if present, otherwise to the ``astakos.im.backends.InvitationBackend``
+ if settings.INVITATIONS_ENABLED is True or ``astakos.im.backends.SimpleBackend`` if not
+ (see backends);
+
+ Upon successful user creation if ``next`` url parameter is present the user is redirected there
+ otherwise renders the same page with a success message.
-def signup(request, template_name='signup.html', extra_context={}, backend=None, success_url = None):
+ On unsuccessful creation, renders the same page with an error message.
+
+ The view uses commit_manually decorator in order to ensure the user will be created
+ only if the procedure has been completed successfully.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``signup.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Template:**
+
+ signup.html or ``template_name`` keyword argument.
+ """
if not backend:
- backend = get_backend()
- if request.method == 'GET':
- try:
- form = backend.get_signup_form(request)
- return render_response(template_name,
- form=form,
- context_instance = get_context(request, extra_context))
- except Exception, e:
- return _on_failure(e, template_name=template_name)
- elif request.method == 'POST':
- try:
- form = backend.get_signup_form(request)
- if not form.is_valid():
- return render_response(template_name,
- form = form,
- context_instance = get_context(request, extra_context))
- status, message = backend.signup(request, form, success_url)
- next = request.POST.get('next')
- if next:
- return redirect(next)
- return _info(status, message)
- except Exception, e:
- return _on_failure(e, template_name=template_name)
-
-def _info(status, message, template_name='base.html'):
- html = render_to_string(template_name, {
- 'status': status,
- 'message': message})
- response = HttpResponse(html)
- return response
+ backend = get_backend()
+ try:
+ form = backend.get_signup_form(request)
+ if request.method == 'POST':
+ if form.is_valid():
+ status, message = backend.signup(request)
+ # rollback incase of error
+ if status == messages.ERROR:
+ transaction.rollback()
+ else:
+ transaction.commit()
+ next = request.POST.get('next')
+ if next:
+ return redirect(next)
+ messages.add_message(request, status, message)
+ except (Invitation.DoesNotExist), e:
+ messages.add_message(request, messages.ERROR, e)
+ return render_response(template_name,
+ form = form if 'form' in locals() else UserCreationForm(),
+ context_instance=get_context(request, extra_context))
-def _on_success(message, template_name='base.html'):
- return _info('success', message, template_name)
+@login_required
+def send_feedback(request, template_name='feedback.html', email_template_name='feedback_mail.txt', extra_context={}):
+ """
+ Allows a user to send feedback.
+
+ In case of GET request renders a form for providing the feedback information.
+ In case of POST sends an email to support team.
+
+ If the user isn't logged in, redirects to settings.LOGIN_URL.
+
+ **Arguments**
+
+ ``template_name``
+ A custom template to use. This is optional; if not specified,
+ this will default to ``feedback.html``.
+
+ ``extra_context``
+ An dictionary of variables to add to the template context.
+
+ **Template:**
-def _on_failure(message, template_name='base.html'):
- return _info('error', message, template_name)
+ signup.html or ``template_name`` keyword argument.
+
+ **Settings:**
+
+ * FEEDBACK_CONTACT_EMAIL: List of feedback recipients
+ """
+ if request.method == 'GET':
+ form = FeedbackForm()
+ if request.method == 'POST':
+ if not request.user:
+ return HttpResponse('Unauthorized', status=401)
+
+ form = FeedbackForm(request.POST)
+ if form.is_valid():
+ subject = _("Feedback from Okeanos")
+ from_email = request.user.email
+ recipient_list = [settings.FEEDBACK_CONTACT_EMAIL]
+ content = render_to_string(email_template_name, {
+ 'message': form.cleaned_data('feedback_msg'),
+ 'data': form.cleaned_data('feedback_data'),
+ 'request': request})
+
+ send_mail(subject, content, from_email, recipient_list)
+
+ resp = json.dumps({'status': 'send'})
+ return HttpResponse(resp)
+ return render_response(template_name,
+ form = form,
+ context_instance = get_context(request, extra_context))
\ No newline at end of file
+++ /dev/null
-# 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 time import time, mktime
-from urllib import quote, unquote
-
-from astakos.im.models import AstakosUser
-
-def get_user_from_token(token):
- try:
- return AstakosUser.objects.get(auth_token=token)
- except AstakosUser.DoesNotExist:
- return None
-
-class AuthMiddleware(object):
- def process_request(self, request):
- request.user = None
- request.user_uniq = None
-
- # Try to find token in a parameter, in a request header, or in a cookie.
- user = get_user_from_token(request.GET.get('X-Auth-Token'))
- if not user:
- user = get_user_from_token(request.META.get('HTTP_X_AUTH_TOKEN'))
- if not user:
- # Back from an im login target.
- if request.GET.get('user', None):
- token = request.GET.get('token', None)
- if token:
- request.set_auth_cookie = True
- user = get_user_from_token(token)
- if not user:
- cookie_value = unquote(request.COOKIES.get('_pithos2_a', ''))
- if cookie_value and '|' in cookie_value:
- token = cookie_value.split('|', 1)[1]
- user = get_user_from_token(token)
- if not user:
- return
-
- # Check if the is active.
- if not user.is_active:
- return
-
- # Check if the token has expired.
- if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
- return
-
- request.user = user
- request.user_uniq = user.username
-
- def process_response(self, request, response):
- if getattr(request, 'user', None) and getattr(request, 'set_auth_cookie', False):
- user_profile = request.user.get_profile()
- expire_fmt = user_profile.auth_token_expires.strftime('%a, %d-%b-%Y %H:%M:%S %Z')
- cookie_value = quote(request.user.uniq + '|' + user_profile.auth_token)
- response.set_cookie('_pithos2_a', value=cookie_value, expires=expire_fmt, path='/')
- return response
\ No newline at end of file
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
'astakos.middleware.LoggingConfigMiddleware',
- 'astakos.middleware.SecureMiddleware',
- 'astakos.middleware.AuthMiddleware'
+ 'astakos.middleware.SecureMiddleware'
)
ROOT_URLCONF = 'astakos.urls'
'astakos.im',
'south',
'django.contrib.auth',
- 'django.contrib.contenttypes'
+ 'django.contrib.contenttypes',
+ 'django.contrib.messages',
+ 'django.contrib.sites',
+ 'django.contrib.sessions'
)
-TEMPLATE_CONTEXT_PROCESSORS = ('astakos.im.context_processors.im_modules',
+TEMPLATE_CONTEXT_PROCESSORS = ('django.contrib.messages.context_processors.messages',
+ 'django.contrib.auth.context_processors.auth',
+ 'astakos.im.context_processors.im_modules',
'astakos.im.context_processors.next',
- 'astakos.im.context_processors.code',)
+ 'astakos.im.context_processors.code',
+ 'astakos.im.context_processors.invitations')
AUTHENTICATION_BACKENDS = (
- 'astakos.im.auth_backends.AstakosUserModelBackend',
+ 'astakos.im.auth_backends.AstakosUserModelCredentialsBackend',
+ 'astakos.im.auth_backends.AstakosUserModelTokenBackend',
)
-CUSTOM_USER_MODEL = 'astakos.im.AstakosUser'
\ No newline at end of file
+CUSTOM_USER_MODEL = 'astakos.im.AstakosUser'
+
+SITE_ID = 1
\ No newline at end of file
# Address to use for outgoing emails
DEFAULT_FROM_EMAIL = '%s <no-reply@grnet.gr>' %SERVICE_NAME
DEFAULT_CONTACT_EMAIL = 'support@%s.grnet.gr' %SERVICE_NAME.lower()
+FEEDBACK_CONTACT_EMAIL = DEFAULT_CONTACT_EMAIL
# Where users should signup with their invitation code
SIGNUP_TARGET = '%s/im/signup/?code=%d&next=%s'
# Where users should activate their local account
ACTIVATION_LOGIN_TARGET = '%s/im/local/activate/?auth=%s&next=%s'
-# Where users should reset their local password
-PASSWORD_RESET_TARGET = '%s/im/local/reset/?username=%s&next=%s'
+## Where users should reset their local password
+#PASSWORD_RESET_TARGET = '%s/im/local/reset/?username=%s&next=%s'
# Identity Management enabled modules
IM_MODULES = ['local', 'twitter', 'shibboleth']
# Force user profile verification
-FORCE_PROFILE_UPDATE = False
+FORCE_PROFILE_UPDATE = True
#Enable invitations
-INVITATIONS_ENABLED = True
\ No newline at end of file
+INVITATIONS_ENABLED = True
+
+# The URL where requests are redirected for login, especially when using the login_required() decorator.
+LOGIN_URL = '/im'
\ No newline at end of file
Administrator Guide
===================
+manage syncdb
+change django_site record
+migrate
+create twitter application
\ No newline at end of file
--- /dev/null
+Backends
+==============
+
+.. automodule:: astakos.im.backends
+ :show-inheritance:
+ :members:
+ :undoc-members:
-Developer Guide
-===============
+Astakos Developer Guide
+=======================
+
+Introduction
+------------
+
+Astakos is a identity management service implemented by GRNET (http://www.grnet.gr). Users can create and manage their account, invite others and send feedback for GRNET services. During the account creation the user can select against which provider wants to authenticate:
+
+* Astakos
+* Twitter
+* Shibboleth
+
+Astakos provides also an administrative interface for managing user accounts.
+
+Astakos is build over django and extends its authentication mechanism.
+
+This document's goals are:
+
+* Define the Astakos ReST API that allows the GRNET services to retrieve user information via HTTP calls
+* Describe the Astakos views and provide guidelines for a developer to extend them
+
+The present document is meant to be read alongside the Django documentation. Thus, it is suggested that the reader is familiar with associated technologies.
+
+Document Revisions
+^^^^^^^^^^^^^^^^^^
+
+========================= ================================
+Revision Description
+========================= ================================
+0.1 (Jub 24, 2012) Initial release.
+========================= ================================
+
+Astakos Users and Authentication
+--------------------------------
+
+Astakos extends django User model.
+
+Each user is uniquely identified by the ``username`` field. An astakos user instance is assigned also with a ``auth_token`` field used by the astakos clients to authenticate a user. All API requests require a token.
+
+Logged on users can perform a number of actions:
+
+* access and edit their profile via: ``https://hostname/im/profile``.
+* change their password via: ``https://hostname/im/password``
+* invite somebody else via: ``https://hostname/im/invite``
+* send feedback for grnet services via: ``https://hostname/im/send_feedback``
+* logout via: ``https://hostname/im/logout``
+
+User entries can also be modified/added via the management interface available at ``https://hostname/im/admin``.
+
+A superuser account can be created the first time you run the manage.py syncdb django command. At a later date, the manage.py createsuperuser command line utility can be used.
+
+Astakos is also compatible with Twitter and Shibboleth (http://shibboleth.internet2.edu/). The connection between Twitter and Astakos is done by ``https://hostname/im/target/twitter/login``. The connection between Shibboleth and Astakos is done by ``https://hostname/im/target/shibboleth/login``. An application that wishes to connect to Astakos, but does not have a token, should redirect the user to ``https://hostname/im/login``.
+
+The login URI accepts the following parameters:
+
+====================== =========================
+Request Parameter Name Value
+====================== =========================
+next The URI to redirect to when the process is finished
+renew Force token renewal (no value parameter)
+====================== =========================
+
+In case the user wants to authenticate via Astakos fills the login form and post it to ``https://hostname/im/local/login``.
+
+Otherwise (the user selects a third party authentication) the login process starts by redirecting the user to an external URI (controlled by the third party), where the actual authentication credentials are entered. Then, the user is redirected back to the login URI, with various identification information in the request headers.
+
+If the user does not exist in the database, Astakos adds the user and creates a random token. If the user exists, the token has not expired and ``renew`` is not set, the existing token is reused. Finally, the login URI redirects to the URI provided with ``next``, adding the ``user`` and ``token`` parameters, which contain the ``Uniq`` and ``Token`` fields respectively.
+
+The Astakos API
+---------------
+
+Authenticate
+^^^^^^^^^^^^
+
+==================================== ========= ==================
+Uri Method Description
+==================================== ========= ==================
+``https://hostname/im/authenticate`` GET Authenticate user using token
+==================================== ========= ==================
+
+|
+
+==================== ===========================
+Request Header Name Value
+==================== ===========================
+X-Auth-Token Authentication token
+==================== ===========================
+
+Extended information on the user serialized in the json format will be returned:
+
+=========================== ============================
+Name Description
+=========================== ============================
+uniq User uniq identifier
+auth_token Authentication token
+auth_token_expires Token expiration date
+auth_token_created Token creation date
+=========================== ============================
+
+Example reply:
+
+::
+
+ {"uniq": "admin",
+ "auth_token": "0000",
+ "auth_token_expires": "Tue, 11-Sep-2012 09:17:14 ",
+ "auth_token_created": "Sun, 11-Sep-2011 09:17:14 "}
+
+|
+
+========================= =====================
+Return Code Description
+========================= =====================
+204 (No Content) The request succeeded
+400 (Bad Request) The request is invalid
+401 (Unauthorized) Missing token or inactive user
+503 (Service Unavailable) The request cannot be completed because of an internal error
+========================= =====================
+
+The Astakos views
+-----------------
+
+Astakos incorporates the ``django.contrib.auth`` mechanism for handling user login,
+logout, password change and password reset.
+
+============================== =====================
+Uri view
+============================== =====================
--- /dev/null
+Forms
+==============
+
+.. automodule:: astakos.im.forms
+ :show-inheritance:
devguide
adminguide
+ views
+ models
+ forms
+ backends
Indices and tables
==================
--- /dev/null
+Models
+==============
+
+.. automodule:: astakos.im.models
+ :show-inheritance:
+ :members:
+ :undoc-members:
--- /dev/null
+Views
+==============
+
+.. automodule:: astakos.im.views
+ :show-inheritance:
+ :members:
+ :undoc-members:
+
+.. automodule:: astakos.im.admin.views
+ :show-inheritance:
+ :members:
+ :undoc-members: