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 { div.field-wrapper label {
margin: 0px;
padding-top: 3px;
text-align: right; 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; 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(): % for name, version in packages.iteritems():
<h3>${name} ${version}</h3> <h3>${name} ${version}</h3>
% endfor % endfor
<br />
<p>Please see <a href="https://rattailproject.org/">rattailproject.org</a> for more info.</p>

View file

@ -10,33 +10,31 @@
</%def> </%def>
<%def name="logo()"> <%def name="logo()">
${h.image(request.static_url('tailbone:static/img/home_logo.png'), "Rattail Logo", id='logo')} ${h.image(request.static_url('tailbone:static/img/home_logo.png'), "Rattail Logo", id='logo')}
</%def>
<%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
${form.field_div('username', form.text('username'))}
${form.field_div('password', form.password('password'))}
<div class="buttons">
${form.submit('submit', "Login")}
<input type="reset" value="Reset" />
</div>
${form.end()}
</div>
</%def> </%def>
${self.logo()} ${self.logo()}
<div class="form"> ${self.login_form()}
${h.form('')}
<input type="hidden" name="referrer" value="${referrer}" />
% 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>
<div class="buttons">
${h.submit('submit', "Login")}
<input type="reset" value="Reset" />
</div>
${h.end_form()}
</div>

View file

@ -3,40 +3,92 @@
<html> <html>
<head> <head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <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" /> <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/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.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.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(): % if not request.rattail_config.production():
<style type="text/css"> <style type="text/css">
.ui-page-theme-a { background-image: url(${request.static_url('tailbone:static/img/testing.png')}); } .ui-page-theme-a { background-image: url(${request.static_url('tailbone:static/img/testing.png')}); }
</style> </style>
% endif % endif
</head> </head>
${self.mobile_body()}
<body>
<div data-role="page">
<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')}
<h1>${self.global_title()}</h1>
</div>
<div role="main" class="ui-content">
% if capture(self.title):
<h2>${self.title()}</h2>
% endif
${self.body()}
</div>
<div data-role="footer">
<h4>powered by ${h.link_to("Rattail", url('mobile.about'))}</h4>
</div>
</div>
</body>
</html> </html>
<%def name="global_title()">${"[STAGE] " if not request.rattail_config.production() else ''}Rattail Demo</%def> <%def name="mobile_body()">
<body>
<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">
${self.mobile_header_link()}
<h1>${self.global_title()}</h1>
</div>
</%def>
<%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>
</%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 -*- ## -*- coding: utf-8 -*-
<%inherit file="/mobile/base.mako" /> <%inherit file="/mobile/base.mako" />
<%def name="title()"></%def> <%def name="title()">Home</%def>
<%def name="page_title()"></%def>
<div style="text-align: center;"> <div style="text-align: center;">
${h.image(request.static_url('tailbone:static/img/home_logo.png'), "Rattail Logo", width='400')} ${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,164 +28,181 @@ from __future__ import unicode_literals, absolute_import
from rattail.db.auth import authenticate_user, set_user_password from rattail.db.auth import authenticate_user, set_user_password
import formencode import formencode as fe
from pyramid.httpexceptions import HTTPFound, HTTPForbidden from pyramid.httpexceptions import HTTPForbidden
from pyramid.security import remember, forget from pyramid.security import remember, forget
from pyramid_simpleform import Form from pyramid_simpleform import Form
from webhelpers.html import literal from webhelpers.html import tags, literal
from webhelpers.html import tags
from tailbone import forms
from tailbone.db import Session from tailbone.db import Session
from tailbone.forms.simpleform import FormRenderer from tailbone.views import View
def forbidden(request): class UserLogin(fe.Schema):
"""
Access forbidden view.
This is triggered whenever access is not allowed for an otherwise
appropriate view.
"""
msg = literal("You do not have permission to do that.")
if not request.authenticated_userid:
msg += literal("&nbsp; (Perhaps you should %s?)" %
tags.link_to("log in", 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())
class UserLogin(formencode.Schema):
allow_extra_fields = True allow_extra_fields = True
filter_extra_fields = True filter_extra_fields = True
username = formencode.validators.NotEmpty() username = fe.validators.NotEmpty()
password = formencode.validators.NotEmpty() password = fe.validators.NotEmpty()
def login(request): class CurrentPasswordCorrect(fe.validators.FancyValidator):
"""
The login view, responsible for displaying and handling the login form.
"""
referrer = request.get_referrer()
# Redirect if already logged in.
if request.user:
return HTTPFound(location=referrer)
form = Form(request, schema=UserLogin)
if form.validate():
user = authenticate_user(Session(),
form.data['username'],
form.data['password'])
if user:
headers = remember(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")
return {'form': FormRenderer(form), 'referrer': referrer}
def logout(request):
"""
View responsible for logging out the current user.
This deletes/invalidates the current session and then redirects to the
login page.
"""
request.session.delete()
request.session.invalidate()
headers = forget(request)
referrer = request.get_referrer()
return HTTPFound(location=referrer, headers=headers)
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): def _to_python(self, value, state):
user = state user = state
if not authenticate_user(Session, user.username, value): if not authenticate_user(Session, user.username, value):
raise formencode.Invalid("The password is incorrect.", value, state) raise fe.Invalid("The password is incorrect.", value, state)
return value return value
class ChangePassword(formencode.Schema): class ChangePassword(fe.Schema):
allow_extra_fields = True allow_extra_fields = True
filter_extra_fields = True filter_extra_fields = True
current_password = formencode.All( current_password = fe.All(
formencode.validators.NotEmpty(), fe.validators.NotEmpty(),
CurrentPasswordCorrect()) CurrentPasswordCorrect())
new_password = formencode.validators.NotEmpty() new_password = fe.validators.NotEmpty()
confirm_password = formencode.validators.NotEmpty() confirm_password = fe.validators.NotEmpty()
chained_validators = [formencode.validators.FieldsMatch( chained_validators = [fe.validators.FieldsMatch(
'new_password', 'confirm_password')] 'new_password', 'confirm_password')]
def change_password(request): class AuthenticationView(View):
"""
Allows a user to change his or her password.
"""
if not request.user: def forbidden(self):
return HTTPFound(location=request.route_url('home')) """
Access forbidden view.
form = Form(request, schema=ChangePassword, state=request.user) This is triggered whenever access is not allowed for an otherwise
if form.validate(): appropriate view.
set_user_password(request.user, form.data['new_password']) """
return HTTPFound(location=request.get_referrer()) msg = literal("You do not have permission to do that.")
if not self.request.authenticated_userid:
msg += literal("&nbsp; (Perhaps you should %s?)" %
tags.link_to("log in", self.request.route_url('login')))
# Store current URL in session, for smarter redirect after login.
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())
return {'form': FormRenderer(form)} def login(self, mobile=False):
"""
The login view, responsible for displaying and handling the login form.
"""
home = 'mobile.home' if mobile else 'home'
referrer = self.request.get_referrer(default=self.request.route_url(home))
# 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(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:
# okay now they're truly logged in
headers = remember(self.request, user.uuid)
# Treat URL from session as referrer, if available.
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
def mobile_login(self):
return self.login(mobile=True)
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)
def mobile_logout(self):
return self.logout(mobile=True)
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'))
form = Form(self.request, schema=ChangePassword, state=self.request.user)
if form.validate():
set_user_password(self.request.user, form.data['new_password'])
return self.redirect(self.request.get_referrer())
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 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_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 add_routes(config):
config.add_route('login', '/login')
config.add_route('logout', '/logout')
config.add_route('become_root', '/root/yes')
config.add_route('stop_root', '/root/no')
config.add_route('change_password', '/change-password')
def includeme(config): def includeme(config):
add_routes(config) AuthenticationView.defaults(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')