-add docs
authorSofia Papagiannaki <papagian@gmail.com>
Sun, 22 Jan 2012 19:12:46 +0000 (21:12 +0200)
committerSofia Papagiannaki <papagian@gmail.com>
Sun, 22 Jan 2012 19:12:46 +0000 (21:12 +0200)
-incorporate CsrfViewMiddleware
-change tamplates to render forms
-change email template location
-api.activate() returns only information about token
- In prepare_response, instead of setting the cookie, log the user in
- use django.contrib.auth.views for reseting and changing user password

Ref: #1874

52 files changed:
astakos/im/admin/forms.py [new file with mode: 0644]
astakos/im/admin/templates/pending_users.html
astakos/im/admin/templates/users_create.html
astakos/im/admin/templates/users_info.html
astakos/im/admin/templates/welcome_email.txt [moved from astakos/im/templates/welcome.txt with 85% similarity]
astakos/im/admin/views.py
astakos/im/api.py
astakos/im/auth_backends.py
astakos/im/backends/__init__.py
astakos/im/backends/invitations.py [deleted file]
astakos/im/backends/simple.py [deleted file]
astakos/im/context_processors.py
astakos/im/fixtures/auth_test_data.json
astakos/im/forms.py
astakos/im/models.py
astakos/im/target/invitation.py
astakos/im/target/local.py
astakos/im/target/shibboleth.py
astakos/im/target/twitter.py
astakos/im/target/util.py
astakos/im/templates/account_base.html [new file with mode: 0644]
astakos/im/templates/activation_email.txt [moved from astakos/im/templates/activation.txt with 83% similarity]
astakos/im/templates/base.html
astakos/im/templates/feedback.html [new file with mode: 0644]
astakos/im/templates/feedback_mail.txt [new file with mode: 0644]
astakos/im/templates/index.html
astakos/im/templates/invitations.html
astakos/im/templates/profile.html [new file with mode: 0644]
astakos/im/templates/reclaim.html [deleted file]
astakos/im/templates/register.html
astakos/im/templates/registration/logged_out.html [new file with mode: 0644]
astakos/im/templates/registration/password_change_form.html [moved from astakos/im/templates/reset.html with 51% similarity]
astakos/im/templates/registration/password_email.txt [moved from astakos/im/templates/password.txt with 74% similarity]
astakos/im/templates/registration/password_reset_complete.html [new file with mode: 0644]
astakos/im/templates/registration/password_reset_confirm.html [new file with mode: 0644]
astakos/im/templates/registration/password_reset_done.html [new file with mode: 0644]
astakos/im/templates/registration/password_reset_form.html [new file with mode: 0644]
astakos/im/templates/signup.html
astakos/im/templates/users_profile.html [deleted file]
astakos/im/urls.py
astakos/im/util.py
astakos/im/views.py
astakos/middleware/auth.py [deleted file]
astakos/settings.d/00-apps.conf
astakos/settings.d/20-im.conf
docs/source/adminguide.rst
docs/source/backends.rst [new file with mode: 0644]
docs/source/devguide.rst
docs/source/forms.rst [new file with mode: 0644]
docs/source/index.rst
docs/source/models.rst [new file with mode: 0644]
docs/source/views.rst [new file with mode: 0644]

diff --git a/astakos/im/admin/forms.py b/astakos/im/admin/forms.py
new file mode 100644 (file)
index 0000000..cd83b78
--- /dev/null
@@ -0,0 +1,62 @@
+# 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
index fcd1a73..53579cf 100644 (file)
@@ -36,7 +36,7 @@
       <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>
index 079e972..bb0effe 100644 (file)
@@ -2,7 +2,7 @@
 
 {% 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">
index fc89252..d7919e5 100644 (file)
@@ -4,119 +4,8 @@
 
 {% 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>
similarity index 85%
rename from astakos/im/templates/welcome.txt
rename to astakos/im/admin/templates/welcome_email.txt
index e542cd8..7d54d2a 100644 (file)
@@ -2,7 +2,7 @@
 
 Αγαπητέ/η {{ user.realname }},
 
-Ο λογαρισμός σας για την υπηρεσία {{ service }} της ΕΔΕΤκατά την Alpha (πιλοτική)
+Ο λογαρισμός σας για την υπηρεσία {{ site_name }} της ΕΔΕΤκατά την Alpha (πιλοτική)
 φάση λειτουργίας της έχει ενεργοποιηθεί.
 
 Για να συνδεθείτε, χρησιμοποιήστε τον παρακάτω σύνδεσμο:
@@ -16,7 +16,7 @@
 υπηρεσίας, δεν αποκλείεται να εμφανιστούν προβλήματα στο λογισμικό
 διαχείρισης ή η υπηρεσία να μην είναι διαθέσιμη κατά διαστήματα. Για
 αυτό το λόγο, σας παρακαλούμε να μη μεταφέρετε ακόμη σημαντικά κομμάτια
-της δουλειάς σας στην υπηρεσία {{ service }}. Επίσης, παρακαλούμε να έχετε
+της δουλειάς σας στην υπηρεσία {{ site_name }}. Επίσης, παρακαλούμε να έχετε
 υπόψη σας ότι όλα τα δεδομένα, θα διαγραφούν κατά τη μετάβαση από την
 έκδοση Alpha στην έκδοση Beta. Θα υπάρξει έγκαιρη ειδοποίησή σας πριν
 από τη μετάβαση αυτή.
@@ -30,7 +30,7 @@
 Για όποιες παρατηρήσεις ή προβλήματα στη λειτουργεία της υπηρεσίας μπορείτε να
 απευθυνθείτε στο {{ support }}.
 
-Σας ευχαριστούμε πολύ για τη συμμετοχή στην Alpha λειτουργία του {{ service }}.
+Σας ευχαριστούμε πολύ για τη συμμετοχή στην Alpha λειτουργία του {{ site_name }}.
 
 Με εκτίμηση,
 
@@ -40,7 +40,7 @@
 
 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:
@@ -53,7 +53,7 @@ This service is, for a few weeks, in Alpha. Although every care has been
 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.
 
@@ -65,6 +65,6 @@ 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
index a8d0b83..bb3449d 100644 (file)
@@ -53,19 +53,27 @@ from django.template.loader import render_to_string
 from django.utils.http import urlencode
 from django.utils.translation import ugettext as _
 from django.core.urlresolvers import reverse
+from 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)
@@ -76,6 +84,31 @@ def requires_admin(func):
 
 @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()
@@ -85,11 +118,43 @@ def admin(request, template_name='admin.html', extra_context={}):
     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', '')
@@ -115,46 +180,102 @@ def users_list(request, template_name='users_list.html', extra_context={}):
               '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)
@@ -183,34 +304,70 @@ def pending_users(request, template_name='pending_users.html', extra_context={})
               '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)
@@ -230,13 +387,40 @@ def users_activate(request, user_id, template_name='pending_users.html', extra_c
               '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', '')
@@ -263,10 +447,13 @@ def invitations_list(request, template_name='invitations_list.html', extra_conte
               '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'
@@ -299,6 +486,9 @@ def invitations_export(request):
 
 @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'
@@ -327,6 +517,22 @@ def users_export(request):
 
 @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))
@@ -338,7 +544,7 @@ def users_create(request, template_name='users_create.html', 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()
index 118d46c..17db824 100644 (file)
@@ -82,11 +82,10 @@ def authenticate(request):
         
         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
index 891d101..96e546a 100644 (file)
@@ -5,7 +5,7 @@ from django.contrib.auth.backends import ModelBackend
 
 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)
@@ -19,7 +19,7 @@ class AstakosUserModelBackend(ModelBackend):
             return AstakosUser.objects.get(pk=user_id)
         except AstakosUser.DoesNotExist:
             return None
-
+    
     #@property
     #def user_class(self):
     #    if not hasattr(self, '_user_class'):
@@ -28,4 +28,19 @@ class AstakosUserModelBackend(ModelBackend):
     #        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
index 14a55a5..07e97bd 100644 (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:
@@ -51,4 +68,139 @@ def get_backend():
         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
diff --git a/astakos/im/backends/invitations.py b/astakos/im/backends/invitations.py
deleted file mode 100644 (file)
index ad738d4..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-# 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
diff --git a/astakos/im/backends/simple.py b/astakos/im/backends/simple.py
deleted file mode 100644 (file)
index 40b3f15..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-# 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
index 89389d5..c9c0c54 100644 (file)
@@ -41,4 +41,7 @@ def next(request):
 
 def code(request):
     return {'code' : request.GET.get('code', '')}
+
+def invitations(request):
+    return {'invitations_enabled' :settings.INVITATIONS_ENABLED}
     
\ No newline at end of file
index 6985104..fc44863 100644 (file)
@@ -1,7 +1,7 @@
 [
     {
         "model": "im.AstakosUser",
-        "pk": 1,
+        "pk": 2,
         "fields": {
             "username": "test",
             "level": 0,
@@ -15,7 +15,7 @@
     },
     {
         "model": "im.AstakosUser",
-        "pk": 2,
+        "pk": 3,
         "fields": {
             "username": "verigak",
             "level": 1,
@@ -30,7 +30,7 @@
     },
     {
         "model": "im.AstakosUser",
-        "pk": 3,
+        "pk": 4,
         "fields": {
             "username": "chazapis",
             "level": 1,
@@ -44,7 +44,7 @@
     },
     {
         "model": "im.AstakosUser",
-        "pk": 4,
+        "pk": 5,
         "fields": {
             "username": "gtsouk",
             "level": 1,
@@ -58,7 +58,7 @@
     },
     {
         "model": "im.AstakosUser",
-        "pk": 5,
+        "pk": 6,
         "fields": {
             "username": "papagian",
             "level": 1,
@@ -72,7 +72,7 @@
     },
     {
         "model": "im.AstakosUser",
-        "pk": 6,
+        "pk": 7,
         "fields": {
             "username": "louridas",
             "level": 1,
@@ -86,7 +86,7 @@
     },
     {
         "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,
index 9a71bda..a869324 100644 (file)
 
 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'))
     
@@ -105,13 +117,47 @@ class InvitedRegisterForm(RegisterForm):
         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
index 68936ec..65b1756 100644 (file)
@@ -45,6 +45,9 @@ from django.contrib.auth.models import User, UserManager
 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()
     
@@ -61,6 +64,7 @@ class AstakosUser(User):
     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):
@@ -117,6 +121,9 @@ class AstakosUser(User):
         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)
index af27353..4976e73 100644 (file)
@@ -37,6 +37,7 @@ from datetime import datetime
 
 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
@@ -56,10 +57,12 @@ def login(request):
         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)
index 05064bc..63db40b 100644 (file)
@@ -37,11 +37,12 @@ from django.template.loader import render_to_string
 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
 
@@ -51,26 +52,23 @@ def login(request, on_failure='index.html'):
     """
     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')
@@ -87,47 +85,3 @@ def activate(request):
     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)
index d35be68..98bc4c2 100644 (file)
@@ -33,6 +33,7 @@
 
 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
@@ -68,7 +69,10 @@ def login(request):
     
     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)
index d9c5fef..a85a6e0 100644 (file)
@@ -39,6 +39,7 @@ import urlparse
 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
@@ -114,8 +115,11 @@ def authenticated(request):
     # 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'))
index 263bfcd..c33f234 100644 (file)
@@ -40,6 +40,7 @@ from django.http import HttpResponse
 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
@@ -69,13 +70,12 @@ def prepare_response(request, user, next='', renew=False):
         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
diff --git a/astakos/im/templates/account_base.html b/astakos/im/templates/account_base.html
new file mode 100644 (file)
index 0000000..efc606a
--- /dev/null
@@ -0,0 +1,25 @@
+{% 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 %}
similarity index 83%
rename from astakos/im/templates/activation.txt
rename to astakos/im/templates/activation_email.txt
index 19f722a..3cc916f 100644 (file)
@@ -3,7 +3,7 @@
 Αγαπητέ/η {{ user.realname }},
 
 Έπειτα από αίτημα σας έχει δημιουργηθεί ο λογαρισμός σας  
-για την υπηρεσία {{ service }} της ΕΔΕΤ κατά την Alpha (πιλοτική) φάση λειτουργίας της.
+για την υπηρεσία {{ site_name }} της ΕΔΕΤ κατά την Alpha (πιλοτική) φάση λειτουργίας της.
 
 Για να τον ενεργοποιήσετε, χρησιμοποιήστε τον παρακάτω σύνδεσμο:
 
@@ -16,7 +16,7 @@
 υπηρεσίας, δεν αποκλείεται να εμφανιστούν προβλήματα στο λογισμικό
 διαχείρισης ή η υπηρεσία να μην είναι διαθέσιμη κατά διαστήματα. Για
 αυτό το λόγο, σας παρακαλούμε να μη μεταφέρετε ακόμη σημαντικά κομμάτια
-της δουλειάς σας στην υπηρεσία {{ service }}. Επίσης, παρακαλούμε να έχετε
+της δουλειάς σας στην υπηρεσία {{ site_name }}. Επίσης, παρακαλούμε να έχετε
 υπόψη σας ότι όλα τα δεδομένα, θα διαγραφούν κατά τη μετάβαση από την
 έκδοση Alpha στην έκδοση Beta. Θα υπάρξει έγκαιρη ειδοποίησή σας πριν
 από τη μετάβαση αυτή.
@@ -30,7 +30,7 @@
 Για όποιες παρατηρήσεις ή προβλήματα στη λειτουργεία της υπηρεσίας μπορείτε να
 απευθυνθείτε στο {{ support }}.
 
-Σας ευχαριστούμε πολύ για τη συμμετοχή στην Alpha λειτουργία του {{ service }}.
+Σας ευχαριστούμε πολύ για τη συμμετοχή στην Alpha λειτουργία του {{ site_name }}.
 
 Με εκτίμηση,
 
@@ -40,7 +40,7 @@
 
 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:
@@ -53,7 +53,7 @@ This service is, for a few weeks, in Alpha. Although every care has been
 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.
 
@@ -65,6 +65,6 @@ 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
index f43fbaf..33d79b0 100644 (file)
@@ -2,7 +2,7 @@
 <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 %}
diff --git a/astakos/im/templates/feedback.html b/astakos/im/templates/feedback.html
new file mode 100644 (file)
index 0000000..8b6df4f
--- /dev/null
@@ -0,0 +1,16 @@
+{% 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 %}
diff --git a/astakos/im/templates/feedback_mail.txt b/astakos/im/templates/feedback_mail.txt
new file mode 100644 (file)
index 0000000..77a55f1
--- /dev/null
@@ -0,0 +1,10 @@
+Feedback message:
+{{ message }}
+
+User info:
+ID: {{ request.user.id }}
+Email: {{ request.user.uniq }}
+
+User application data:
+{{ data|safe }}
+
index 736d4ad..1a3cf7a 100644 (file)
     {% 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="">
index d1eca50..46e96a4 100644 (file)
@@ -1,25 +1,22 @@
-<!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">
@@ -40,6 +37,4 @@
     </div>
   </form>
   {% endif %}
-</div>
-</body>
-</html>
+{% endblock body %}
diff --git a/astakos/im/templates/profile.html b/astakos/im/templates/profile.html
new file mode 100644 (file)
index 0000000..e0375b1
--- /dev/null
@@ -0,0 +1,17 @@
+{% 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 %}
diff --git a/astakos/im/templates/reclaim.html b/astakos/im/templates/reclaim.html
deleted file mode 100644 (file)
index 5bb7e11..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-{% 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 %}
index 362e9b5..635ceb6 100644 (file)
@@ -5,7 +5,7 @@
 {% 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>
diff --git a/astakos/im/templates/registration/logged_out.html b/astakos/im/templates/registration/logged_out.html
new file mode 100644 (file)
index 0000000..84ddcf3
--- /dev/null
@@ -0,0 +1,6 @@
+{% 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%}
@@ -1,16 +1,9 @@
-{% 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">
similarity index 74%
rename from astakos/im/templates/password.txt
rename to astakos/im/templates/registration/password_email.txt
index e3f4e32..541bec8 100644 (file)
@@ -1,12 +1,9 @@
 --- 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 }}/, αφού
 έχετε ενεργοποιήσει την πρόσκλησή σας.
 
 Για όποιες παρατηρήσεις ή προβλήματα στη λειτουργεία της υπηρεσίας μπορείτε να
@@ -29,7 +26,7 @@
 λειτουργικότητα και την αξιοπιστία της καινοτομικής αυτής υπηρεσίας.
 
 
-Σας ευχαριστούμε πολύ για τη συμμετοχή στην 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:
@@ -55,18 +50,16 @@ This service is, for a few weeks, in Alpha. Although every care has been
 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
diff --git a/astakos/im/templates/registration/password_reset_complete.html b/astakos/im/templates/registration/password_reset_complete.html
new file mode 100644 (file)
index 0000000..b303311
--- /dev/null
@@ -0,0 +1,6 @@
+{% extends 'base.html'%}
+
+{% block title%}
+        <h2>Password reset</h2>
+        <p>Password reset successfully</p>
+{% endblock title%}
diff --git a/astakos/im/templates/registration/password_reset_confirm.html b/astakos/im/templates/registration/password_reset_confirm.html
new file mode 100644 (file)
index 0000000..b8f37a0
--- /dev/null
@@ -0,0 +1,18 @@
+{% 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 %}
diff --git a/astakos/im/templates/registration/password_reset_done.html b/astakos/im/templates/registration/password_reset_done.html
new file mode 100644 (file)
index 0000000..95334dd
--- /dev/null
@@ -0,0 +1,6 @@
+{% extends 'base.html'%}
+
+{% block title%}
+        <h2>Password reset</h2>
+        <p>E-mail sent</p>
+{% endblock title%}
diff --git a/astakos/im/templates/registration/password_reset_form.html b/astakos/im/templates/registration/password_reset_form.html
new file mode 100644 (file)
index 0000000..fd98c94
--- /dev/null
@@ -0,0 +1,14 @@
+{% 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 %}
index 0e48233..01f162e 100644 (file)
@@ -10,7 +10,7 @@
     {% 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="">
diff --git a/astakos/im/templates/users_profile.html b/astakos/im/templates/users_profile.html
deleted file mode 100644 (file)
index 90b0f87..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-{% 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 %}
index 595f07f..b7b1b4f 100644 (file)
 
 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',
@@ -59,14 +59,17 @@ urlpatterns += patterns('',
 )
 
 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:
index 385460d..6f0ee22 100644 (file)
@@ -54,11 +54,10 @@ def isoformat(d):
 
    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,
index 1e29416..ba563ec 100644 (file)
@@ -54,35 +54,52 @@ from django.shortcuts import render_to_response
 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():
@@ -97,18 +114,54 @@ 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
@@ -129,15 +182,18 @@ def invite(request, template_name='invitations.html', extra_context={}):
                 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,
@@ -146,120 +202,158 @@ def invite(request, template_name='invitations.html', extra_context={}):
         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
diff --git a/astakos/middleware/auth.py b/astakos/middleware/auth.py
deleted file mode 100644 (file)
index f99d3e1..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-# 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
index 2dc5b2e..fa8f787 100644 (file)
@@ -7,9 +7,12 @@ TEMPLATE_LOADERS = (
 
 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'
@@ -25,15 +28,24 @@ INSTALLED_APPS = (
     '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
index f392a41..979c87a 100644 (file)
@@ -25,6 +25,7 @@ SERVICE_NAME = 'Astakos'
 # 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'
@@ -32,14 +33,17 @@ 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
index 648df1c..334fa46 100644 (file)
@@ -1,2 +1,6 @@
 Administrator Guide
 ===================
+manage syncdb
+change django_site record
+migrate
+create twitter application
\ No newline at end of file
diff --git a/docs/source/backends.rst b/docs/source/backends.rst
new file mode 100644 (file)
index 0000000..f103e87
--- /dev/null
@@ -0,0 +1,7 @@
+Backends
+==============
+
+.. automodule:: astakos.im.backends
+   :show-inheritance:
+   :members:
+   :undoc-members:
index cf18c40..2d43894 100644 (file)
@@ -1,2 +1,128 @@
-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
+==============================  =====================
diff --git a/docs/source/forms.rst b/docs/source/forms.rst
new file mode 100644 (file)
index 0000000..b9369d9
--- /dev/null
@@ -0,0 +1,5 @@
+Forms
+==============
+
+.. automodule:: astakos.im.forms
+   :show-inheritance:
index a759fd2..3272bd2 100644 (file)
@@ -8,6 +8,10 @@ Contents:
    
    devguide
    adminguide
+   views
+   models
+   forms
+   backends
 
 Indices and tables
 ==================
diff --git a/docs/source/models.rst b/docs/source/models.rst
new file mode 100644 (file)
index 0000000..d458b25
--- /dev/null
@@ -0,0 +1,7 @@
+Models
+==============
+
+.. automodule:: astakos.im.models
+   :show-inheritance:
+   :members:
+   :undoc-members:
diff --git a/docs/source/views.rst b/docs/source/views.rst
new file mode 100644 (file)
index 0000000..9b24ac3
--- /dev/null
@@ -0,0 +1,12 @@
+Views
+==============
+
+.. automodule:: astakos.im.views
+   :show-inheritance:
+   :members:
+   :undoc-members:
+
+.. automodule:: astakos.im.admin.views
+   :show-inheritance:
+   :members:
+   :undoc-members: