Merge github.com:web2py/web2py

This commit is contained in:
Michele Comitini
2013-01-10 00:25:31 +01:00
24 changed files with 1292 additions and 487 deletions
-1
View File
@@ -121,7 +121,6 @@ commit:
push:
hg push
git push
git push mdipierro
tag:
git tag -l '$(S)'
hg tag -l '$(S)'
+1 -1
View File
@@ -1 +1 @@
Version 2.4.1-alpha.2+timestamp.2012.12.28.01.21.20
Version 2.4.1-alpha.2+timestamp.2013.01.09.16.56.34
+16
View File
@@ -163,6 +163,22 @@ class Servers:
"tcp://127.0.0.1:9996")
mongrel2_handler(app, conn, debug=False)
@staticmethod
def motor(app, address, **options):
#https://github.com/rpedroso/motor
import motor
app = motor.WSGIContainer(app)
http_server = motor.HTTPServer(app)
http_server.listen(address=address[0], port=address[1])
#http_server.start(2)
motor.IOLoop.instance().start()
@staticmethod
def pulsar(app, address, **options):
from pulsar.apps import wsgi
sys.argv = ['anyserver.py']
s = wsgi.WSGIServer(callable=app, bind="%s:%d" % address)
s.start()
def run(servername, ip, port, softcron=True, logging=False, profiler=None):
if logging:
+23
View File
@@ -212,3 +212,26 @@ def toggle_breakpoint():
except Exception, e:
session.flash = str(e)
return response.json({'ok': ok, 'lineno': lineno})
def list_breakpoints():
"Return a list of linenumbers for current breakpoints"
breakpoints = []
ok = None
try:
filename = os.path.join(request.env['applications_parent'],
'applications', request.vars.filename)
# normalize path name: replace slashes, references, etc...
filename = os.path.normpath(os.path.normcase(filename))
for bp in qdb_debugger.do_list_breakpoint():
no, bp_filename, bp_lineno, temporary, enabled, hits, cond = bp
# normalize path name: replace slashes, references, etc...
bp_filename = os.path.normpath(os.path.normcase(bp_filename))
if filename == bp_filename:
breakpoints.append(bp_lineno)
ok = True
except Exception, e:
session.flash = str(e)
ok = False
return response.json({'ok': ok, 'breakpoints': breakpoints})
+3 -1
View File
@@ -1493,7 +1493,9 @@ def errors():
else:
for item in request.vars:
if item[:7] == 'delete_':
# delete_all} rows doesn't contain any ticket
# Remove anything else as requested
if item[:7] == 'delete_' and (not item == "delete_all}"):
os.unlink(apath('%s/errors/%s' % (app, item[7:]), r=request))
func = lambda p: os.stat(apath('%s/errors/%s' %
(app, p), r=request)).st_mtime
+43 -2
View File
@@ -148,8 +148,12 @@ function getSelectionRange() {
return sel;
}
function doToggleBreakpoint(filename, url) {
var sel = getSelectionRange();
function doToggleBreakpoint(filename, url, sel) {
if (sel==null) {
// use cursor position to determine the breakpoint line
// (gutter already tell us the selected line)
sel = getSelectionRange();
}
var dataForPost = prepareMultiPartPOST(new Array(
prepareDataForSave('filename', filename),
prepareDataForSave('sel_start', sel["start"]),
@@ -199,6 +203,43 @@ function doToggleBreakpoint(filename, url) {
return false;
}
// on load, update all breakpoints markers:
function doListBreakpoints(filename, url) {
var dataForPost = prepareMultiPartPOST(new Array(
prepareDataForSave('filename', filename)
));
jQuery.ajax({
type: "POST",
contentType: 'multipart/form-data;boundary="'+dataForPost[1]+'"',
url: url,
dataType: "json",
data: dataForPost[0],
timeout: 5000,
beforeSend: function(xhr) {
xhr.setRequestHeader('web2py-component-location',
document.location);
xhr.setRequestHeader('web2py-component-element',
'doListBreakpoints');},
success: function(json,text,xhr){
try {
if (json.error) {
window.location.href=json.redirect;
} else {
if (window.mirror) {
for (i in json.breakpoints) {
lineno = json.breakpoints[i];
// mark the breakpoint if ok=True
editor.setMarker(lineno-1,
"<span style='color: red'>●</span> %N%");
}
}
}
} catch(e) { on_error(); }
},
error: function(json) { on_error(); }
});
return false;
}
function keepalive(url) {
jQuery.ajax({
+14 -11
View File
@@ -44,12 +44,25 @@ function web2py_event_handlers() {
doc.on('keyup', 'input.double, input.decimal', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();});
var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?";
doc.on('click', "input[type='checkbox'].delete", function(){if(this.checked) if(!confirm(confirm_message)) this.checked=false;});
doc.ajaxSuccess(function(e, xhr) {
var redirect=xhr.getResponseHeader('web2py-redirect-location');
if (redirect != null) {
var command=xhr.getResponseHeader('web2py-component-command');
var flash=xhr.getResponseHeader('web2py-component-flash');
if (redirect !== null) {
window.location = redirect;
};
if(command !== null){
eval(decodeURIComponent(command));
}
if(flash) {
jQuery('.flash')
.html(decodeURIComponent(flash))
.append('<span id="closeflash">&times;</span>')
.slideDown();
}
});
doc.ajaxError(function(e, xhr, settings, exception) {
doc.off('click', '.flash')
switch(xhr.status){
@@ -98,8 +111,6 @@ function web2py_ajax_page(method, action, data, target) {
'complete':function(xhr,text){
var html=xhr.responseText;
var content=xhr.getResponseHeader('web2py-component-content');
var command=xhr.getResponseHeader('web2py-component-command');
var flash=xhr.getResponseHeader('web2py-component-flash');
var t = jQuery('#'+target);
if(content=='prepend') t.prepend(html);
else if(content=='append') t.append(html);
@@ -107,14 +118,6 @@ function web2py_ajax_page(method, action, data, target) {
web2py_trap_form(action,target);
web2py_trap_link(target);
web2py_ajax_init('#'+target);
if(command)
eval(decodeURIComponent(command));
if(flash) {
jQuery('.flash')
.html(decodeURIComponent(flash))
.append('<span id="closeflash">&times;</span>')
.slideDown();
}
}
});
}
+16 -3
View File
@@ -25,6 +25,13 @@
<link rel="stylesheet" href="{{=cm}}/lib/util/dialog.css">
<script src="{{=cm}}/emmet.min.js"></script>
<script language="Javascript" type="text/javascript" src="{{=URL('static','js/ajax_editor.js')}}"></script>
<script language="Javascript" type="text/javascript">
jQuery(document).ready(function(){
doListBreakpoints({{=XML("'%s','%s://%s%s'" % (filename,
request.env['wsgi_url_scheme'], request.env['http_host'],
URL(c='debug', f='list_breakpoints')))}});
});
</script>
{{elif TEXT_EDITOR == 'ace':}}
<script src="{{=URL(r=request,c='static',f='ace/src/ace.js')}}" type="text/javascript" charset="utf-8"></script>
<script src="{{=URL(r=request,c='static',f='ace/src/theme-%s.js' % TEXT_EDITOR_THEME)}}" type="text/javascript" charset="utf-8"></script>
@@ -103,7 +110,7 @@ jQuery(document).ready(function(){
{{if filetype=='python':}}
{{=A(SPAN(T('toggle breakpoint')),
_value="breakpoint", _name="breakpoint",
_onclick="return doToggleBreakpoint('%s','%s://%s%s');" % (filename,
_onclick="return doToggleBreakpoint('%s','%s://%s%s',null);" % (filename,
request.env['wsgi_url_scheme'], request.env['http_host'],
URL(c='debug', f='toggle_breakpoint')),
_class="button special btn btn-inverse")}}
@@ -202,7 +209,13 @@ jQuery(document).ready(function(){
autofocus: true,
onCursorActivity: function() {
editor.setLineClass(hlLine, null, null);
hlLine = editor.setLineClass(editor.getCursor().line, null, "activeline");}
hlLine = editor.setLineClass(editor.getCursor().line, null, "activeline");},
onGutterClick: function(cm, n) {
sel = {start: n, end: n, data: ''};
doToggleBreakpoint({{=XML("'%s','%s://%s%s',sel" % (filename,
request.env['wsgi_url_scheme'], request.env['http_host'],
URL(c='debug', f='toggle_breakpoint')))}});
}
};
var editor = CodeMirror.fromTextArea(
document.getElementById("body"),cm_opts);
@@ -293,4 +306,4 @@ window.onload = function() {
{{pass}}
</div>
</div>
<!-- end "edit" block -->
<!-- end "edit" block -->
+14 -11
View File
@@ -44,12 +44,25 @@ function web2py_event_handlers() {
doc.on('keyup', 'input.double, input.decimal', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();});
var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?";
doc.on('click', "input[type='checkbox'].delete", function(){if(this.checked) if(!confirm(confirm_message)) this.checked=false;});
doc.ajaxSuccess(function(e, xhr) {
var redirect=xhr.getResponseHeader('web2py-redirect-location');
if (redirect != null) {
var command=xhr.getResponseHeader('web2py-component-command');
var flash=xhr.getResponseHeader('web2py-component-flash');
if (redirect !== null) {
window.location = redirect;
};
if(command !== null){
eval(decodeURIComponent(command));
}
if(flash) {
jQuery('.flash')
.html(decodeURIComponent(flash))
.append('<span id="closeflash">&times;</span>')
.slideDown();
}
});
doc.ajaxError(function(e, xhr, settings, exception) {
doc.off('click', '.flash')
switch(xhr.status){
@@ -98,8 +111,6 @@ function web2py_ajax_page(method, action, data, target) {
'complete':function(xhr,text){
var html=xhr.responseText;
var content=xhr.getResponseHeader('web2py-component-content');
var command=xhr.getResponseHeader('web2py-component-command');
var flash=xhr.getResponseHeader('web2py-component-flash');
var t = jQuery('#'+target);
if(content=='prepend') t.prepend(html);
else if(content=='append') t.append(html);
@@ -107,14 +118,6 @@ function web2py_ajax_page(method, action, data, target) {
web2py_trap_form(action,target);
web2py_trap_link(target);
web2py_ajax_init('#'+target);
if(command)
eval(decodeURIComponent(command));
if(flash) {
jQuery('.flash')
.html(decodeURIComponent(flash))
.append('<span id="closeflash">&times;</span>')
.slideDown();
}
}
});
}
+14 -11
View File
@@ -44,12 +44,25 @@ function web2py_event_handlers() {
doc.on('keyup', 'input.double, input.decimal', function(){this.value=this.value.reverse().replace(/[^0-9\-\.,]|[\-](?=.)|[\.,](?=[0-9]*[\.,])/g,'').reverse();});
var confirm_message = (typeof w2p_ajax_confirm_message != 'undefined') ? w2p_ajax_confirm_message : "Are you sure you want to delete this object?";
doc.on('click', "input[type='checkbox'].delete", function(){if(this.checked) if(!confirm(confirm_message)) this.checked=false;});
doc.ajaxSuccess(function(e, xhr) {
var redirect=xhr.getResponseHeader('web2py-redirect-location');
if (redirect != null) {
var command=xhr.getResponseHeader('web2py-component-command');
var flash=xhr.getResponseHeader('web2py-component-flash');
if (redirect !== null) {
window.location = redirect;
};
if(command !== null){
eval(decodeURIComponent(command));
}
if(flash) {
jQuery('.flash')
.html(decodeURIComponent(flash))
.append('<span id="closeflash">&times;</span>')
.slideDown();
}
});
doc.ajaxError(function(e, xhr, settings, exception) {
doc.off('click', '.flash')
switch(xhr.status){
@@ -98,8 +111,6 @@ function web2py_ajax_page(method, action, data, target) {
'complete':function(xhr,text){
var html=xhr.responseText;
var content=xhr.getResponseHeader('web2py-component-content');
var command=xhr.getResponseHeader('web2py-component-command');
var flash=xhr.getResponseHeader('web2py-component-flash');
var t = jQuery('#'+target);
if(content=='prepend') t.prepend(html);
else if(content=='append') t.append(html);
@@ -107,14 +118,6 @@ function web2py_ajax_page(method, action, data, target) {
web2py_trap_form(action,target);
web2py_trap_link(target);
web2py_ajax_init('#'+target);
if(command)
eval(decodeURIComponent(command));
if(flash) {
jQuery('.flash')
.html(decodeURIComponent(flash))
.append('<span id="closeflash">&times;</span>')
.slideDown();
}
}
});
}
+1 -1
View File
@@ -4,7 +4,7 @@
{{
if request.args(0)=='login':
if not 'register' in auth.settings.actions_disabled:
form.add_button(T('Register'),URL(args='register'),_class='btn')
form.add_button(T('Register'),URL(args='register', vars={'_next': request.vars._next} if request.vars._next else None),_class='btn')
pass
if not 'request_reset_password' in auth.settings.actions_disabled:
form.add_button(T('Lost Password'),URL(args='request_reset_password'),_class='btn')
+1 -1
View File
@@ -10,7 +10,7 @@ Web2Py framework modules
========================
"""
__all__ = ['A', 'B', 'BEAUTIFY', 'BODY', 'BR', 'CAT', 'CENTER', 'CLEANUP', 'CODE', 'CRYPT', 'DAL', 'DIV', 'EM', 'EMBED', 'FIELDSET', 'FORM', 'Field', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEAD', 'HR', 'HTML', 'HTTP', 'I', 'IFRAME', 'IMG', 'INPUT', 'IS_ALPHANUMERIC', 'IS_DATE', 'IS_DATETIME', 'IS_DATETIME_IN_RANGE', 'IS_DATE_IN_RANGE', 'IS_DECIMAL_IN_RANGE', 'IS_EMAIL', 'IS_EMPTY_OR', 'IS_EQUAL_TO', 'IS_EXPR', 'IS_FLOAT_IN_RANGE', 'IS_IMAGE', 'IS_INT_IN_RANGE', 'IS_IN_DB', 'IS_IN_SET', 'IS_IPV4', 'IS_LENGTH', 'IS_LIST_OF', 'IS_LOWER', 'IS_MATCH', 'IS_NOT_EMPTY', 'IS_NOT_IN_DB', 'IS_NULL_OR', 'IS_SLUG', 'IS_STRONG', 'IS_TIME', 'IS_UPLOAD_FILENAME', 'IS_UPPER', 'IS_URL', 'LABEL', 'LEGEND', 'LI', 'LINK', 'LOAD', 'MARKMIN', 'MENU', 'META', 'OBJECT', 'OL', 'ON', 'OPTGROUP', 'OPTION', 'P', 'PRE', 'SCRIPT', 'SELECT', 'SPAN', 'SQLFORM', 'SQLTABLE', 'STRONG', 'STYLE', 'TABLE', 'TAG', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD', 'TITLE', 'TR', 'TT', 'UL', 'URL', 'XHTML', 'XML', 'redirect', 'current', 'embed64']
__all__ = ['A', 'B', 'BEAUTIFY', 'BODY', 'BR', 'CAT', 'CENTER', 'CLEANUP', 'CODE', 'CRYPT', 'DAL', 'DIV', 'EM', 'EMBED', 'FIELDSET', 'FORM', 'Field', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEAD', 'HR', 'HTML', 'HTTP', 'I', 'IFRAME', 'IMG', 'INPUT', 'IS_ALPHANUMERIC', 'IS_DATE', 'IS_DATETIME', 'IS_DATETIME_IN_RANGE', 'IS_DATE_IN_RANGE', 'IS_DECIMAL_IN_RANGE', 'IS_EMAIL', 'IS_EMPTY_OR', 'IS_EQUAL_TO', 'IS_EXPR', 'IS_FLOAT_IN_RANGE', 'IS_IMAGE', 'IS_JSON', 'IS_INT_IN_RANGE', 'IS_IN_DB', 'IS_IN_SET', 'IS_IPV4', 'IS_LENGTH', 'IS_LIST_OF', 'IS_LOWER', 'IS_MATCH', 'IS_NOT_EMPTY', 'IS_NOT_IN_DB', 'IS_NULL_OR', 'IS_SLUG', 'IS_STRONG', 'IS_TIME', 'IS_UPLOAD_FILENAME', 'IS_UPPER', 'IS_URL', 'LABEL', 'LEGEND', 'LI', 'LINK', 'LOAD', 'MARKMIN', 'MENU', 'META', 'OBJECT', 'OL', 'ON', 'OPTGROUP', 'OPTION', 'P', 'PRE', 'SCRIPT', 'SELECT', 'SPAN', 'SQLFORM', 'SQLTABLE', 'STRONG', 'STYLE', 'TABLE', 'TAG', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD', 'TITLE', 'TR', 'TT', 'UL', 'URL', 'XHTML', 'XML', 'redirect', 'current', 'embed64']
from globals import current
from html import *
@@ -121,13 +121,13 @@ server for requests. It can be used for the optional"scope" parameters for Face
"""
# Create an OpenerDirector with support
# for Basic HTTP Authentication...
auth_handler = urllib2.HTTPBasicAuthHandler()
auth_handler.add_password(None,
uri,
self.client_id,
self.client_secret)
opener = urllib2.build_opener(auth_handler)
password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_mgr.add_password(realm=None,
uri=uri,
user=self.client_id,
passwd=self.client_secret)
handler = urllib2.HTTPBasicAuthHandler(password_mgr)
opener = urllib2.build_opener(handler)
return opener
def accessToken(self):
+16 -13
View File
@@ -129,18 +129,21 @@ class OpenIDAuth(object):
def _define_alt_login_table(self):
"""
Define the OpenID login table.
Note: type is what I used for our project. We're going to support 'fackbook' and
'plurk' alternate login methods. Otherwise it's always 'openid' and you
Note: oidtype is what I used for our project.
We're going to support 'fackbook' and
'plurk' alternate login methods.
Otherwise it's always 'openid' and you
may not need it. This should be easy to changed.
(Just remove the field of "type" and remove the
"and db.alt_logins.type == type_" in _find_matched_openid function)
"and db.alt_logins.oidtype == type_"
in _find_matched_openid function)
"""
db = self.db
table = db.define_table(
self.table_alt_logins_name,
Field('username', length=512, default=''),
Field('type', length=128, default='openid', readable=False),
Field('user', self.table_user, readable=False),
Field('oidtype', length=128, default='openid', readable=False),
Field('oiduser', self.table_user, readable=False),
)
table.username.requires = IS_NOT_IN_DB(db, table.username)
self.table_alt_logins = table
@@ -213,7 +216,7 @@ class OpenIDAuth(object):
# Get existed OpenID user
user = db(
self.table_user.id == alt_login.user).select().first()
self.table_user.id == alt_login.oiduser).select().first()
if user:
if current.session.w2popenid:
del(current.session.w2popenid)
@@ -230,7 +233,7 @@ class OpenIDAuth(object):
Get the matched OpenID for given
"""
query = (
(db.alt_logins.username == oid) & (db.alt_logins.type == type_))
(db.alt_logins.username == oid) & (db.alt_logins.oidtype == type_))
alt_login = db(query).select().first() # Get the OpenID record
return alt_login
@@ -239,7 +242,7 @@ class OpenIDAuth(object):
Associate the user logged in with given OpenID
"""
# print "[DB] %s authenticated" % oid
self.db.alt_logins.insert(username=oid, user=user.id)
self.db.alt_logins.insert(username=oid, oiduser=user.id)
def _form_with_notification(self):
"""
@@ -400,7 +403,7 @@ width: 400px;
if 'delete_openid' in request.vars:
self.remove_openid(request.vars.delete_openid)
query = self.db.alt_logins.user == self.auth.user.id
query = self.db.alt_logins.oiduser == self.auth.user.id
alt_logins = self.db(query).select()
l = []
for alt_login in alt_logins:
@@ -529,7 +532,7 @@ class Web2pyStore(OpenIDStore):
self.database.define_table(self.table_oid_nonces_name,
Field('server_url',
'string', length=2047, required=True),
Field('timestamp',
Field('itimestamp',
'integer', required=True),
Field('salt', 'string',
length=40, required=True)
@@ -591,12 +594,12 @@ class Web2pyStore(OpenIDStore):
db = self.database
if abs(timestamp - time.time()) > nonce.SKEW:
return False
query = (db.oid_nonces.server_url == server_url) & (db.oid_nonces.timestamp == timestamp) & (db.oid_nonces.salt == salt)
query = (db.oid_nonces.server_url == server_url) & (db.oid_nonces.itimestamp == timestamp) & (db.oid_nonces.salt == salt)
if db(query).count() > 0:
return False
else:
db.oid_nonces.insert(server_url=server_url,
timestamp=timestamp,
itimestamp=timestamp,
salt=salt)
return True
@@ -628,7 +631,7 @@ class Web2pyStore(OpenIDStore):
"""
db = self.database
query = (db.oid_nonces.timestamp < time.time() - nonce.SKEW)
query = (db.oid_nonces.itimestamp < time.time() - nonce.SKEW)
return db(query).delete()
def cleanupAssociations(self):
+499 -340
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -991,7 +991,7 @@ class DIV(XmlComponent):
>>> a=FORM( INPUT(_type='text'), SELECT(range(1)), TEXTAREA() )
>>> for c in a.elements('input, select, textarea'): c['_disabled'] = 'disabled'
>>> a.xml()
'<form action="" enctype="multipart/form-data" method="post"><input disabled="disabled" type="text" /><select disabled="disabled"><option value="0">0</option></select><textarea cols="40" disabled="disabled" rows="10"></textarea></form>'
'<form action="#" enctype="multipart/form-data" method="post"><input disabled="disabled" type="text" /><select disabled="disabled"><option value="0">0</option></select><textarea cols="40" disabled="disabled" rows="10"></textarea></form>'
Elements that are matched can also be replaced or removed by specifying
a "replace" argument (note, a list of the original matching elements
@@ -1478,9 +1478,9 @@ class A(DIV):
(self['component'], self['target'] or '', d)
self['_href'] = self['_href'] or '#null'
elif self['callback']:
returnfalse = "var e = arguments[0] || window.event; e.cancelBubble=true; if (e.stopPropagation) e.stopPropagation();"
returnfalse = "var e = arguments[0] || window.event; e.cancelBubble=true; if (e.stopPropagation) {e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault();}"
if d:
self['_onclick'] = "if(confirm(w2p_ajax_confirm_message||'Are you sure you want o delete this object?')){ajax('%s',[],'%s');%s};%s" % \
self['_onclick'] = "if(confirm(w2p_ajax_confirm_message||'Are you sure you want to delete this object?')){ajax('%s',[],'%s');%s};%s" % \
(self['callback'], self['target'] or '', d, returnfalse)
else:
self['_onclick'] = "ajax('%s',[],'%s');%sreturn false" % \
@@ -1932,7 +1932,7 @@ class FORM(DIV):
>>> from validators import IS_NOT_EMPTY
>>> form=FORM(INPUT(_name=\"test\", requires=IS_NOT_EMPTY()))
>>> form.xml()
'<form action=\"\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"test\" type=\"text\" /></form>'
'<form action=\"#\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"test\" type=\"text\" /></form>'
a FORM is container for INPUT, TEXTAREA, SELECT and other helpers
@@ -2033,7 +2033,7 @@ class FORM(DIV):
def _postprocessing(self):
if not '_action' in self.attributes:
self['_action'] = ''
self['_action'] = '#'
if not '_method' in self.attributes:
self['_method'] = 'post'
if not '_enctype' in self.attributes:
@@ -2415,25 +2415,25 @@ def test():
<table><tr><td>a</td><td>b</td><td>c</td></tr><tr><td>d</td><td>e</td><td>f</td></tr><tr><td>1</td><td>2</td><td>3</td></tr></table>
>>> form=FORM(INPUT(_type='text', _name='myvar', requires=IS_EXPR('int(value)<10')))
>>> print form.xml()
<form action=\"\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"myvar\" type=\"text\" /></form>
<form action=\"#\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"myvar\" type=\"text\" /></form>
>>> print form.accepts({'myvar':'34'}, formname=None)
False
>>> print form.xml()
<form action="" enctype="multipart/form-data" method="post"><input class="invalidinput" name="myvar" type="text" value="34" /><div class="error_wrapper"><div class="error" id="myvar__error">invalid expression</div></div></form>
<form action="#" enctype="multipart/form-data" method="post"><input class="invalidinput" name="myvar" type="text" value="34" /><div class="error_wrapper"><div class="error" id="myvar__error">invalid expression</div></div></form>
>>> print form.accepts({'myvar':'4'}, formname=None, keepvalues=True)
True
>>> print form.xml()
<form action=\"\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"myvar\" type=\"text\" value=\"4\" /></form>
<form action=\"#\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"myvar\" type=\"text\" value=\"4\" /></form>
>>> form=FORM(SELECT('cat', 'dog', _name='myvar'))
>>> print form.accepts({'myvar':'dog'}, formname=None, keepvalues=True)
True
>>> print form.xml()
<form action=\"\" enctype=\"multipart/form-data\" method=\"post\"><select name=\"myvar\"><option value=\"cat\">cat</option><option selected=\"selected\" value=\"dog\">dog</option></select></form>
<form action=\"#\" enctype=\"multipart/form-data\" method=\"post\"><select name=\"myvar\"><option value=\"cat\">cat</option><option selected=\"selected\" value=\"dog\">dog</option></select></form>
>>> form=FORM(INPUT(_type='text', _name='myvar', requires=IS_MATCH('^\w+$', 'only alphanumeric!')))
>>> print form.accepts({'myvar':'as df'}, formname=None)
False
>>> print form.xml()
<form action="" enctype="multipart/form-data" method="post"><input class="invalidinput" name="myvar" type="text" value="as df" /><div class="error_wrapper"><div class="error" id="myvar__error">only alphanumeric!</div></div></form>
<form action="#" enctype="multipart/form-data" method="post"><input class="invalidinput" name="myvar" type="text" value="as df" /><div class="error_wrapper"><div class="error" id="myvar__error">only alphanumeric!</div></div></form>
>>> session={}
>>> form=FORM(INPUT(value=\"Hello World\", _name=\"var\", requires=IS_MATCH('^\w+$')))
>>> if form.accepts({}, session,formname=None): print 'passed'
+4
View File
@@ -1686,6 +1686,9 @@ SQLITE = set((
'WHERE',
))
MONGODB_NONRESERVED = set(('SAFE',))
# remove from here when you add a list.
JDBCSQLITE = SQLITE
DB2 = INFORMIX = INGRES = JDBCPOSTGRESQL = COMMON
@@ -1708,6 +1711,7 @@ ADAPTERS = {
'jdbc:sqlite': JDBCSQLITE,
'jdbc:postgres': JDBCPOSTGRESQL,
'common': COMMON,
'mongodb_nonreserved': MONGODB_NONRESERVED
}
ADAPTERS['all'] = reduce(lambda a, b: a.union(b), (
+4
View File
@@ -20,6 +20,10 @@ except ImportError:
import contrib.simplejson as json_parser # fallback to pure-Python module
def loads_json(o):
# deserialize a json string
return json_parser.loads(o)
def custom_json(o):
if hasattr(o, 'custom_json') and callable(o.custom_json):
return o.custom_json()
+64 -46
View File
@@ -30,6 +30,7 @@ from utils import md5_hash
from validators import IS_EMPTY_OR, IS_NOT_EMPTY, IS_LIST_OF, IS_DATE, \
IS_DATETIME, IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE, IS_STRONG
import serializers
import datetime
import urllib
import re
@@ -39,6 +40,9 @@ import inspect
import settings
is_gae = settings.global_settings.web2py_runtime_gae
table_field = re.compile('[\w_]+\.[\w_]+')
widget_class = re.compile('^\w*')
@@ -164,11 +168,9 @@ class TimeWidget(StringWidget):
class DateWidget(StringWidget):
_class = 'date'
class DatetimeWidget(StringWidget):
_class = 'datetime'
class TextWidget(FormWidget):
_class = 'text'
@@ -184,6 +186,22 @@ class TextWidget(FormWidget):
attr = cls._attributes(field, default, **attributes)
return TEXTAREA(**attr)
class JSONWidget(FormWidget):
_class = 'json'
@classmethod
def widget(cls, field, value, **attributes):
"""
generates a TEXTAREA for JSON notation.
see also: :meth:`FormWidget.widget`
"""
if not isinstance(value, basestring):
if value is not None:
value = serializers.json(value)
default = dict(value=value)
attr = cls._attributes(field, default, **attributes)
return TEXTAREA(**attr)
class BooleanWidget(FormWidget):
_class = 'boolean'
@@ -235,7 +253,6 @@ class OptionsWidget(FormWidget):
raise SyntaxError(
'widget cannot determine options of %s' % field)
opts = [OPTION(v, _value=k) for (k, v) in options]
return SELECT(*opts, **attr)
@@ -523,20 +540,25 @@ class UploadWidget(FormWidget):
requires = attr["requires"]
if requires == [] or isinstance(requires, IS_EMPTY_OR):
inp = DIV(inp, '[',
A(current.T(
UploadWidget.GENERIC_DESCRIPTION), _href=url),
'|',
INPUT(_type='checkbox',
_name=field.name + cls.ID_DELETE_SUFFIX,
_id=field.name + cls.ID_DELETE_SUFFIX),
LABEL(current.T(cls.DELETE_FILE),
_for=field.name + cls.ID_DELETE_SUFFIX),
']', br, image)
inp = DIV(inp,
SPAN('[',
A(current.T(
UploadWidget.GENERIC_DESCRIPTION), _href=url),
'|',
INPUT(_type='checkbox',
_name=field.name + cls.ID_DELETE_SUFFIX,
_id=field.name + cls.ID_DELETE_SUFFIX),
LABEL(current.T(cls.DELETE_FILE),
_for=field.name + cls.ID_DELETE_SUFFIX,
_style='display:inline'),
']', _style='white-space:nowrap'),
br, image)
else:
inp = DIV(inp, '[',
A(cls.GENERIC_DESCRIPTION, _href=url),
']', br, image)
inp = DIV(inp,
SPAN('[',
A(cls.GENERIC_DESCRIPTION, _href=url),
']', _style='white-space:nowrap'),
br, image)
return inp
@classmethod
@@ -716,7 +738,7 @@ def formstyle_table2cols(form, fields):
def formstyle_divs(form, fields):
''' divs only '''
table = TAG['']()
table = FIELDSET()
for id, label, controls, help in fields:
_help = DIV(help, _class='w2p_fc')
_controls = DIV(controls, _class='w2p_fw')
@@ -748,7 +770,7 @@ def formstyle_ul(form, fields):
def formstyle_bootstrap(form, fields):
''' bootstrap format form layout '''
form['_class'] = 'form-horizontal'
form.add_class('form-horizontal')
parent = FIELDSET()
for id, label, controls, help in fields:
# wrappers
@@ -782,12 +804,12 @@ def formstyle_bootstrap(form, fields):
if _submit:
# submit button has unwrapped label and controls, different class
parent.append(DIV(label, controls, _class='form-actions'))
parent.append(DIV(label, controls, _class='form-actions', _id=id))
# unflag submit (possible side effect)
_submit = False
else:
# unwrapped label
parent.append(DIV(label, _controls, _class='control-group'))
parent.append(DIV(label, _controls, _class='control-group', _id=id))
return parent
@@ -843,6 +865,7 @@ class SQLFORM(FORM):
widgets = Storage(dict(
string=StringWidget,
text=TextWidget,
json=JSONWidget,
password=PasswordWidget,
integer=IntegerWidget,
double=DoubleWidget,
@@ -1132,7 +1155,7 @@ class SQLFORM(FORM):
# </block>
# when deletable, add delete? checkbox
self.custom.deletable = ''
self.custom.delete = self.custom.deletable = ''
if record and deletable:
#add secondary css class for cascade delete warning
css = 'delete'
@@ -1154,7 +1177,8 @@ class SQLFORM(FORM):
_id=self.FIELDKEY_DELETE_RECORD + SQLFORM.ID_LABEL_SUFFIX),
widget,
col3.get(self.FIELDKEY_DELETE_RECORD, '')))
self.custom.deletable = widget
self.custom.delete = self.custom.deletable = widget
# when writable, add submit button
self.custom.submit = ''
@@ -1807,28 +1831,21 @@ class SQLFORM(FORM):
buttonurl=url(args=[]), callback=None,
delete=None, trap=True):
if showbuttontext:
if callback:
return A(SPAN(_class=ui.get(buttonclass)),
SPAN(T(buttontext), _title=buttontext,
_class=ui.get('buttontext')),
callback=callback, delete=delete,
_class=trap_class(ui.get('button'), trap))
else:
return A(SPAN(_class=ui.get(buttonclass)),
SPAN(T(buttontext), _title=buttontext,
_class=ui.get('buttontext')),
_href=buttonurl,
_class=trap_class(ui.get('button'), trap))
return A(SPAN(_class=ui.get(buttonclass)),
SPAN(T(buttontext), _title=buttontext,
_class=ui.get('buttontext')),
_href=buttonurl,
callback=callback,
delete=delete,
_class=trap_class(ui.get('button'), trap))
else:
if callback:
return A(SPAN(_class=ui.get(buttonclass)),
callback=callback, delete=delete,
_title=buttontext,
_class=trap_class(ui.get('buttontext'), trap))
else:
return A(SPAN(_class=ui.get(buttonclass)),
_href=buttonurl, _title=buttontext,
_class=trap_class(ui.get('buttontext'), trap))
return A(SPAN(_class=ui.get(buttonclass)),
_href=buttonurl,
callback=callback,
delete=delete,
_title=buttontext,
_class=trap_class(ui.get('buttontext'), trap))
dbset = db(query)
tablenames = db._adapter.tables(dbset.query)
if left is not None:
@@ -1948,8 +1965,8 @@ class SQLFORM(FORM):
table = db[request.args[-2]]
if ondelete:
ondelete(table, request.args[-1])
ret = db(table[table._id.name] == request.args[-1]).delete()
return ret
db(table[table._id.name] == request.args[-1]).delete()
redirect(referrer)
exportManager = dict(
csv_with_hidden_cols=(ExporterCSV, 'CSV (hidden cols)'),
@@ -2267,7 +2284,7 @@ class SQLFORM(FORM):
elif not isinstance(value, DIV):
value = field.formatter(value)
trcols.append(TD(value))
row_buttons = TD(_class='row_buttons')
row_buttons = TD(_class='row_buttons',_nowrap=True)
if links and links_in_grid:
toadd = []
for link in links:
@@ -2293,6 +2310,7 @@ class SQLFORM(FORM):
if deletable and (not callable(deletable) or deletable(row)):
row_buttons.append(gridbutton(
'buttondelete', 'Delete',
url(args=['delete', tablename, id]),
callback=url(args=['delete', tablename, id]),
delete='tr'))
if buttons_placement in ['right', 'both']:
+7 -2
View File
@@ -31,6 +31,7 @@ ALLOWED_DATATYPES = [
'datetime',
'upload',
'password',
'json',
]
@@ -62,14 +63,14 @@ class TestFields(unittest.TestCase):
def testFieldTypes(self):
# Check that string, text, and password default length is 512
# Check that string, and password default length is 512
for typ in ['string', 'password']:
self.assert_(Field('abc', typ).length == 512,
"Default length for type '%s' is not 512 or 255" % typ)
# Check that upload default length is 512
self.assert_(Field('abc', 'upload').length == 512,
"Default length for type 'upload' is not 128")
"Default length for type 'upload' is not 512")
# Check that Tables passed in the type creates a reference
self.assert_(Field('abc', Table(None, 'temp')).type
@@ -113,6 +114,10 @@ class TestFields(unittest.TestCase):
self.assertEqual(db.t.insert(a=True), 1)
self.assertEqual(db().select(db.t.a)[0].a, True)
db.t.drop()
db.define_table('t', Field('a', 'json', default={}))
self.assertEqual(db.t.insert(a={}), 1)
self.assertEqual(db().select(db.t.a)[0].a, {})
db.t.drop()
db.define_table('t', Field('a', 'date',
default=datetime.date.today()))
t0 = datetime.date.today()
+1 -1
View File
@@ -74,7 +74,7 @@ class TestBareHelpers(unittest.TestCase):
def testFORM(self):
self.assertEqual(FORM('<>', _a='1', _b='2').xml(),
'<form a="1" action="" b="2" enctype="multipart/form-data" method="post">&lt;&gt;</form>')
'<form a="1" action="#" b="2" enctype="multipart/form-data" method="post">&lt;&gt;</form>')
def testH1(self):
self.assertEqual(H1('<>', _a='1', _b='2').xml(),
+35 -25
View File
@@ -272,7 +272,7 @@ class Mail(object):
cc=None,
bcc=None,
reply_to=None,
sender='%(sender)s',
sender=None,
encoding='utf-8',
raw=False,
headers={}
@@ -360,9 +360,11 @@ class Mail(object):
text = encode_header(text)
return text
sender = sender or self.settings.sender
if not isinstance(self.settings.server, str):
raise Exception('Server address not specified')
if not isinstance(self.settings.sender, str):
if not isinstance(sender, str):
raise Exception('Sender address not specified')
if not raw:
@@ -458,11 +460,11 @@ class Mail(object):
c.set_armor(1)
c.signers_clear()
# search for signing key for From:
for sigkey in c.op_keylist_all(self.settings.sender, 1):
for sigkey in c.op_keylist_all(sender, 1):
if sigkey.can_sign:
c.signers_add(sigkey)
if not c.signers_enum(0):
self.error = 'No key for signing [%s]' % self.settings.sender
self.error = 'No key for signing [%s]' % sender
return False
c.set_passphrase_cb(lambda x, y, z: sign_passphrase)
try:
@@ -619,7 +621,6 @@ class Mail(object):
# no cryptography process as usual
payload = payload_in
sender = sender % dict(sender=self.settings.sender)
payload['From'] = encoded_or_raw(sender.decode(encoding))
origTo = to[:]
if to:
@@ -655,16 +656,16 @@ class Mail(object):
attachments = attachments and [(a.my_filename, a.my_payload) for a in attachments if not raw]
if attachments:
result = mail.send_mail(
sender=self.settings.sender, to=origTo,
sender=sender, to=origTo,
subject=subject, body=text, html=html,
attachments=attachments, **xcc)
elif html and (not raw):
result = mail.send_mail(
sender=self.settings.sender, to=origTo,
sender=sender, to=origTo,
subject=subject, body=text, html=html, **xcc)
else:
result = mail.send_mail(
sender=self.settings.sender, to=origTo,
sender=sender, to=origTo,
subject=subject, body=text, **xcc)
else:
smtp_args = self.settings.server.split(':')
@@ -679,7 +680,7 @@ class Mail(object):
if self.settings.login:
server.login(*self.settings.login.split(':', 1))
result = server.sendmail(
self.settings.sender, to, payload.as_string())
sender, to, payload.as_string())
server.quit()
except Exception, e:
logger.warn('Mail.send failure:%s' % e)
@@ -881,6 +882,7 @@ class Auth(object):
profile_fields=None,
email_case_sensitive=True,
username_case_sensitive=True,
update_fields = ['email'],
)
# ## these are messages that can be customized
default_messages = dict(
@@ -2047,7 +2049,8 @@ class Auth(object):
if not self in self.settings.login_methods:
# do not store password in db
form.vars[passfield] = None
user = self.get_or_create_user(form.vars)
user = self.get_or_create_user(
form.vars, self.settings.update_fields)
break
if not user:
# alternates have failed, maybe because service inaccessible
@@ -2067,7 +2070,8 @@ class Auth(object):
if not self in self.settings.login_methods:
# do not store password in db
form.vars[passfield] = None
user = self.get_or_create_user(form.vars)
user = self.get_or_create_user(
form.vars, self.settings.update_fields)
break
if not user:
self.log_event(self.messages.login_failed_log,
@@ -2087,7 +2091,8 @@ class Auth(object):
if cas_user:
cas_user[passfield] = None
user = self.get_or_create_user(
table_user._filter_fields(cas_user))
table_user._filter_fields(cas_user),
self.settings.update_fields)
elif hasattr(cas, 'login_form'):
return cas.login_form()
else:
@@ -3309,14 +3314,14 @@ class Auth(object):
restrict_search=False,
resolve=True,
extra=None,
menugroups=None):
menu_groups=None):
if not hasattr(self, '_wiki'):
self._wiki = Wiki(self, render=render,
manage_permissions=manage_permissions,
force_prefix=force_prefix,
restrict_search=restrict_search,
env=env, extra=extra or {},
menugroups=menugroups)
menu_groups=menu_groups)
else:
self._wiki.env.update(env or {})
# if resolve is set to True, process request as wiki call
@@ -4694,15 +4699,19 @@ class Expose(object):
class Wiki(object):
everybody = 'everybody'
rows_page = 25
def markmin_render(self, page):
html = MARKMIN(page.body, extra=self.extra,
def markmin_base(self,body):
return MARKMIN(body, extra=self.extra,
url=True, environment=self.env,
autolinks=lambda link: expand_one(link, {})).xml()
html += DIV(_class='w2p_wiki_tags',
*[A(t.strip(), _href=URL(args='_search', vars=dict(q=t)))
for t in page.tags or [] if t.strip()]).xml()
return html
def render_tags(self, tags):
return DIV(
_class='w2p_wiki_tags',
*[A(t.strip(), _href=URL(args='_search', vars=dict(q=t)))
for t in tags or [] if t.strip()])
def markmin_render(self, page):
return self.markmin_base(page.body) + self.render_tags(page.tags).xml()
def html_render(self, page):
html = page.body
@@ -4712,6 +4721,7 @@ class Wiki(object):
html = replace_autolinks(html, lambda link: expand_one(link, {}))
# @{component:name} -> <script>embed component name</script>
html = replace_components(html, self.env)
html = html + self.render_tags(page.tags).xml()
return html
@staticmethod
@@ -4726,7 +4736,7 @@ class Wiki(object):
def __init__(self, auth, env=None, render='markmin',
manage_permissions=False, force_prefix='',
restrict_search=False, extra=None, menugroups=None):
restrict_search=False, extra=None, menu_groups=None):
self.env = env or {}
self.env['component'] = Wiki.component
if render == 'markmin':
@@ -4735,7 +4745,7 @@ class Wiki(object):
render = self.html_render
self.render = render
self.auth = auth
self.menugroups = menugroups
self.menu_groups = menu_groups
if self.auth.user:
self.force_prefix = force_prefix % self.auth.user
else:
@@ -4853,11 +4863,11 @@ class Wiki(object):
return True
def can_see_menu(self):
if self.menugroups is None:
if self.menu_groups is None:
return True
if self.auth.user:
groups = self.auth.user_groups.values()
if any(t in self.menugroups for t in groups):
if any(t in self.menu_groups for t in groups):
return True
return False
+35
View File
@@ -21,6 +21,16 @@ import unicodedata
from cStringIO import StringIO
from utils import simple_hash, web2py_uuid, DIGEST_ALG_BY_SIZE
JSONErrors = (NameError, TypeError, ValueError, AttributeError,
KeyError)
try:
import json as simplejson
except ImportError:
from gluon.contrib import simplejson
from gluon.contrib.simplejson.decoder import JSONDecodeError
JSONErrors += (JSONDecodeError,)
__all__ = [
'CLEANUP',
'CRYPT',
@@ -53,6 +63,7 @@ __all__ = [
'IS_UPLOAD_FILENAME',
'IS_UPPER',
'IS_URL',
'IS_JSON',
]
try:
@@ -300,6 +311,30 @@ class IS_LENGTH(Validator):
return (value, translate(self.error_message)
% dict(min=self.minsize, max=self.maxsize))
class IS_JSON(Validator):
"""
example::
INPUT(_type='text', _name='name',
requires=IS_JSON(error_message="This is not a valid json input")
>>> IS_JSON()('{"a": 100}')
('{"a": 100}', None)
>>> IS_JSON()('spam1234')
('spam1234', 'invalid json')
"""
def __init__(self, error_message='invalid json'):
self.error_message = error_message
def __call__(self, value):
try:
simplejson.loads(value)
return (value, None)
except JSONErrors:
pass
return (value, translate(self.error_message))
class IS_IN_SET(Validator):
"""
@@ -0,0 +1,464 @@
#!/bin/bash
# ------------------------------------------------------------------------------
# Description : Installation and basic configuration of web2py, uWSGI, Redmine,
# Unicorn, Nginx and PostgreSQL.
# Usage : Copy the script in /home/username and run it as root, you may
# need to allow exectuion (chmod +x). Ex.:
# sudo ./setup-ubuntu-12-04-redmine-unicorn-web2py-uwsgi-nginx.sh
# File : setup-ubuntu-12-04-redmine-unicorn-web2py-uwsgi-nginx.sh
# Author : Richard V?zina
# Email : ml.richard.vezina@gmail.com
# Copyright : Richard V?zina
# Date : ven 28 d?c 2012 13:27:11 EST
# Disclaimers : This script is provided "as is", without warranty of any kind.
# Licence : CC BY-NC 2.5 CA
# ------------------------------------------------------------------------------
echo 'setup-ubuntu-12-04-redmine-unicorn-web2py-uwsgi-nginx.sh'
echo 'Requires Ubuntu = 12.04 (May works with 12.10 not tested) and installs Redmine + Unicorn + Web2py + uWSGI + Nginx + PostgreSQL'
# Check if user has root privileges
if [[ $EUID -ne 0 ]]; then
echo "You must run the script as root or using sudo"
exit 1
fi
# ------------------------------------------------------------------------------
# We concentrate here user prompts!!
# Get Redmine Postgres Database Password
echo -e "Redmine Postgres Database Password: \c "
read REDMINEPASSWORD
# Get Web2py Admin Password
echo -e "Web2py Admin Password: \c "
read PW
cd ~
openssl genrsa 1024 > self_signed.key
chmod 400 self_signed.key
openssl req -new -x509 -nodes -sha1 -days 1780 -key self_signed.key > self_signed.cert
openssl x509 -noout -fingerprint -text < self_signed.cert > self_signed.info
# ------------------------------------------------------------------------------
apt-get update
apt-get -y upgrade
apt-get autoremove
apt-get autoclean
apt-get -y install postgresql
apt-get -y install nginx-full
apt-get -y install build-essential python-dev libxml2-dev python-pip unzip
apt-get -y install ruby1.9.3 # Ref.: http://askubuntu.com/questions/137485/rails-3-not-using-rvm
apt-get -y install libpq-dev # Required for gem1.9.3 install pg Ref.: http://stackoverflow.com/questions/6040583/unable-to-install-pg-gem-on-ubuntu-cant-find-the-libpq-fe-h-header
gem1.9.3 install rails --no-rdoc --no-ri # For testing (faster) --no-rdoc --no-ri
gem1.9.3 install unicorn --no-rdoc --no-ri # For testing (faster) --no-rdoc --no-ri
gem1.9.3 install pg --no-rdoc --no-ri # For testing (faster) --no-rdoc --no-ri
cd /opt
wget http://rubyforge.org/frs/download.php/76627/redmine-2.2.0.tar.gz
wget http://rubyforge.org/frs/download.php/76628/redmine-2.2.0.tar.gz.md5
md5sum --check redmine-2.2.0.tar.gz.md5 > redmine_md5_checked_successfully
if [ -f redmine_md5_checked_successfully ]
then
tar xvfz redmine-2.2.0.tar.gz
rm redmine_md5_checked_successfully
else
echo "Redmine md5 check sum failed..."
exit 1
fi
cd redmine-2.2.0
bundle install --without development test rmagick sqlite mysql
mkdir /var/www
ln -s /opt/redmine-2.2.0/public /var/www/redmine
chown -R www-data.www-data /var/www
chown -R www-data.www-data /opt/redmine-2.2.0/public
# To avoid prompt during execution of the script use psql instead of createuser
#echo "Enter a postgres redmine user password twice:"
#createuser -P -S -D -R -l -e redmine
# createuser switch: -P --pwprompt -S --no-superuser -D --no-createdb -R --no-createrole -l --login -e --echo
sudo -u postgres psql -c "CREATE ROLE redmine LOGIN; ALTER ROLE redmine WITH ENCRYPTED PASSWORD '$REDMINEPASSWORD';"
# createdb wouldn't work without having root password
#createdb -U postgres -w -E UTF8 -O redmine -e redmine
# createdb switch: -U username --username=username -w --no-password -E Encoding -O owner --owner=owner -e --echo
sudo -u postgres psql -c "CREATE DATABASE redmine WITH ENCODING='UTF8' OWNER=redmine;"
cd /opt/redmine-2.2.0/config
# Here we change related to an issue with new rails version as far as I understand
# Ref1.: http://www.redmine.org/projects/redmine/wiki/HowTo_Install_Redmine_in_a_sub-URI # Preferred solution used
# Ref2.: http://www.redmine.org/issues/12102 # JS and CSS was not working until I add this line 'RedmineApp::Application.routes.default_scope = { :path => "/redmine", :shallow_path => "/redmine" }' before 'RedmineApp::Application.initialize!'
cp environment.rb environment.rb_original # Backup default environment.rb
sed '/RedmineApp::Application.initialize!/c \RedmineApp::Application.routes.default_scope = { :path => "/redmine", :shallow_path => "/redmine" }\nRedmineApp::Application.initialize!\nRedmine::Utils::relative_url_root = "/redmine"' environment.rb_original > environment.rb
# Now we configure Redmine database access
#nano database.yml
# paste :
echo 'production:
adapter: postgresql
database: redmine
host: localhost
username: redmine
password: "'$REDMINEPASSWORD'"
encoding: utf8' > database.yml
rake generate_secret_token
RAILS_ENV=production rake db:migrate
RAILS_ENV=production rake redmine:load_default_data
mkdir /opt/redmine-2.2.0/tmp/pids
#mkdir /opt/redmine-2.2.0/log # if not there
cd /opt/redmine-2.2.0/config
# Create Unicorn specific Redmine config in /opt/redmine-2.2.0/config/unicorn.rb
echo '#unicorn.rb Starts here
worker_processes 1
working_directory "/opt/redmine-2.2.0" # needs to be the correct directory for redmine
# This loads the application in the master process before forking
# worker processes
# Read more about it here:
# http://unicorn.bogomips.org/Unicorn/Configurator.html
preload_app true
timeout 45
# This is where we specify the socket.
# We will point the upstream Nginx module to this socket later on
listen "/tmp/unicorn_rails.socket", :backlog => 64 #directory structure needs to be created.
pid "/opt/redmine-2.2.0/tmp/pids/unicorn_rails.pid" # make sure this points to a valid directory. Make sure it is named the same as the real process name in order to allow init.d script start-stop-daemon command to kill unicorn process properly
# Set the path of the log files inside the log folder of the testapp
stderr_path "/opt/redmine-2.2.0/log/unicorn_rails.stderr.log"
stdout_path "/opt/redmine-2.2.0/log/unicorn_rails.stdout.log"
before_fork do |server, worker|
# This option works in together with preload_app true setting
# What is does is prevent the master process from holding
# the database connection
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
end
after_fork do |server, worker|
# Here we are establishing the connection after forking worker
# processes
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
# change below if your redmine instance is running differently
worker.user('\''www-data'\'', '\''www-data'\'') if Process.euid == 0
end
#unicorn.rb Ends here' > unicorn.rb
chown www-data:www-data unicorn.rb
chown -R www-data:www-data /opt/redmine-2.2.0/tmp
mkdir /etc/unicorn
# Set some config for Unicorn in /etc/unicorn/redmine
echo 'RAILS_ROOT=/opt/redmine-2
RAILS_ENV=production' > /etc/unicorn/redmine
# Create a Unicorn Redmine start script in /etc/init.d/redmine
echo '#! /bin/sh
### BEGIN INIT INFO
# Provides: redmine
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: redmine initscript
# Description: This script startup unicorn server and redmine and should
# be placed in /etc/init.d.
### END INIT INFO
# ------------------------------------------------------------------------------
# Author: Richard V?zina <ml.richard.vezina@gmail.com>
# Base on Ubuntu 12.04 : /etc/init.d/skeleton
# ven 21 d?c 2012 11:08:31 EST
# ------------------------------------------------------------------------------
# Do NOT "set -e"
# PATH should only include /usr/* if it runs after the mountnfs.sh script
APP=/opt/redmine-2.2.0/
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Unicorn and Redmine"
NAME=unicorn_rails
DAEMON=/usr/local/bin/$NAME
DAEMON_ARGS=" -E production -c $APP/config/unicorn.rb -D"
PIDFILE=/opt/redmine-2.2.0/tmp/pids/$NAME.pid
SCRIPTNAME=/etc/init.d/redmine
# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0
# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
# Define LSB log_* functions.
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
# and status_of_proc is working.
. /lib/lsb/init-functions
#
# Function that starts the daemon/service
#
do_start()
{
# Return
# 0 if daemon has been started
# 1 if daemon was already running
# 2 if daemon could not be started
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|| return 1
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
$DAEMON_ARGS \
|| return 2
# Add code here, if necessary, that waits for the process to be ready
# to handle requests from services started subsequently which depend
# on this one. As a last resort, sleep for some time.
}
#
# Function that stops the daemon/service
#
do_stop()
{
# Return
# 0 if daemon has been stopped
# 1 if daemon was already stopped
# 2 if daemon could not be stopped
# other if a failure occurred
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
RETVAL="$?"
[ "$RETVAL" = 2 ] && return 2
# Wait for children to finish too if this is a daemon that forks
# and if the daemon is only ever run from this initscript.
# If the above conditions are not satisfied then add some other code
# that waits for the process to drop all resources that could be
# needed by services started subsequently. A last resort is to
# sleep for some time.
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
[ "$?" = 2 ] && return 2
# Many daemons don'\''t delete their pidfiles when they exit.
rm -f $PIDFILE
return "$RETVAL"
}
#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
#
# If the daemon can reload its configuration without
# restarting (for example, when it is sent a SIGHUP),
# then implement that here.
#
start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
return 0
}
case "$1" in
start)
[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
do_start
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
stop)
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
do_stop
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
status)
status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
;;
#reload|force-reload)
#
# If do_reload() is not implemented then leave this commented out
# and leave '\''force-reload'\'' as an alias for '\''restart'\''.
#
#log_daemon_msg "Reloading $DESC" "$NAME"
#do_reload
#log_end_msg $?
#;;
restart|force-reload)
#
# If the "reload" option is implemented then remove the
# '\''force-reload'\'' alias
#
log_daemon_msg "Restarting $DESC" "$NAME"
do_stop
case "$?" in
0|1)
do_start
case "$?" in
0) log_end_msg 0 ;;
1) log_end_msg 1 ;; # Old process is still running
*) log_end_msg 1 ;; # Failed to start
esac
;;
*)
# Failed to stop
log_end_msg 1
;;
esac
;;
*)
#echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
exit 3
;;
esac
:' > /etc/init.d/redmine
chmod +x /etc/init.d/redmine
# Backup default Nginx site and replace it
cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default_original
rm /etc/nginx/sites-available/default
# Create configuration file /etc/nginx/sites-available/default
echo 'upstream unicorn_server {
# This is the socket we configured in unicorn.rb
server unix:/tmp/unicorn_rails.socket
fail_timeout=0;
}
server {
listen 80;
#return 301 https://192.168.1.126$request_uri; # http://$hostname$request_uri; # idem #http://wiki.nginx.org/Pitfalls#Taxing_Rewrites
charset utf-8;
server_name localhost; # $hostname;
root /var/www;
access_log /var/log/nginx/yoursite.access.log;
error_log /var/log/nginx/yoursite.error.log;
#to enable correct use of response.static_version
#location ~* /(\w+)/static(?:/_[\d]+\.[\d]+\.[\d]+)?/(.*)$ {
# alias /home/www-data/web2py/applications/$1/static/$2;
# expires max;
#}
location ~* /(\w+)/static/ {
root /home/www-data/web2py/applications/;
#remove next comment on production
#expires max;
}
location ~^\/(?!redmine(.*)) {
#uwsgi_pass 127.0.0.1:9001;
uwsgi_pass unix:///tmp/web2py.socket;
include uwsgi_params;
uwsgi_param UWSGI_SCHEME $scheme;
uwsgi_param SERVER_SOFTWARE nginx/$nginx_version;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://unicorn_server;
break;
}
}
}
server {
listen 443 default_server ssl;
charset utf-8;
server_name localhost; # $hostname;
root /var/www;
ssl_certificate /etc/nginx/ssl/self_signed.cert;
ssl_certificate_key /etc/nginx/ssl/self_signed.key;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_ciphers ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA;
ssl_protocols SSLv3 TLSv1;
keepalive_timeout 70;
location ~^\/(?!redmine(.*)) {
#uwsgi_pass 127.0.0.1:9001;
uwsgi_pass unix:///tmp/web2py.socket;
include uwsgi_params;
uwsgi_param UWSGI_SCHEME $scheme;
uwsgi_param SERVER_SOFTWARE nginx/$nginx_version;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://unicorn_server;
break;
}
}
}' >/etc/nginx/sites-available/default
#ln -s /etc/nginx/sites-available/web2py /etc/nginx/sites-enabled/web2py
#rm /etc/nginx/sites-enabled/default
# We copy ssl files we previously created
if [ -f /etc/nginx/ssl ]
then
cp ~/self_signed.* /etc/nginx/ssl/
rm ~/self_signed.*
else
mkdir /etc/nginx/ssl
cp ~/self_signed.* /etc/nginx/ssl/
rm ~/self_signed.*
fi
pip install --upgrade pip
PIPPATH=`which pip`
$PIPPATH install --upgrade uwsgi
# Prepare folders for uwsgi
sudo mkdir /etc/uwsgi
sudo mkdir /var/log/uwsgi
# Create configuration file /etc/uwsgi/web2py.xml
echo '<uwsgi>
<socket>/tmp/web2py.socket</socket>
<pythonpath>/home/www-data/web2py/</pythonpath>
<mount>/=wsgihandler:application</mount>
<master/>
<processes>4</processes>
<harakiri>60</harakiri>
<reload-mercy>8</reload-mercy>
<cpu-affinity>1</cpu-affinity>
<stats>/tmp/stats.socket</stats>
<max-requests>2000</max-requests>
<limit-as>512</limit-as>
<reload-on-as>256</reload-on-as>
<reload-on-rss>192</reload-on-rss>
<uid>www-data</uid>
<gid>www-data</gid>
<cron>0 0 -1 -1 -1 python /home/www-data/web2py/web2py.py -Q -S welcome -M -R scripts/sessions2trash.py -A -o</cron>
<no-orphans/>
</uwsgi>' > /etc/uwsgi/web2py.xml
#Create a configuration file for uwsgi in emperor-mode
#for Upstart in /etc/init/uwsgi-emperor.conf
echo '# Emperor uWSGI script
description "uWSGI Emperor"
start on runlevel [2345]
stop on runlevel [06]
##
#remove the comments in the next section to enable static file compression for the welcome app
#in that case, turn on gzip_static on; on /etc/nginx/nginx.conf
##
#pre-start script
# python /home/www-data/web2py/web2py.py -S welcome -R scripts/zip_static_files.py
# chown -R www-data:www-data /home/www-data/web2py/*
#end script
respawn
exec uwsgi --master --die-on-term --emperor /etc/uwsgi --logto /var/log/uwsgi/uwsgi.log
' > /etc/init/uwsgi-emperor.conf
# Install Web2py
mkdir /home/www-data
cd /home/www-data
wget http://web2py.com/examples/static/web2py_src.zip
unzip web2py_src.zip
rm web2py_src.zip
# Download latest version of sessions2trash.py
wget http://web2py.googlecode.com/hg/scripts/sessions2trash.py -O /home/www-data/web2py/scripts/sessions2trash.py
chown -R www-data:www-data web2py
cd /home/www-data/web2py
sudo -u www-data python -c "from gluon.main import save_password; save_password('$PW',443)"
/etc/init.d/redmine start
start uwsgi-emperor
/etc/init.d/nginx restart
ufw allow 80 # Or check your firewall configuration