OMG..lots of changes for sake of mobile login / user menu etc.

Feeling a bit sloppy right about now...oh well good enough
This commit is contained in:
Lance Edgar 2016-12-11 18:07:30 -06:00
parent e3ae427e37
commit ee0bdc4b74
10 changed files with 335 additions and 183 deletions

View file

@ -19,13 +19,13 @@ div.field-wrapper {
}
div.field-wrapper label {
margin: 0px;
padding-top: 3px;
text-align: right;
width: 100px;
width: auto;
}
div.field-wrapper input {
div.field-wrapper div.field input[type="text"],
div.field-wrapper div.field input[type="password"] {
margin-left: 1em;
width: 150px;
}

View file

@ -0,0 +1,14 @@
/****************************************
* Global styles for mobile templates
****************************************/
.replacement-header {
display: none;
}
/* used by login page */
.error {
color: red;
margin-bottom: 1em;
}

View file

@ -0,0 +1,39 @@
/************************************************************
*
* tailbone.mobile.js
*
* Global logic for mobile app
*
************************************************************/
$(function() {
// must init header/footer toolbars since ours are "external"
$('[data-role="header"], [data-role="footer"]').toolbar({theme: 'a'});
});
$(document).on('pagecontainerchange', function(event, ui) {
// in some cases (i.e. when no user is logged in) we may want the (external)
// header toolbar button to change between pages. here's how we do that.
// note however that we do this *always* even when not technically needed
var link = $('[data-role="header"] a');
var newlink = ui.toPage.find('.replacement-header a');
link.text(newlink.text());
link.attr('href', newlink.attr('href'));
link.removeClass('ui-icon-home ui-icon-user');
link.addClass(newlink.attr('class'));
});
$(document).on('pageshow', function() {
// on login page, auto-focus username
el = $('#username');
if (el.is(':visible')) {
el.focus();
}
});

View file

@ -8,3 +8,6 @@
% for name, version in packages.iteritems():
<h3>${name} ${version}</h3>
% endfor
<br />
<p>Please see <a href="https://rattailproject.org/">rattailproject.org</a> for more info.</p>

View file

@ -13,30 +13,28 @@
${h.image(request.static_url('tailbone:static/img/home_logo.png'), "Rattail Logo", id='logo')}
</%def>
${self.logo()}
<div class="form">
${h.form('')}
<input type="hidden" name="referrer" value="${referrer}" />
<%def name="login_form()">
<div class="form">
${form.begin(**{'data-ajax': 'false'})}
${form.hidden('referrer', value=referrer)}
## this is used by mobile view
% if error:
<div class="error">${error}</div>
% endif
<div class="field-wrapper">
<label for="username">Username</label>
<input type="text" name="username" id="username" value="" />
</div>
<div class="field-wrapper">
<label for="password">Password</label>
<input type="password" name="password" id="password" value="" />
</div>
${form.field_div('username', form.text('username'))}
${form.field_div('password', form.password('password'))}
<div class="buttons">
${h.submit('submit', "Login")}
${form.submit('submit', "Login")}
<input type="reset" value="Reset" />
</div>
${h.end_form()}
</div>
${form.end()}
</div>
</%def>
${self.logo()}
${self.login_form()}

View file

@ -3,40 +3,92 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>${self.global_title()} &raquo; ${capture(self.title)}</title>
<title>${self.global_title()} &raquo; ${self.title()}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
${h.javascript_link('https://code.jquery.com/jquery-1.12.4.min.js')}
${h.javascript_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js')}
${h.stylesheet_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css')}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.mobile.js'))}
${h.stylesheet_link(request.static_url('tailbone:static/css/mobile.css'))}
% if not request.rattail_config.production():
<style type="text/css">
.ui-page-theme-a { background-image: url(${request.static_url('tailbone:static/img/testing.png')}); }
</style>
% endif
</head>
${self.mobile_body()}
</html>
<%def name="mobile_body()">
<body>
<div data-role="page">
<div data-role="page" data-url="${self.page_url()}"${' data-rel="dialog"' if dialog else ''|n}>
${self.mobile_usermenu()}
${self.mobile_header()}
${self.mobile_page_body()}
${self.mobile_footer()}
</div><!-- page -->
</body>
</%def>
<%def name="app_title()">Rattail Demo</%def>
<%def name="global_title()">${"[STAGE] " if not request.rattail_config.production() else ''}${self.app_title()}</%def>
<%def name="page_url()">${request.current_route_url()}</%def>
<%def name="page_title()">${self.title()}</%def>
<%def name="mobile_header()">
<div data-role="header">
${h.link_to("Home", url('mobile.home'), class_='ui-btn-left')}
${h.link_to("About", url('mobile.about'), class_='ui-btn-right')}
${self.mobile_header_link()}
<h1>${self.global_title()}</h1>
</div>
</%def>
<div role="main" class="ui-content">
% if capture(self.title):
<h2>${self.title()}</h2>
<%def name="mobile_header_link()">
% if request.user:
${h.link_to(request.user.get_short_name(), '#usermenu', class_='ui-btn-left ui-btn ui-btn-inline ui-mini ui-corner-all ui-btn-icon-left ui-icon-user')}
% elif request.matched_route.name in ('mobile.login', 'mobile.about'):
${h.link_to("Home", url('mobile.home'), class_='ui-btn-left ui-btn ui-btn-inline ui-mini ui-corner-all ui-btn-icon-left ui-icon-home')}
% else:
${h.link_to("Login", url('mobile.login'), class_='ui-btn-left ui-btn ui-btn-inline ui-mini ui-corner-all ui-btn-icon-left ui-icon-user')}
% endif
</%def>
<%def name="mobile_usermenu()">
<div id="usermenu" data-role="panel" data-display="overlay">
<ul data-role="listview">
<li data-icon="home">${h.link_to("Home", url('mobile.home'))}</li>
<li data-icon="user">${h.link_to("Logout", url('logout'), **{'data-ajax': 'false'})}</li>
<li data-icon="info">${h.link_to("About {}".format(capture(self.app_title)), url('mobile.about'))}</li>
</ul>
</div>
</%def>
<%def name="mobile_page_body()">
<div role="main" class="ui-content">
% if capture(self.page_title):
<h2>${self.page_title()}</h2>
% endif
${self.body()}
<div class="replacement-header">
${self.mobile_header_link()}
</div>
</div>
</%def>
<%def name="mobile_footer()">
<div data-role="footer">
<h4>powered by ${h.link_to("Rattail", url('mobile.about'))}</h4>
</div>
</div>
</body>
</html>
<%def name="global_title()">${"[STAGE] " if not request.rattail_config.production() else ''}Rattail Demo</%def>
</%def>

View file

@ -0,0 +1,20 @@
## -*- coding: utf-8 -*-
<%inherit file="tailbone:templates/mobile/base.mako" />
<%def name="mobile_body()">
<body>
${self.mobile_header()}
<div data-role="page" data-url="${self.page_url()}"${' data-rel="dialog"' if dialog else ''|n}>
${self.mobile_usermenu()}
${self.mobile_page_body()}
</div><!-- page -->
${self.mobile_footer()}
</body>
</%def>

View file

@ -1,7 +1,9 @@
## -*- coding: utf-8 -*-
<%inherit file="/mobile/base.mako" />
<%def name="title()"></%def>
<%def name="title()">Home</%def>
<%def name="page_title()"></%def>
<div style="text-align: center;">
${h.image(request.static_url('tailbone:static/img/home_logo.png'), "Rattail Logo", width='400')}

View file

@ -0,0 +1,7 @@
## -*- coding: utf-8 -*-
<%inherit file="/mobile/base.mako" />
<%namespace file="/login.mako" import="login_form" />
<%def name="title()">Login</%def>
${login_form()}

View file

@ -28,18 +28,52 @@ from __future__ import unicode_literals, absolute_import
from rattail.db.auth import authenticate_user, set_user_password
import formencode
from pyramid.httpexceptions import HTTPFound, HTTPForbidden
import formencode as fe
from pyramid.httpexceptions import HTTPForbidden
from pyramid.security import remember, forget
from pyramid_simpleform import Form
from webhelpers.html import literal
from webhelpers.html import tags
from webhelpers.html import tags, literal
from tailbone import forms
from tailbone.db import Session
from tailbone.forms.simpleform import FormRenderer
from tailbone.views import View
def forbidden(request):
class UserLogin(fe.Schema):
allow_extra_fields = True
filter_extra_fields = True
username = fe.validators.NotEmpty()
password = fe.validators.NotEmpty()
class CurrentPasswordCorrect(fe.validators.FancyValidator):
def _to_python(self, value, state):
user = state
if not authenticate_user(Session, user.username, value):
raise fe.Invalid("The password is incorrect.", value, state)
return value
class ChangePassword(fe.Schema):
allow_extra_fields = True
filter_extra_fields = True
current_password = fe.All(
fe.validators.NotEmpty(),
CurrentPasswordCorrect())
new_password = fe.validators.NotEmpty()
confirm_password = fe.validators.NotEmpty()
chained_validators = [fe.validators.FieldsMatch(
'new_password', 'confirm_password')]
class AuthenticationView(View):
def forbidden(self):
"""
Access forbidden view.
@ -47,145 +81,128 @@ def forbidden(request):
appropriate view.
"""
msg = literal("You do not have permission to do that.")
if not request.authenticated_userid:
if not self.request.authenticated_userid:
msg += literal("&nbsp; (Perhaps you should %s?)" %
tags.link_to("log in", request.route_url('login')))
tags.link_to("log in", self.request.route_url('login')))
# Store current URL in session, for smarter redirect after login.
request.session['next_url'] = request.current_route_url()
request.session.flash(msg, allow_duplicate=False)
return HTTPFound(location=request.get_referrer())
self.request.session['next_url'] = self.request.current_route_url()
self.request.session.flash(msg, allow_duplicate=False)
return self.redirect(self.request.get_referrer())
class UserLogin(formencode.Schema):
allow_extra_fields = True
filter_extra_fields = True
username = formencode.validators.NotEmpty()
password = formencode.validators.NotEmpty()
def login(request):
def login(self, mobile=False):
"""
The login view, responsible for displaying and handling the login form.
"""
referrer = request.get_referrer()
home = 'mobile.home' if mobile else 'home'
referrer = self.request.get_referrer(default=self.request.route_url(home))
# Redirect if already logged in.
if request.user:
return HTTPFound(location=referrer)
# redirect if already logged in
if self.request.user:
if not mobile:
self.request.session.flash("{} is already logged in".format(self.request.user), 'error')
return self.redirect(referrer)
form = Form(request, schema=UserLogin)
form = Form(self.request, schema=UserLogin)
context = {'form': forms.FormRenderer(form), 'referrer': referrer, 'dialog': mobile}
if form.validate():
user = authenticate_user(Session(),
form.data['username'],
form.data['password'])
if user:
headers = remember(request, user.uuid)
# okay now they're truly logged in
headers = remember(self.request, user.uuid)
# Treat URL from session as referrer, if available.
referrer = request.session.pop('next_url', referrer)
return HTTPFound(location=referrer, headers=headers)
request.session.flash("Invalid username or password")
referrer = self.request.session.pop('next_url', referrer)
return self.redirect(referrer, headers=headers)
else:
if mobile:
context['error'] = "Invalid username or password"
else:
self.request.session.flash("Invalid username or password")
return context
return {'form': FormRenderer(form), 'referrer': referrer}
def mobile_login(self):
return self.login(mobile=True)
def logout(request):
def logout(self, mobile=False):
"""
View responsible for logging out the current user.
This deletes/invalidates the current session and then redirects to the
login page.
"""
self.request.session.delete()
self.request.session.invalidate()
headers = forget(self.request)
login = 'mobile.login' if mobile else 'login'
referrer = self.request.get_referrer(default=self.request.route_url(login))
return self.redirect(referrer, headers=headers)
request.session.delete()
request.session.invalidate()
headers = forget(request)
referrer = request.get_referrer()
return HTTPFound(location=referrer, headers=headers)
def mobile_logout(self):
return self.logout(mobile=True)
def become_root(request):
"""
Elevate the current request to 'root' for full system access.
"""
if not request.is_admin:
raise HTTPForbidden()
request.session['is_root'] = True
request.session.flash("You have been elevated to 'root' and now have full system access")
return HTTPFound(location=request.get_referrer())
def stop_root(request):
"""
Lower the current request from 'root' back to normal access.
"""
if not request.is_admin:
raise HTTPForbidden()
request.session['is_root'] = False
request.session.flash("Your normal system access has been restored")
return HTTPFound(location=request.get_referrer())
class CurrentPasswordCorrect(formencode.validators.FancyValidator):
def _to_python(self, value, state):
user = state
if not authenticate_user(Session, user.username, value):
raise formencode.Invalid("The password is incorrect.", value, state)
return value
class ChangePassword(formencode.Schema):
allow_extra_fields = True
filter_extra_fields = True
current_password = formencode.All(
formencode.validators.NotEmpty(),
CurrentPasswordCorrect())
new_password = formencode.validators.NotEmpty()
confirm_password = formencode.validators.NotEmpty()
chained_validators = [formencode.validators.FieldsMatch(
'new_password', 'confirm_password')]
def change_password(request):
def change_password(self):
"""
Allows a user to change his or her password.
"""
if not self.request.user:
return self.redirect(self.request.route_url('home'))
if not request.user:
return HTTPFound(location=request.route_url('home'))
form = Form(request, schema=ChangePassword, state=request.user)
form = Form(self.request, schema=ChangePassword, state=self.request.user)
if form.validate():
set_user_password(request.user, form.data['new_password'])
return HTTPFound(location=request.get_referrer())
set_user_password(self.request.user, form.data['new_password'])
return self.redirect(self.request.get_referrer())
return {'form': FormRenderer(form)}
return {'form': forms.FormRenderer(form)}
def become_root(self):
"""
Elevate the current request to 'root' for full system access.
"""
if not self.request.is_admin:
raise HTTPForbidden()
self.request.session['is_root'] = True
self.request.session.flash("You have been elevated to 'root' and now have full system access")
return self.redirect(self.request.get_referrer())
def add_routes(config):
def stop_root(self):
"""
Lower the current request from 'root' back to normal access.
"""
if not self.request.is_admin:
raise HTTPForbidden()
self.request.session['is_root'] = False
self.request.session.flash("Your normal system access has been restored")
return self.redirect(self.request.get_referrer())
@classmethod
def defaults(cls, config):
# forbidden
config.add_forbidden_view(cls, attr='forbidden')
# login
config.add_route('login', '/login')
config.add_view(cls, attr='login', route_name='login', renderer='/login.mako')
config.add_route('mobile.login', '/mobile/login')
config.add_view(cls, attr='mobile_login', route_name='mobile.login', renderer='/mobile/login.mako')
# logout
config.add_route('logout', '/logout')
config.add_route('become_root', '/root/yes')
config.add_route('stop_root', '/root/no')
config.add_view(cls, attr='logout', route_name='logout')
config.add_route('mobile.logout', '/mobile/logout')
config.add_view(cls, attr='mobile_logout', route_name='mobile.logout')
# change password
config.add_route('change_password', '/change-password')
config.add_view(cls, attr='change_password', route_name='change_password', renderer='/change_password.mako')
# become/stop root
config.add_route('become_root', '/root/yes')
config.add_view(cls, attr='become_root', route_name='become_root')
config.add_route('stop_root', '/root/no')
config.add_view(cls, attr='stop_root', route_name='stop_root')
def includeme(config):
add_routes(config)
config.add_forbidden_view(forbidden)
config.add_view(login, route_name='login',
renderer='/login.mako')
config.add_view(logout, route_name='logout')
config.add_view(become_root, route_name='become_root')
config.add_view(stop_root, route_name='stop_root')
config.add_view(change_password, route_name='change_password',
renderer='/change_password.mako')
AuthenticationView.defaults(config)