Commits

Jérémie Ducastel committed 0b07f6d Draft

WIP players > users

  • Participants
  • Parent commits f706c30

Comments (0)

Files changed (34)

 from google.appengine.ext import ndb
 from google.appengine.api import users as gusers
 
+from users.models import User
+
 # exceptions
 
 # class et fonctions
 class Challenge(ndb.Model):
     """ challenges issued, may be refused """
+    challenger = ndb.KeyProperty(User)
+    challengee = ndb.KeyProperty(User)
 
 
 class Map(ndb.Model):
 # @author J.Ducastel <nospam0@ducastel.name>
 
 # imports
+from google.appengine.api import users as gusers
+
 from django import forms
+from django.http import HttpResponse, HttpResponseRedirect
+from django.core.urlresolvers import reverse
+
+from lsnrevival.players.models import Player
+from lsnrevival.players.widgets import ChoiceSubmitWidget
 
 # exceptions
 
 # class et fonctions
+
 class RegisterForm(forms.Form):
-    """ player creation / registering form """
 
+    submit_CHOICES = (
+        ('google', 'Register with google'),
+    )
+    submit = forms.ChoiceField(submit_CHOICES, widget=ChoiceSubmitWidget)
+
+    def get_response(self):
+        if self.cleaned_data.get('submit') == 'google':
+            return HttpResponseRedirect(gusers.create_login_url(reverse('users:register_google')))
 
 class LoginForm(forms.Form):
-    """ player login form """
+
+    submit_CHOICES = (
+        ('google', 'Log in with google'),
+    )
+    submit = forms.ChoiceField(submit_CHOICES, widget=ChoiceSubmitWidget)
+
+    def get_response(self):
+        if self.cleaned_data.get('submit') == 'google':
+            return HttpResponseRedirect(gusers.create_login_url(reverse('users:login_google')))
+
+# @deprecated
+class RegisterGoogleForm(forms.Form):
+    """ register a user linked to a google account """
+
+    # don't know why but using reverse crashes imports
+    url = '/users/register/google/' # reverse('users:register_google')
+
+    submit_CHOICES = (('google', 'Register with google'),)
+    submit = forms.ChoiceField(choices=submit_CHOICES, widget=ChoiceSubmitWidget)
+
+# @deprecated
+class LoginGoogleForm(forms.Form):
+    """ log in a user linked to a google account """
+
+    # don't know why but using reverse crashes imports
+    url = '/users/login/google/' # reverse('users:register_google')
+
+    submit_CHOICES = (('google', 'Login with google'),)
+    submit = forms.ChoiceField(choices=submit_CHOICES, widget=ChoiceSubmitWidget)
 
 
 class PlayerSettingsForm(forms.Form):
 # @author J.Ducastel <nospam0@ducastel.name>
 
 # imports
+from django.conf.urls.defaults import *
 
 # exceptions
 
-# class et fonctions
+# classes et fonctions
 
-#end
+# urls
+
+urlpatterns = patterns('budjinn.users.views',
+    # log in processing
+    url(r'^login/$', 'login', name='login'),
+    # generic register processing
+    url(r'^register/$', 'register', name='register'),
+    # google login/ register processing
+    url(r'^register/google/$', 'register_google', name='register_google'),
+    url(r'^login/google/$', 'login_google', name='login_google'),
+    # email register processing
+    #url(r'^register/$', 'register_email', name='register_email'),
+    # logout processing
+    # url(r'^logout/$', 'logout', name='logout'),
+    # editing own email account
+    # url(r'^change/$', 'change_own', name='change_own')
+)
-# -*- coding: utf-8 -*-
-"""
-pure player-related views
-"""
-##
-# @author J.Ducastel <nospam0@ducastel.name>
-
-# imports
-
-# exceptions
-
-# class et fonctions
-
-def register(request):
-    """ player creation / registering """
-
-def login(request):
-    """ player log in """
-
-def logout(request):
-    """ player log out """
-
-def settings(request):
-    """ player editing his settings """
-
-#end
+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+from django.shortcuts import render_to_response, get_object_or_404, redirect
+from django.core.urlresolvers import reverse
+from django.template import RequestContext
+from django.http import HttpResponse, HttpResponseRedirect
+
+from google.appengine.api import users as gusers
+
+from budjinn.users.forms import RegisterForm, LoginForm, RegisterGoogleForm, LoginGoogleForm
+from budjinn.users.models import User
+
+# exceptions
+
+# views
+def login(request):
+    """ general login forms """
+    # are we provided with a google account ?
+    user = User.get_from_google()
+    if user.has_google_account():
+        # is it linked to a known user ?
+        if user.is_saved(): # must have forgot to have an account
+            return HttpResponseRedirect(reverse('web:dashboard'))
+        else:
+            return HttpResponseRedirect(reverse('users:register'))
+    if request.POST:
+        login_form = LoginForm(request.POST)
+        if login_form.is_valid():
+            return login_form.get_response()
+    else:
+        login_form = LoginForm()
+    register_form = RegisterForm() # just in case
+    templates = ['login.html', 'users/login.html']
+    return render_to_response(templates, RequestContext(request, locals()))
+
+def register(request):
+    """ general login forms """
+    # are we provided with a google account ?
+    user = User.get_from_google()
+    if user.has_google_account():
+        # is it linked to a known user ?
+        if user.is_saved(): # must have forgot to have an account
+            return HttpResponseRedirect(reverse('web:dashboard'))
+    # display / process the form
+    if request.POST:
+        register_form = RegisterForm(request.POST)
+        if register_form.is_valid():
+            return register_form.get_response()
+    else:
+        register_form = RegisterForm()
+    login_form = LoginForm() # alternative
+    templates = ['register.html', 'users/register.html']
+    return render_to_response(templates, RequestContext(request, locals()))
+
+def register_google(request):
+    """ step 3 for registering with google, called back after google log in """
+    # are we provided with a google account ?
+    user = User.get_from_google()
+    if user.has_google_account():
+        # is it linked to a known user ?
+        if user.is_saved():
+            pass # no need to create it
+        else: # registering this new user
+            user.put()
+        # redirect user to her home
+        return HttpResponseRedirect(reverse('web:dashboard'))
+    else: # we are not provided with a google account
+        # therefore we step back to initial register process
+        return HttpResponseRedirect(reverse('users:register'))
+
+def login_google(request):
+    """ step 3 for registering with google, called back after google log in """
+    # are we provided with a google account ?
+    user = User.get_from_google()
+    if user.has_google_account():
+        # is it linked to a known user ?
+        if user.is_saved():
+            # logged ok, redirecting, maybe log event
+            return HttpResponseRedirect(reverse('web:dashboard'))
+        else:
+            # provide with a register form option
+            pass
+    else: # we are not provided with a google account
+        # therefore we step back to initial register process
+        return HttpResponseRedirect(reverse('users:login'))
+
+
+#end

settings_appengine.py

     # 'django.contrib.auth.middleware.AuthenticationMiddleware',
     # 'django.contrib.messages.middleware.MessageMiddleware',
     # no google account middleware
-    # 'users.middleware.usersMiddleware',
+    'users.middleware.usersMiddleware',
 )
 
 ROOT_URLCONF = 'urls'
     # Uncomment the next line to enable admin documentation:
     # 'django.contrib.admindocs',
     # no google account
-    # 'users',
+    'users',
     'web', # vues web budjinn
     #'admin', # admin budjinn
     # gaeunit for unit testing
     # web
     url(r'', include('web.urls', namespace='web')),
     # users
-    # url(r'^users/', include('users.urls', namespace='users')),
+    url(r'^users/', include('users.urls', namespace='users')),
 )

users/__init__.py

Empty file added.

users/decorators.py

+# -*- coding: utf-8 -*-
+"""
+decorator functions
+
+to use with your views
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+from django.shortcuts import render_to_response, redirect
+from django.template import RequestContext
+
+# import settings
+from .exceptions import Http403, Forbidden
+
+# classes et fonctions
+
+def _call_intercept(view, request, *args, **kw):
+    """ calls view but intercepts Http403 errors """
+    try:
+        r =  view(request, *args, **kw)
+        return r
+    except Http403, exception:
+        # rendering project 403 or defaulting to self
+        r = render_to_response(
+            ['403.html', 'users/403.html'],
+            RequestContext(request, locals()))
+        r.status_code = 403
+        return r
+
+def login_required(view):
+    """ Decorator for views that redirects to login if request.user is not logged in or unsaved """
+    def inner(request, *args, **kw):
+        if not request.nuser.is_logged_in() or not request.nuser.is_saved():
+            return redirect(request.nuser.login_url(request.path))
+        return _call_intercept(view, request, *args, **kw)
+    return inner

users/docs/login.rst

+===============
+procedures
+===============
+
+login procedure
+===============
+
+1. GET the users:login view, which provides a multi-options LoginForm :
+    - google-login
+    - email + password login
+    - mozilla persona login
+2. POST the users:login view, which process the LoginForm :
+    - if submit = google then redirects to google login url then users:login_google view
+    - if email + password check the fields then put user in session then redirects to user home
+    - if mozilla persona ?
+3. (IF google-login) user is redirected by google to GET users:login_google
+    - if user is known, redirect to his home (event may be logged as last logged etc)
+    - if user is not known, provide a page with a RegisterForm
+
+register procedure
+==================
+
+1. GET the users:register view, which provides a multi-options RegisterForm :
+    - google-register
+    - email + password register
+    - mozilla persona register
+2. POST the users:register view, which processes the RegisterForm :
+    - if submit = google then redirects to google login url then users:register_google view
+    - if email + password check the field, save the user, redirect to his home
+    - if mozilla persona ?
+3. (IF google-login) user is redirected by google to GET users:login_google
+    - if user is known, redirect to his home
+    - if user is not known, create it, email him, then redirect to his home

users/exceptions.py

+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+
+# exceptions
+
+# class et fonctions
+from django.http import Http404
+
+class Forbidden(Exception):
+    """ would break a security rule """
+    pass
+
+class Http4xx(Exception):
+    """ those exceptions will be catched by middleware and trigger HTTP errors pages """
+
+class Http403(Http4xx):
+    """ Forbidden """
+    pass
+
+#end
+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+from google.appengine.api import users as gusers
+
+from django import forms
+from django.http import HttpResponse, HttpResponseRedirect
+from django.core.urlresolvers import reverse
+
+from .models import User
+from .widgets import ChoiceSubmitWidget
+
+# exceptions
+
+# class et fonctions
+
+class RegisterForm(forms.Form):
+
+    submit_CHOICES = (
+        ('google', 'Register with google'),
+    )
+    submit = forms.ChoiceField(submit_CHOICES, widget=ChoiceSubmitWidget)
+
+    def get_response(self):
+        if self.cleaned_data.get('submit') == 'google':
+            return HttpResponseRedirect(gusers.create_login_url(reverse('users:register_google')))
+
+class LoginForm(forms.Form):
+
+    submit_CHOICES = (
+        ('google', 'Log in with google'),
+    )
+    submit = forms.ChoiceField(submit_CHOICES, widget=ChoiceSubmitWidget)
+
+    def get_response(self):
+        if self.cleaned_data.get('submit') == 'google':
+            return HttpResponseRedirect(gusers.create_login_url(reverse('users:login_google')))
+
+# @deprecated
+class RegisterGoogleForm(forms.Form):
+    """ register a user linked to a google account """
+
+    # don't know why but using reverse crashes imports
+    url = '/users/register/google/' # reverse('users:register_google')
+
+    submit_CHOICES = (('google', 'Register with google'),)
+    submit = forms.ChoiceField(choices=submit_CHOICES, widget=ChoiceSubmitWidget)
+
+# @deprecated
+class LoginGoogleForm(forms.Form):
+    """ log in a user linked to a google account """
+
+    # don't know why but using reverse crashes imports
+    url = '/users/login/google/' # reverse('users:register_google')
+
+    submit_CHOICES = (('google', 'Login with google'),)
+    submit = forms.ChoiceField(choices=submit_CHOICES, widget=ChoiceSubmitWidget)
+
+#end

users/middleware.py

+# -*- coding: utf-8 -*-
+"""
+users (No Google Account) middleware
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+from .models import User
+
+# exceptions
+
+# class et fonctions
+
+class usersMiddleware:
+
+    def process_request(self, request):
+        """ injects noaccount request attributes
+
+        Injects request.nuser (may be unsaved)
+        """
+        request.nuser = User.get_user()
+
+#end
+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+import datetime
+
+from google.appengine.ext import ndb
+from google.appengine.api import users as gusers
+
+from django.core.urlresolvers import reverse
+
+# exceptions
+
+APP_VERSION = 3
+
+# classes et fonctions
+
+# optimisation : stocker logout url
+class User(ndb.Model):
+    """ un utilisateur de l'application """
+    # compte google si relie = user.user_id()
+    google_account_id = ndb.StringProperty()
+    email = ndb.StringProperty()
+    nickname = ndb.StringProperty()
+    # adresse jabber
+    im = ndb.StringProperty()
+    status = ndb.StringProperty(default=u"beta") # premium, beta, blocked, etc
+    status_expiration_date = ndb.DateProperty()
+    # meta
+    # touched when log in so can be used as last activity / log in
+    last_update = ndb.DateTimeProperty(auto_now=True)
+    logout_url = ndb.StringProperty(indexed=False)
+    # LSN / player related fields
+    # challenges related fields
+    # how many challenges accepted at a time ?
+    accept_challenges = ndb.IntegerProperty(default=1)
+    accept_random_challenges = ndb.BooleanProperty(default=False)
+
+    unsaved = False
+
+    @classmethod
+    def factory(cls, google_user=None):
+        user = User()
+        if google_user is not None:
+            user.set_google_user(google_user)
+        user.unsaved = True
+        return user
+
+    @classmethod
+    def get_from_google(cls):
+        """ get google-logged user if availaible
+        if there is no google user, return a blank User
+        if there is a google user but unkown, return an unsaved User """
+        google_user = gusers.get_current_user()
+        if not google_user:
+            # return unsaved, unlinked account
+            return User.factory()
+        # searching and returning user
+        user = cls.get_by_google_id(google_user.user_id())
+        if user:
+            return user
+        # returning unsaved user linked to found google account
+        return User.factory(google_user=google_user)
+
+    @classmethod
+    def get_user(cls):
+        """ get logged in user if available
+        (email logged, google logged, oauth)
+        else unsaved user (may be linked to unknown google account """
+        return cls.get_from_google()
+
+    @classmethod
+    def get_by_google_id(cls, id):
+        """ get by google account id """
+        q = cls.query().filter(cls.google_account_id == str(id))
+        return q.get()
+
+    def __unicode__(self):
+        return unicode(self.email) or self.google_account_id
+
+    # @param users.User google_user
+    def set_google_user(self, google_user):
+        """ links itself to a google.appengine.api.users.User instance """
+        self.email = google_user.email()
+        self.im = self.email
+        self.nickname = google_user.nickname()
+        self.google_account_id = google_user.user_id()
+
+    def is_logged_in(self):
+        return self.google_account_id is not None
+
+    #def logout_url(self, redirect_to='/'):
+    #    return gusers.create_logout_url(redirect_to)
+
+    def login_url(self, redirect_to='/'):
+        url = reverse('users:login')
+        return url
+        return gusers.create_login_url(redirect_to)
+
+    def add_event(self, name, related_entity=False, details=None):
+        """ adds an event for user """
+        event = UserEvent.factory(self, name,
+            related_entity=related_entity, details=details)
+        event.put()
+        return event
+
+    # hacks to emulate is_saved
+    # @link http://stackoverflow.com/questions/12083254/is-it-possible-to-determine-with-ndb-if-model-is-persistent-in-the-datastore-or/12096066#12096066
+    #_is_saved = False  # class variable provides default value
+    #
+    #@classmethod
+    #def _post_get_hook(cls, key, future):
+    #    obj = future.get_result()
+    #    if obj is not None:
+    #    # test needed because post_get_hook is called even if get() fails!
+    #        obj._is_saved = True
+    #
+    #def _post_put_hook(self, future):
+    #    self._is_saved = True
+
+    def is_saved(self):
+        return not self.unsaved
+
+    def has_google_account(self):
+        return self.google_account_id is not None
+
+
+class UserEvent(ndb.Model):
+    """ parent = user """
+    datetime = ndb.DateTimeProperty(auto_now_add=True)
+    name = ndb.StringProperty(required=True, verbose_name="event identifier ('logged in', etc)")
+    details = ndb.TextProperty(verbose_name="Additional details if necessary")
+    app_version = APP_VERSION
+    related_entity = ndb.KeyProperty() # optional related entity
+
+    @classmethod
+    def factory(cls, user, name, related_entity=None, details=None):
+        """ creates an event for given user """
+        event = cls(parent=user.key, name=name)
+        if isinstance(related_entity, ndb.Model):
+            event.related_entity.key
+        elif isinstance(related_entity, ndb.Key):
+            event.related_entity = related_entity
+        if details:
+            event.details = details
+        return event
+#end
+users : No Google Account
+========================
+
+Equivalent de Noaccount pour Google App Engine

users/templates/inc/login.form.html

+<form action="{% url users:login %}" method="post">
+    <fieldset>
+        <legend>Log in</legend>
+        {{ login_form.as_p }}
+        <p>Not registered ? <a href="{% url users:register %}">Register</a>
+    </fieldset>
+</form>

users/templates/inc/register.form.html

+<form action="{% url users:register %}" method="post">
+    {% csrf_token %}
+    <fieldset>
+        <legend>Register</legend>
+        {{ register_form.as_p }}
+    </fieldset>
+</form>

users/templates/users/403.html

+{% extends "users/layout.html" %}
+
+{% block body %}
+    {{ exception }}
+{% endblock %}

users/templates/users/layout.html

+{% load i18n static %}
+{% get_static_prefix as STATIC_PREFIX %}
+<!doctype html>
+<!--[if lte IE 7]> <html class="no-js ie7 oldie" lang="fr"> <![endif]-->
+<!--[if IE 8]> <html class="no-js ie8 oldie" lang="fr"> <![endif]-->
+<!--[if gt IE 8]><!--> <html class="no-js" lang="fr"> <!--<![endif]-->
+<head>
+  <meta charset="UTF-8">
+
+</head>
+<body>{% block body %}
+    <h1>login required</h1>
+{% endblock %}</body>
+</html>

users/templates/users/login.html

+{% extends "users/layout.html" %}
+
+{% block body %}
+<form action="{% url users:login %}" method="post">
+    {% csrf_token %}
+    <fieldset>
+        <legend>Log in</legend>
+        {{ login_form.as_p }}
+    </fieldset>
+</form>
+
+<form action="{% url users:register %}" method="post">
+    {% csrf_token %}
+    <fieldset>
+        <legend>Not registered ? Register</legend>
+        {{ register_form.as_p }}
+    </fieldset>
+</form>
+{% endblock %}

users/templates/users/register.html

+{% extends "users/layout.html" %}
+
+{% block body %}
+<form action="{% url users:register %}" method="post">
+    {% csrf_token %}
+    <fieldset>
+        <legend>Register</legend>
+        {{ register_form.as_p }}
+    </fieldset>
+</form>
+
+<form action="{% url users:login %}" method="post">
+    <fieldset>
+        <legend>Already registered ? Log in</legend>
+        {{ login_form.as_p }}
+    </fieldset>
+</form>
+{% endblock %}

users/tests/__init__.py

+#from .register_google import *
+#from .login_google import *
+
+from .login import *
+from .register import *

users/tests/lib.py

+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+import os
+
+# from unittest import TestCase
+from decimal import Decimal
+from datetime import date, datetime, timedelta
+
+from django.forms import Form
+from django.test import TestCase
+
+from google.appengine.ext import testbed
+from google.appengine.api import users as gusers
+
+# from google.appengine.datastore import datastore_stub_util
+
+# import relatifs non permis, + on est censé se trouver à la racine du projet non dans test
+from users.models import User
+
+class BaseTestCase(TestCase):
+
+    def _pre_setup(self):
+        """ by surclassing this method we ignore SQL fixtures setup from django Test Case
+        AND we sets appengien testbed """
+        self.testbed = testbed.Testbed()
+        self.testbed.activate()
+        self.testbed.init_user_stub()
+        self.testbed.init_datastore_v3_stub()
+        self.testbed.init_memcache_stub()
+
+    def _post_teardown(self):
+        """ by surclassing this we silent django database/transaction errors """
+        pass
+
+    #def setUp(self):
+    #    self.testbed = testbed.Testbed()
+    #    self.testbed.activate()
+    #    self.testbed.init_user_stub()
+
+    # not needed with uneedtest ?
+    #def setUp(self):
+    #    # First, create an instance of the Testbed class.
+    #    self.testbed = testbed.Testbed()
+    #    # Then activate the testbed, which prepares the service stubs for use.
+    #    self.testbed.activate()
+    #    # Create a consistency policy that will simulate the High Replication consistency model.
+    #    self.policy = datastore_stub_util.PseudoRandomHRConsistencyPolicy(probability=0)
+    #    # Initialize the datastore stub with this policy.
+    #    self.testbed.init_datastore_v3_stub(consistency_policy=self.policy)
+    #
+    #def tearDown(self):
+    #    self.testbed.deactivate()
+
+    @classmethod
+    def create_user(cls, **args):
+        user = User(**args)
+        user.put()
+        return user
+
+    def set_google_user(self, email, user_id, is_admin=False):
+        """ fake a google-connected user """
+        self.testbed.setup_env(
+            SERVER_NAME = 'localhost',
+            SERVER_PORT = '8000',
+            AUTH_DOMAIN = 'testbed',
+            USER_EMAIL = email,
+            USER_ID = user_id,
+            USER_IS_ADMIN = ('1' if is_admin else '0'),
+            overwrite = True)
+        # self.fail(unicode(os.environ))
+        #os.environ['AUTH_DOMAIN'] =  "localhost"
+        #os.environ['USER_EMAIL'] = email or ''
+        #os.environ['USER_ID'] = user_id or ''
+        #os.environ['USER_IS_ADMIN'] = '1' if is_admin else '0'
+
+    def unset_google_user(self):
+        self.set_google_user('', '')
+
+
+class FormTestCase(BaseTestCase):
+
+    form_class = None
+
+    def form_factory(self, form_class=None, data=None, instance=None):
+        # return a built form here
+        form_class = form_class or self.form_class
+        if not form_class or not issubclass(form_class, Form):
+            self.fail("form_class shall be a form class")
+        if data is None and instance is None:
+            return form_class()
+        elif data is None:
+            return form_class(instance=instance)
+        elif instance is None:
+            return form_class(data=data)
+        else:
+            return form_class(data=data, instance=instance)
+
+    def get_valid_data(self, data_set=None):
+        """ should return a valid data dict
+        may select a particular data set with data_set """
+        return {}
+
+    def assertValid(self, data=None, instance=None):
+        """ assert form is valid with data and/or instance
+        takes data from get_valid_data() if not provided """
+        data = data or self.get_valid_data()
+        form = self.form_factory(data=data, instance=instance)
+        if not form.is_valid():
+            self.fail("Form should be valid with %s, errors were %s" % (data, form.errors))
+        return form
+
+    def assertInvalid(self, data, instance=None):
+        """ assert form is invalid with provided data """
+        form = self.form_factory(data=data, instance=instance)
+        if form.is_valid():
+            self.fail("form should be invalid with %s" % data)
+        return form
+
+    def assertFieldRequired(self, field_name, data=None):
+        """ assert field is required """
+        data = data or self.get_valid_data()
+        dc = data.copy()
+        del dc[field_name]
+        return self.assertInvalid(dc)
+
+    def assertFieldInvalid(self, field_name, value=None, data=None):
+        """ assert form is invalid if field has provided value """
+        data = data or self.get_valid_data()
+        dc = data.copy()
+        dc[field_name] = value
+        return self.assertInvalid(dc)
+
+    def assertFieldValid(self, field_name, value=None, data=None):
+        """ assert form is valid if field has provided value """
+        data = data or self.get_valid_data()
+        dc = data.copy()
+        dc[field_name] = value
+        return self.assertValid(data=dc)
+
+# end

users/tests/login.py

+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+import os
+from google.appengine.api import users as gusers
+
+from django.http import HttpResponse, HttpResponseRedirect
+from django.core.urlresolvers import reverse
+
+from ..tests.lib import BaseTestCase
+from ..models import User
+
+class TestLoginViews(BaseTestCase):
+    """ test log in the app using a google account """
+
+    def setUp(self):
+        self.url = reverse('users:login')
+        # specific google login view
+        self.url_google = reverse('users:login_google')
+        self.user_url = reverse('web:dashboard')
+        self.google_id = '36'
+        self.user_email = 'test@gmail.com'
+        self.user = User(google_account_id=self.google_id, email=self.user_email)
+        self.user.put()
+
+    def tearDown(self):
+        self.unset_google_user()
+
+    def test_GET_unlogged(self):
+        """ shall provide a login form """
+        r = self.client.get(self.url)
+        # shall contain a login form
+        self.assertContains(r, '<form', status_code=200)
+        self.assertContains(r, 'action="%s"' % self.url)
+        # and meybe a register form too ?
+        register_url = reverse('users:register')
+        self.assertContains(r, 'action="%s"' % register_url)
+
+    def test_GET_google_logged(self):
+        """ shall redirect to users home """
+        self.set_google_user(self.user_email, self.google_id)
+        r = self.client.get(self.url)
+        self.assertRedirects(r, self.user_url)
+        # shall have set logout_url and last_update
+        user = self.user.key.get()
+        # self.fail(user.__dict__)
+        if user.logout_url is None:
+            self.fail('shall have set logout_url')
+
+    def test_GET_google_logged_unknown(self):
+        """ logged but unkown user, shall redirect to register page """
+        self.set_google_user('unknown@gmail.com', '999')
+        r = self.client.get(self.url)
+        self.assertRedirects(r, reverse('users:register'))
+
+    def test_POST_google(self):
+        """ shall redirect to google login url """
+        data = {'submit': 'google'}
+        r = self.client.post(self.url, data)
+        expected_redirect = gusers.create_login_url(self.url_google)
+        self.assertRedirects(r, expected_redirect)
+
+    def test_GET_login_google_logged(self):
+        """ shall redirect user to its home """
+        # google login
+        self.set_google_user(self.user_email, self.google_id)
+        r = self.client.get(self.url_google)
+        self.assertRedirects(r, self.user_url)
+
+    def test_GET_login_google_unlogged(self):
+        """ shall redirect to generic login """
+        r = self.client.get(self.url_google)
+        self.assertRedirects(r, self.url)
+
+#end

users/tests/login_google.py

+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+import os
+from google.appengine.api import users as gusers
+
+from django.http import HttpResponse, HttpResponseRedirect
+from django.core.urlresolvers import reverse
+
+from ..tests.lib import BaseTestCase
+from ..models import User
+
+class TestLoginGoogle(BaseTestCase):
+    """ test log in the app using a google account """
+
+    def setUp(self):
+        self.url = reverse('users:login_google')
+        # general login url
+        self.general_login_url = reverse('users:login')
+
+    def tearDown(self):
+        # make sure no user is connected
+        self.unset_google_user()
+
+    def test_get_unlogged(self):
+        # GETting the login view without a google account (optional, should provide with a login form)
+        r = self.client.get(self.url)
+        self.assertContains(r, '<form', status_code=200)
+        self.assertContains(r, 'action="%s"' % self.url)
+        # we make sure no user is created / connected yet
+        self.assertEqual(gusers.get_current_user(), None)
+        self.assertEqual(User.query().count(), 0)
+
+    def test_post_unlogged(self):
+        # validating the form (POST) should redirect to google login url (GET)
+        r = self.client.post(self.url, {'submit': 'google'}, follow=False)
+        expected_redirect = gusers.create_login_url(self.url)
+        self.assertRedirects(r, expected_redirect)
+
+    def test_get_logged_registered(self):
+        """ known and logged user access the login page, shall be redirected """
+        user_email, user_id = 'test@gmail.com', '36'
+        self.set_google_user(user_email, user_id)
+        user = User(google_account_id=user_id, email=user_email)
+        user.put()
+        # self.fail("user = %s \n google = %s" % (user, gusers.get_current_user()))
+        r = self.client.get(self.url, follow=False)
+        # no new user shall be created
+        self.assertRedirects(r, reverse('web:dashboard'))
+        # do we test logging the event here ?
+
+    def test_get_logged_unregistered(self):
+        """ logged but unregistered user shall be provided with a register form option """
+        user_email, user_id = 'test@gmail.com', '36'
+        self.set_google_user(user_email, user_id)
+        r = self.client.get(self.url)
+        register_url = reverse('users:register_google')
+        self.assertContains(r, '<form', status_code=200)
+        self.assertContains(r, 'action="%s"' % register_url)
+
+    def test_form_in_general_login(self):
+        r= self.client.get(self.general_login_url)
+        self.assertContains(r, 'action="%s"' % self.url)
+
+    # do we check that he google-logged user has an attribute telling login method there ?
+
+# end

users/tests/register.py

+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+import os
+from google.appengine.api import users as gusers
+
+from django.http import HttpResponse, HttpResponseRedirect
+from django.core.urlresolvers import reverse
+from django.core import mail
+
+from ..tests.lib import BaseTestCase
+from ..models import User
+
+class TestRegisterViews(BaseTestCase):
+
+    def setUp(self):
+        self.url = reverse('users:register')
+        self.url_google = reverse('users:register_google')
+        self.user_url = reverse('web:dashboard')
+
+    def tearDown(self):
+        self.unset_google_user()
+
+    def test_GET_unlogged(self):
+        """ shall provide a form """
+        r = self.client.get(self.url)
+        # shall contain a login form
+        self.assertContains(r, '<form', status_code=200)
+        self.assertContains(r, 'action="%s"' % self.url)
+        # and maybe a register form too ?
+        self.assertContains(r, 'action="%s"' % reverse('users:login'))
+
+    def test_GET_google_logged(self):
+        """ redirect to user home """
+        # creating and login the user
+        user = User(email='test@gmail.com', google_account_id='36')
+        user.put()
+        self.set_google_user(email=user.email, user_id=user.google_account_id)
+        r = self.client.get(self.url)
+        self.assertRedirects(r, self.user_url)
+
+    def test_GET_google_logged_unknown(self):
+        """ shouldn't be there, provide a form """
+        self.set_google_user(email='new@gmail.com', user_id='999')
+        r = self.client.get(self.url)
+        self.assertContains(r, '<form', status_code=200)
+
+    def test_POST_google(self):
+        """ shall redirect to google login """
+        r = self.client.post(self.url, {'submit': 'google'})
+        expected_redirect = gusers.create_login_url(self.url_google)
+        self.assertRedirects(r, expected_redirect)
+
+    def test_GET_register_google_logged(self):
+        """ shall create user, email him, redirect to her home """
+        # here we fake the user associating its google account to the app
+        user_email, user_id = 'test@gmail.com', '36'
+        self.set_google_user(user_email, user_id)
+        self.assertEqual(gusers.get_current_user().user_id(), user_id)
+        # which then redirects back to the register view (GET)
+        r = self.client.get(self.url_google, follow=False)
+        # GETting the view while providing a google account should create a User
+        user = User.get_by_google_id(user_id)
+        if not user:
+            # self.fail(unicode(os.environ))
+            self.fail("should have created a user, created none")
+        self.assertEqual(user.email, user_email)
+        if user.logout_url is None:
+            self.fail('shall have set logout_url')
+        # a greeting email should have been sent to new user
+        self.assertEqual(len(mail.outbox), 1)
+        self.assertEqual(mail.outbox[0].to, user_email)
+        # testing email content there ?
+        # and redirects to dashboard
+        # WHY does the dashboard redirects to login then ???
+        #r2 = self.client.get('/dashboard/')
+        #print r2
+        #expected_redirect = reverse('web:dashboard')
+        #self.assertRedirects(r, expected_redirect)
+
+    def test_GET_register_google_unlogged(self):
+        """ shall redirect to generic register """
+        r = self.client.get(self.url_google)
+        self.assertRedirects(r, self.url)

users/tests/register_google.py

+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+import os
+from google.appengine.api import users as gusers
+
+from django.http import HttpResponse, HttpResponseRedirect
+from django.core.urlresolvers import reverse
+from django.core import mail
+
+from ..tests.lib import BaseTestCase
+from ..models import User
+from ..forms import RegisterGoogleForm
+
+class TestRegisterGoogle(BaseTestCase):
+    """ test registering the app using a google account """
+
+    def setUp(self):
+        self.url = RegisterGoogleForm.url
+
+    def tearDown(self):
+        # make sure no user is connected
+        self.unset_google_user()
+
+    def test_mock_user_connected(self):
+        user_email = 'test@gmail.com'
+        user_id = '42'
+        self.set_google_user(user_email, user_id)
+        #import os
+        #self.fail(os.environ)
+        # checking this
+        guser = gusers.get_current_user()
+        self.assertEqual(guser.user_id(), user_id)
+
+    def test_get_unlogged(self):
+        # GETting the register view without a google account (optional, should provide with a register form)
+        r = self.client.get(self.url)
+        self.assertContains(r, '<form', status_code=200)
+        # we make sure no user is created / connected yet
+        self.assertEqual(gusers.get_current_user(), None)
+        self.assertEqual(User.query().count(), 0)
+
+    def test_post_unlogged(self):
+        # validating the form (POST) should redirect to google login url (GET)
+        r = self.client.post(self.url, {'submit': 'google'}, follow=False)
+        expected_redirect = gusers.create_login_url(self.url)
+        self.assertRedirects(r, expected_redirect)
+
+    def test_get_registering(self):
+        """ new user is returning from google-login """
+        # here we fake the user associating its google account to the app
+        user_email, user_id = 'test@gmail.com', '36'
+        self.set_google_user(user_email, user_id)
+        self.assertEqual(gusers.get_current_user().user_id(), user_id)
+        # which then redirects back to the register view (GET)
+        r = self.client.get(self.url, follow=False)
+        # GETting the view while providing a google account should create a User
+        user = User.get_by_google_id(user_id)
+        if not user:
+            # self.fail(unicode(os.environ))
+            self.fail("should have created a user, created none")
+        self.assertEqual(user.email, user_email)
+        # a greeting email should have been sent to new user
+        self.assertEqual(len(mail.outbox), 1)
+        self.assertEqual(mail.outbox[0].to, user_email)
+        # testing email content there ?
+        # and redirects to dashboard
+        # WHY does the dashboard redirects to login then ???
+        #r2 = self.client.get('/dashboard/')
+        #print r2
+        #expected_redirect = reverse('web:dashboard')
+        #self.assertRedirects(r, expected_redirect)
+
+    def test_get_logged(self):
+        """ known and logged user access the register page, shall be redirected """
+        user_email, user_id = 'test@gmail.com', '36'
+        self.set_google_user(user_email, user_id)
+        user = User(google_account_id=user_id, email=user_email)
+        user.put()
+        r = self.client.get(self.url)
+        self.assertRedirects(r, reverse('web:dashboard'))
+
+# end
+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+from django.conf.urls.defaults import *
+
+# exceptions
+
+# classes et fonctions
+
+# urls
+
+urlpatterns = patterns('lsnrevival.users.views',
+    # log in processing
+    url(r'^login/$', 'login', name='login'),
+    # generic register processing
+    url(r'^register/$', 'register', name='register'),
+    # google login/ register processing
+    url(r'^register/google/$', 'register_google', name='register_google'),
+    url(r'^login/google/$', 'login_google', name='login_google'),
+    # email register processing
+    #url(r'^register/$', 'register_email', name='register_email'),
+    # logout processing
+    # url(r'^logout/$', 'logout', name='logout'),
+    # editing own email account
+    # url(r'^change/$', 'change_own', name='change_own')
+)
+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+from django.shortcuts import render_to_response, get_object_or_404, redirect
+from django.core.urlresolvers import reverse
+from django.template import RequestContext
+from django.http import HttpResponse, HttpResponseRedirect
+
+from google.appengine.api import users as gusers
+
+from .forms import RegisterForm, LoginForm, RegisterGoogleForm, LoginGoogleForm
+from .models import User
+
+# exceptions
+
+# views
+def login(request):
+    """ general login forms """
+    # are we provided with a google account ?
+    user = User.get_from_google()
+    if user.has_google_account():
+        # is it linked to a known user ?
+        if user.is_saved(): # must have forgot to have an account
+            return HttpResponseRedirect(reverse('web:user_home'))
+        else:
+            return HttpResponseRedirect(reverse('users:register'))
+    if request.POST:
+        login_form = LoginForm(request.POST)
+        if login_form.is_valid():
+            return login_form.get_response()
+    else:
+        login_form = LoginForm()
+    register_form = RegisterForm() # just in case
+    templates = ['login.html', 'users/login.html']
+    return render_to_response(templates, RequestContext(request, locals()))
+
+def register(request):
+    """ general login forms """
+    # are we provided with a google account ?
+    user = User.get_from_google()
+    if user.has_google_account():
+        # is it linked to a known user ?
+        if user.is_saved(): # must have forgot to have an account
+            return HttpResponseRedirect(reverse('web:user_home'))
+    # display / process the form
+    if request.POST:
+        register_form = RegisterForm(request.POST)
+        if register_form.is_valid():
+            return register_form.get_response()
+    else:
+        register_form = RegisterForm()
+    login_form = LoginForm() # alternative
+    templates = ['register.html', 'users/register.html']
+    return render_to_response(templates, RequestContext(request, locals()))
+
+def register_google(request):
+    """ step 3 for registering with google, called back after google log in """
+    # are we provided with a google account ?
+    user = User.get_from_google()
+    if user.has_google_account():
+        # is it linked to a known user ?
+        if user.is_saved():
+            pass # no need to create it
+        else: # registering this new user
+            # setting logout url
+            user.logout_url = gusers.create_logout_url(reverse('web:index'))
+            user.put()
+        # redirect user to her home
+        return HttpResponseRedirect(reverse('web:user_home'))
+    else: # we are not provided with a google account
+        # therefore we step back to initial register process
+        return HttpResponseRedirect(reverse('users:register'))
+
+def login_google(request):
+    """ step 3 for registering with google, called back after google log in """
+    # are we provided with a google account ?
+    user = User.get_from_google()
+    if user.has_google_account():
+        # is it linked to a known user ?
+        if user.is_saved():
+            # logged ok, redirecting, maybe log event ?
+            user.logout_url = gusers.create_logout_url(reverse('web:index'))
+            user.put()
+            return HttpResponseRedirect(reverse('web:user_home'))
+        else:
+            # provide with a register form option
+            pass
+    else: # we are not provided with a google account
+        # therefore we step back to initial register process
+        return HttpResponseRedirect(reverse('users:login'))
+
+
+#end
+# -*- coding: utf-8 -*-
+"""
+
+"""
+##
+# @author J.Ducastel <nospam0@ducastel.name>
+
+# imports
+from django import forms
+from django.utils.safestring import mark_safe
+
+# exceptions
+
+# classes et fonctions
+class ChoiceSubmitWidget(forms.widgets.Widget):
+    """A widget that renders a choice field as multiple submit buttons"""
+    def __init__(self, choices=None, attrs={}):
+        self.label = None
+        self.choices=choices
+        self.attrs = attrs
+
+    def render(self, name, value=None, attrs={}):
+        final_attrs = self.build_attrs(
+            self.attrs,
+            type=u"submit",
+            name=name,
+            )
+        flat_attrs = forms.widgets.flatatt(final_attrs)
+        out = u''
+        for value, label in self.choices:
+            out = out + u'<button%s value="%s">%s</button>' % (
+                flat_attrs, value, label)
+        return mark_safe(out)
 # exceptions
 
 # class et fonctions
+class ChallengeForm(forms.Form):
+    """ issue a challenge """
+    # challengee =
+    # Force Points
+    # Turns limit
+    # map
+
+    #def __init__(self, data=None, **kw):
+    #    super(ChallengeForm, self).__init__(data, **kw)
+
+
+
 class AppengineUploadForm(forms.Form):
 
     url = None

web/templates/index.html

     <article>
         <header>Welcome to LSN revival !</header>
     </article>
+
+    {% include "inc/login.form.html" %}
 </section>
 {% endblock %}

web/templates/user_home.html

+{% extends "site.html" %}
+
+{% block content %}
+    <p>Hello {{ request.nuser }}</p>
+
+<article>
+    <header>Registered players</header>
+    <ul>{% for p in players %}
+        <li>{{ p }}</li>{% endfor %}
+    </ul>
+</article>
+{% endblock %}
 urlpatterns = patterns('lsnrevival.web.views',
     # accueil
     url(r'^$', 'index', name="index"),
+    url(r'^home/$', 'user_home', name="user_home"),
 
     # errors pages
     #url(r'^404/?$', 'error404', name="error404"),
 
 from google.appengine.api import users as gusers
 
-#from users.models import User
-#from users.decorators import login_required
-#from users.forms import LoginForm
+from users.models import User
+from users.decorators import login_required
+from users.forms import LoginForm, RegisterForm
 
 from .forms import LSNFileUploadForm
 from .utils import get_uploads
 # site pages
 def index(request):
     """ accueil public general """
+    login_form = LoginForm()
     return render_to_response('index.html',
         RequestContext(request, locals()))
 
-def player_home(request):
+@login_required
+def user_home(request):
     """ player home when logged """
+    players = User.query().fetch()
     # challenge form ?
+    return render_to_response('user_home.html',
+        RequestContext(request, locals()))
 
 def players(request):
     """ players list """