r4521 - trunk/wooly/python/wooly
by eallen@fedoraproject.org
Author: eallen
Date: 2011-02-11 20:25:48 +0000 (Fri, 11 Feb 2011)
New Revision: 4521
Modified:
trunk/wooly/python/wooly/forms.py
trunk/wooly/python/wooly/forms.strings
Log:
Added ability to put a title attribute on input text fields
Modified: trunk/wooly/python/wooly/forms.py
===================================================================
--- trunk/wooly/python/wooly/forms.py 2011-02-11 20:25:05 UTC (rev 4520)
+++ trunk/wooly/python/wooly/forms.py 2011-02-11 20:25:48 UTC (rev 4521)
@@ -144,10 +144,15 @@
super(ScalarInput, self).__init__(app, name, param)
self.size = 32
+ self.title = None
def render_size(self, session, *args):
return self.size
+ def render_title(self, session, *args):
+ if self.title:
+ return "title=\"%s\"" % self.title
+
class StringInput(ScalarInput):
def __init__(self, app, name):
super(StringInput, self).__init__(app, name, None)
Modified: trunk/wooly/python/wooly/forms.strings
===================================================================
--- trunk/wooly/python/wooly/forms.strings 2011-02-11 20:25:05 UTC (rev 4520)
+++ trunk/wooly/python/wooly/forms.strings 2011-02-11 20:25:48 UTC (rev 4521)
@@ -70,7 +70,7 @@
<button class="{class}" type="{type}" tabindex="{tab_index}" {disabled_attr} name="{name}" value="{value}" onclick="return {onclick}('{name}', '{value}')">{content}</button>
[ScalarInput.html]
-<input type="text" name="{name}" value="{value}" tabindex="{tab_index}" {disabled_attr} size="{size}"/>
+<input type="text" name="{name}" value="{value}" tabindex="{tab_index}" {disabled_attr} size="{size}" {title}/>
[MultilineStringInput.html]
<textarea name="{name}" tabindex="{tab_index}" {disabled_attr} rows="{rows}" cols="{columns}">{value}</textarea>
13 years, 2 months
r4520 - trunk/wooly/resources
by eallen@fedoraproject.org
Author: eallen
Date: 2011-02-11 20:25:05 +0000 (Fri, 11 Feb 2011)
New Revision: 4520
Modified:
trunk/wooly/resources/mootools.js
Log:
Added OverText class
Modified: trunk/wooly/resources/mootools.js
===================================================================
--- trunk/wooly/resources/mootools.js 2011-02-11 18:28:47 UTC (rev 4519)
+++ trunk/wooly/resources/mootools.js 2011-02-11 20:25:05 UTC (rev 4520)
@@ -3744,3 +3744,254 @@
});
+/*
+---
+
+script: OverText.js
+
+name: OverText
+
+description: Shows text over an input that disappears when the user clicks into it. The text remains hidden if the user adds a value.
+
+license: MIT-style license
+
+authors:
+ - Aaron Newton
+
+requires:
+ - Core/Options
+ - Core/Events
+ - Core/Element.Event
+ - /Class.Binds
+ - /Class.Occlude
+ - /Element.Position
+ - /Element.Shortcuts
+
+provides: [OverText]
+
+...
+*/
+
+var OverText = new Class({
+
+ Implements: [Options, Events, Class.Occlude],
+
+ Binds: ['reposition', 'assert', 'focus', 'hide'],
+
+ options: {/*
+ textOverride: null,
+ onFocus: $empty()
+ onTextHide: $empty(textEl, inputEl),
+ onTextShow: $empty(textEl, inputEl), */
+ element: 'label',
+ positionOptions: {
+ position: 'upperLeft',
+ edge: 'upperLeft',
+ offset: {
+ x: 4,
+ y: 2
+ }
+ },
+ poll: false,
+ pollInterval: 250,
+ wrap: false
+ },
+
+ property: 'OverText',
+
+ initialize: function(element, options){
+ this.element = document.id(element);
+ if (this.occlude()) return this.occluded;
+ this.setOptions(options);
+ this.attach(this.element);
+ OverText.instances.push(this);
+ if (this.options.poll) this.poll();
+ return this;
+ },
+
+ toElement: function(){
+ return this.element;
+ },
+
+ attach: function(){
+ var val = this.options.textOverride || this.element.get('alt') || this.element.get('title');
+ if (!val) return;
+ this.text = new Element(this.options.element, {
+ 'class': 'overTxtLabel',
+ styles: {
+ lineHeight: 'normal',
+ position: 'absolute',
+ cursor: 'text'
+ },
+ html: val,
+ events: {
+ click: this.hide.pass(this.options.element == 'label', this)
+ }
+ }).inject(this.element, 'after');
+ if (this.options.element == 'label') {
+ if (!this.element.get('id')) this.element.set('id', 'input_' + new Date().getTime());
+ this.text.set('for', this.element.get('id'));
+ }
+
+ if (this.options.wrap) {
+ this.textHolder = new Element('div', {
+ styles: {
+ lineHeight: 'normal',
+ position: 'relative'
+ },
+ 'class':'overTxtWrapper'
+ }).adopt(this.text).inject(this.element, 'before');
+ }
+
+ return this.enable();
+ },
+
+ destroy: function(){
+ this.element.eliminate('OverTextDiv').eliminate('OverText');
+ this.disable();
+ if (this.text) this.text.destroy();
+ if (this.textHolder) this.textHolder.destroy();
+ return this;
+ },
+
+ disable: function(){
+ this.element.removeEvents({
+ focus: this.focus,
+ blur: this.assert,
+ change: this.assert
+ });
+ window.removeEvent('resize', this.reposition);
+ this.hide(true, true);
+ return this;
+ },
+
+ enable: function(){
+ this.element.addEvents({
+ focus: this.focus,
+ blur: this.assert,
+ change: this.assert
+ });
+ window.addEvent('resize', this.reposition);
+ this.assert(true);
+ this.reposition();
+ return this;
+ },
+
+ wrap: function(){
+ if (this.options.element == 'label') {
+ if (!this.element.get('id')) this.element.set('id', 'input_' + new Date().getTime());
+ this.text.set('for', this.element.get('id'));
+ }
+ },
+
+ startPolling: function(){
+ this.pollingPaused = false;
+ return this.poll();
+ },
+
+ poll: function(stop){
+ //start immediately
+ //pause on focus
+ //resumeon blur
+ if (this.poller && !stop) return this;
+ var test = function(){
+ if (!this.pollingPaused) this.assert(true);
+ }.bind(this);
+ if (stop) $clear(this.poller);
+ else this.poller = test.periodical(this.options.pollInterval, this);
+ return this;
+ },
+
+ stopPolling: function(){
+ this.pollingPaused = true;
+ return this.poll(true);
+ },
+
+ focus: function(){
+ if (this.text && (!this.text.isDisplayed() || this.element.get('disabled'))) return;
+ this.hide();
+ },
+
+ hide: function(suppressFocus, force){
+ if (this.text && (this.text.isDisplayed() && (!this.element.get('disabled') || force))){
+ this.text.hide();
+ this.fireEvent('textHide', [this.text, this.element]);
+ this.pollingPaused = true;
+ if (!suppressFocus){
+ try {
+ this.element.fireEvent('focus');
+ this.element.focus();
+ } catch(e){} //IE barfs if you call focus on hidden elements
+ }
+ }
+ return this;
+ },
+
+ show: function(){
+ if (this.text && !this.text.isDisplayed()){
+ this.text.show();
+ this.reposition();
+ this.fireEvent('textShow', [this.text, this.element]);
+ this.pollingPaused = false;
+ }
+ return this;
+ },
+
+ assert: function(suppressFocus){
+ this[this.test() ? 'show' : 'hide'](suppressFocus);
+ },
+
+ test: function(){
+ var v = this.element.get('value');
+ return !v;
+ },
+
+ reposition: function(){
+ this.assert(true);
+ if (!this.element.isVisible()) return this.stopPolling().hide();
+ if (this.text && this.test()) this.text.position($merge(this.options.positionOptions, {relativeTo: this.element}));
+ return this;
+ }
+
+});
+
+OverText.instances = [];
+
+$extend(OverText, {
+
+ each: function(fn) {
+ return OverText.instances.map(function(ot, i){
+ if (ot.element && ot.text) return fn.apply(OverText, [ot, i]);
+ return null; //the input or the text was destroyed
+ });
+ },
+
+ update: function(){
+
+ return OverText.each(function(ot){
+ return ot.reposition();
+ });
+
+ },
+
+ hideAll: function(){
+
+ return OverText.each(function(ot){
+ return ot.hide(true, true);
+ });
+
+ },
+
+ showAll: function(){
+ return OverText.each(function(ot) {
+ return ot.show();
+ });
+ }
+
+});
+
+if (window.Fx && Fx.Reveal) {
+ Fx.Reveal.implement({
+ hideInputs: Browser.Engine.trident ? 'select, input, textarea, object, embed, .overTxtLabel' : false
+ });
+}
\ No newline at end of file
13 years, 2 months
r4519 - trunk/wooly/python/wooly
by tmckay@fedoraproject.org
Author: tmckay
Date: 2011-02-11 18:28:47 +0000 (Fri, 11 Feb 2011)
New Revision: 4519
Modified:
trunk/wooly/python/wooly/pages.py
Log:
Improve cumin web server performance by adding Cache-Control max-age=N headers for static pages.
BZ676902
Modified: trunk/wooly/python/wooly/pages.py
===================================================================
--- trunk/wooly/python/wooly/pages.py 2011-02-11 17:26:52 UTC (rev 4518)
+++ trunk/wooly/python/wooly/pages.py 2011-02-11 18:28:47 UTC (rev 4519)
@@ -258,6 +258,9 @@
def get_content_type(self, session):
return "text/css"
+ def get_cache_control(self, session):
+ return "max-age=86400"
+
def get_css(self):
if not self.__css:
writer = Writer()
@@ -297,6 +300,9 @@
def get_content_type(self, session):
return "text/javascript"
+ def get_cache_control(self, session):
+ return "max-age=86400"
+
def get_javascript(self):
if not self.__javascript:
writer = Writer()
@@ -344,6 +350,9 @@
def get_last_modified(self, session):
return self.then
+ def get_cache_control(self, session):
+ return "max-age=86400"
+
def get_content_type(self, session):
name = self.rname.get(session)
base, ext = os.path.splitext(name)
13 years, 2 months
r4518 - in trunk/cumin: bin etc python/cumin
by tmckay@fedoraproject.org
Author: tmckay
Date: 2011-02-11 17:26:52 +0000 (Fri, 11 Feb 2011)
New Revision: 4518
Modified:
trunk/cumin/bin/cumin
trunk/cumin/bin/cumin-admin
trunk/cumin/bin/cumin-bench
trunk/cumin/bin/cumin-data
trunk/cumin/bin/cumin-smoke-test
trunk/cumin/bin/cumin-web
trunk/cumin/etc/sysvinit-cumin
trunk/cumin/python/cumin/config.py
Log:
Incorporate configuration changes from 1.3.2 branch into trunk that allow multiple instances (this is BZ670651)
BZ644302, cumin may be started more than once, also included.
Modified: trunk/cumin/bin/cumin
===================================================================
--- trunk/cumin/bin/cumin 2011-02-10 19:40:58 UTC (rev 4517)
+++ trunk/cumin/bin/cumin 2011-02-11 17:26:52 UTC (rev 4518)
@@ -1,32 +1,74 @@
-#!/bin/bash
+#!/usr/bin/python
-function die {
- kill "$data" "$web"
-}
+from optparse import OptionParser
+from time import sleep
+import subprocess
+import os
+import sys
+import signal
-trap die EXIT
+def call_sys_exit(sig,frame):
+ sys.exit()
-function start_data {
- cumin-data &
- data="$!"
-}
+def set_exit_handler(func):
+ if os.name == "nt":
+ try:
+ import win32api
+ win32api.SetConsoleCtrlHandler(func, True)
+ except ImportError:
+ version = ".".join(map(str, sys.version_info[:2]))
+ raise Exception("pywin32 not installed for Python " + version)
+ else:
+ signal.signal(signal.SIGTERM, func)
-function start_web {
- cumin-web &
- web="$!"
-}
+def main():
-start_data
-start_web
+ # tuple indices, for clarity
+ PROCESS = 0
+ SECTION = 1
+ PROGRAM = 2
-while [ 0 ]; do
- sleep 30
+ parser = OptionParser()
- if [ ! -d "/proc/$data" ]; then
- start_data
- fi
+ parser.add_option("--webs", dest="webs", default="web",
+ help="Configuration section names for cumin-web instances."\
+ "\nEach value implies a separate cumin-web instance.")
- if [ ! -d "/proc/$web" ]; then
- start_web
- fi
-done
+ parser.add_option("--datas", dest="datas", default="data",
+ help="Configuration section names for cumin-data instances."\
+ "\nEach value implies a separate cumin-data instance.")
+
+ (options, args) = parser.parse_args()
+
+ # Get our list cumin-web and data instances
+ # create list elements to hold the process object, section arg, and app
+ apps = []
+ for app in options.webs.split(','):
+ apps.append([None, app, "cumin-web"])
+
+ for app in options.datas.split(','):
+ apps.append([None, app, "cumin-data"])
+
+ # Launch and babysit
+ try:
+ while True:
+ for app in apps:
+ if not app[PROCESS] or app[PROCESS].poll():
+ arg = "--section=" + app[SECTION]
+ app[PROCESS] = subprocess.Popen([app[PROGRAM], arg])
+ sleep(30)
+
+ finally:
+ for app in apps:
+ if app[PROCESS]:
+ os.kill(app[PROCESS].pid, signal.SIGTERM)
+
+if __name__ == "__main__":
+ # This is necessary so that on a SIGTERM we can call sys.exit()
+ # and cause the finally block to be executed. Ctrl-C will
+ # run the finally block anyway.
+ set_exit_handler(call_sys_exit)
+ try:
+ main()
+ except KeyboardInterrupt:
+ pass
Modified: trunk/cumin/bin/cumin-admin
===================================================================
--- trunk/cumin/bin/cumin-admin 2011-02-10 19:40:58 UTC (rev 4517)
+++ trunk/cumin/bin/cumin-admin 2011-02-11 17:26:52 UTC (rev 4518)
@@ -26,14 +26,11 @@
setup_initial_logging()
- config = CuminConfig()
- values = config.parse()
+ parser = CuminDataOptionParser()
- parser = CuminOptionParser(values.data)
parser.set_usage(generate_usage())
+ config, values, opts, args = get_configuration(parser, CuminDataConfig)
- opts, args = parser.parse_args()
-
setup_operational_logging(opts)
try:
@@ -54,7 +51,7 @@
broker_uris = [x.strip() for x in opts.brokers.split(",")]
- app = Cumin(config.home, broker_uris, opts.database)
+ app = Cumin(config.get_home(), broker_uris, opts.database)
app.check()
app.init()
Modified: trunk/cumin/bin/cumin-bench
===================================================================
--- trunk/cumin/bin/cumin-bench 2011-02-10 19:40:58 UTC (rev 4517)
+++ trunk/cumin/bin/cumin-bench 2011-02-11 17:26:52 UTC (rev 4518)
@@ -10,22 +10,17 @@
def do_main():
setup_initial_logging()
- config = CuminConfig()
- values = config.parse()
+ config, values, opts, args = get_configuration(CuminWebOptionParser(), CuminWebConfig)
- parser = CuminOptionParser(values.web)
-
- opts, args = parser.parse_args()
-
setup_operational_logging(opts)
broker_uris = [x.strip() for x in opts.brokers.split(",")]
- cumin = Cumin(config.home, broker_uris, opts.database)
+ cumin = Cumin(config.get_home(), broker_uris, opts.database)
cumin.debug = True
- cumin.user = values.web.user
- cumin.update_interval = values.web.update_interval
+ cumin.user = values.user
+ cumin.update_interval = values.update_interval
cumin.check()
cumin.init()
Modified: trunk/cumin/bin/cumin-data
===================================================================
--- trunk/cumin/bin/cumin-data 2011-02-10 19:40:58 UTC (rev 4517)
+++ trunk/cumin/bin/cumin-data 2011-02-11 17:26:52 UTC (rev 4518)
@@ -13,15 +13,8 @@
def main():
setup_initial_logging()
- config = CuminConfig()
- values = config.parse().data
+ config, values, opts, args = get_configuration(CuminDataOptionParser(), CuminDataConfig)
- parser = CuminOptionParser(values)
- parser.add_option("--print-stats", action="store_true")
- parser.add_option("--print-events", type="int", default=0, metavar="LEVEL")
-
- opts, args = parser.parse_args()
-
setup_operational_logging(opts)
model_dir = os.path.join(config.home, "model")
Modified: trunk/cumin/bin/cumin-smoke-test
===================================================================
--- trunk/cumin/bin/cumin-smoke-test 2011-02-10 19:40:58 UTC (rev 4517)
+++ trunk/cumin/bin/cumin-smoke-test 2011-02-11 17:26:52 UTC (rev 4518)
@@ -15,22 +15,16 @@
def main():
setup_initial_logging()
- config = CuminConfig()
- values = config.parse()
+ config, values, opts, args = get_configuration(CuminWebOptionParser(), CuminWebConfig)
- parser = CuminOptionParser(values.web)
-
- parser.add_option("--host", default=values.web.host)
- parser.add_option("--port", default=values.web.port)
-
- opts, args = parser.parse_args()
-
setup_operational_logging(opts)
- cumin = Cumin(config.home, opts.brokers, opts.database,
+ broker_uris = [x.strip() for x in opts.brokers.split(",")]
+
+ cumin = Cumin(config.get_home(), broker_uris, opts.database,
opts.host, opts.port)
- cumin.user = values.web.user
+ cumin.user = values.user
cumin.check()
cumin.init()
Modified: trunk/cumin/bin/cumin-web
===================================================================
--- trunk/cumin/bin/cumin-web 2011-02-10 19:40:58 UTC (rev 4517)
+++ trunk/cumin/bin/cumin-web 2011-02-11 17:26:52 UTC (rev 4518)
@@ -13,32 +13,24 @@
def main():
setup_initial_logging()
- config = CuminConfig()
- values = config.parse()
+ config, values, opts, args = get_configuration(CuminWebOptionParser(), CuminWebConfig)
- parser = CuminOptionParser(values.web)
-
- parser.add_option("--host", default=values.web.host)
- parser.add_option("--port", default=values.web.port, type=int)
-
- opts, args = parser.parse_args()
-
setup_operational_logging(opts)
broker_uris = [x.strip() for x in opts.brokers.split(",")]
- cumin = Cumin(config.home, broker_uris, opts.database,
+ cumin = Cumin(config.get_home(), broker_uris, opts.database,
opts.host, opts.port)
cumin.debug = opts.debug
- cumin.user = values.web.user
- cumin.update_interval = values.web.update_interval
+ cumin.user = values.user
+ cumin.update_interval = values.update_interval
# set default values for form inputs
- cumin.set_form_defaults(values.web.request_memory,
- values.web.request_memory_vm,
- values.web.request_disk,
- values.web.request_disk_vm)
+ cumin.set_form_defaults(values.request_memory,
+ values.request_memory_vm,
+ values.request_disk,
+ values.request_disk_vm)
cumin.check()
cumin.init()
Modified: trunk/cumin/etc/sysvinit-cumin
===================================================================
--- trunk/cumin/etc/sysvinit-cumin 2011-02-10 19:40:58 UTC (rev 4517)
+++ trunk/cumin/etc/sysvinit-cumin 2011-02-11 17:26:52 UTC (rev 4518)
@@ -11,6 +11,9 @@
test -x /usr/bin/cumin || exit 1
test -x /usr/bin/cumin-database || exit 1
+SYSCONFIG="/etc/sysconfig/cumin"
+[ -r "$SYSCONFIG" ] && source "$SYSCONFIG"
+
cumin-database check-started &> /dev/null || {
echo "Cumin's database server is not running"
echo "Run 'cumin-database start' as root"
@@ -34,10 +37,16 @@
start() {
echo -n $"Starting cumin: "
- daemon --user cumin --check $servicename $processname \&
+ status $processname &> /dev/null
RETVAL=$?
+ if [ $RETVAL -eq 0 ] ; then
+ success
+ else
+ daemon --user cumin --check $servicename $processname $CUMIN_OPTIONS \&
+ RETVAL=$?
+ [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$servicename
+ fi
echo
- [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$servicename
}
stop() {
Modified: trunk/cumin/python/cumin/config.py
===================================================================
--- trunk/cumin/python/cumin/config.py 2011-02-10 19:40:58 UTC (rev 4517)
+++ trunk/cumin/python/cumin/config.py 2011-02-11 17:26:52 UTC (rev 4518)
@@ -17,9 +17,31 @@
if not os.path.isdir(self.home):
raise Exception("Home path '%s' is not a directory")
- web = CuminConfigSection(self, "web")
- web.log_file.default = os.path.join(self.home, "log", "web.log")
+ test = CuminConfigSection(self, "test")
+ test.log_file.default = os.path.join(self.home, "log", "test.log")
+ def get_home(self):
+ return self.home
+
+ def parse(self):
+ paths = list()
+
+ paths.append(os.path.join(os.sep, "etc", "cumin", "cumin.conf"))
+ paths.append(os.path.join(self.home, "etc", "cumin.conf"))
+ paths.append(os.path.join(os.path.expanduser("~"), ".cumin.conf"))
+
+ return self.parse_files(paths)
+
+class CuminWebConfig(CuminConfig):
+ def __init__(self, section_name):
+ super(CuminWebConfig, self).__init__()
+
+ self.create_section(section_name)
+
+ def create_section(self, name):
+ web = CuminConfigSection(self, name)
+ web.log_file.default = os.path.join(self.home, "log", name + ".log")
+
param = ConfigParameter(web, "update-interval", int)
param.default = 10
@@ -42,8 +64,15 @@
param = ConfigParameter(web, "request-disk-vm", int)
param.default = 5 * 1024 #MB
- data = CuminConfigSection(self, "data")
- data.log_file.default = os.path.join(self.home, "log", "data.log")
+class CuminDataConfig(CuminConfig):
+ def __init__(self, section_name):
+ super(CuminDataConfig, self).__init__()
+
+ self.create_section(section_name)
+
+ def create_section(self, name):
+ data = CuminConfigSection(self, name)
+ data.log_file.default = os.path.join(self.home, "log", name + ".log")
param = ConfigParameter(data, "packages", str)
@@ -56,18 +85,6 @@
param = ConfigParameter(data, "vacuum-interval", int)
param.default = 60 * 60 # 1 hour
- test = CuminConfigSection(self, "test")
- test.log_file.default = os.path.join(self.home, "log", "test.log")
-
- def parse(self):
- paths = list()
-
- paths.append(os.path.join(os.sep, "etc", "cumin", "cumin.conf"))
- paths.append(os.path.join(self.home, "etc", "cumin.conf"))
- paths.append(os.path.join(os.path.expanduser("~"), ".cumin.conf"))
-
- return self.parse_files(paths)
-
class CuminConfigSection(ConfigSection):
def __init__(self, config, name):
super(CuminConfigSection, self).__init__(config, name)
@@ -86,17 +103,77 @@
param = ConfigParameter(self, "debug", bool)
param.default = False
-class CuminOptionParser(OptionParser):
- def __init__(self, section):
+class CuminMultiOptionParser(OptionParser,object):
+ def __init__(self, default_section):
OptionParser.__init__(self)
- self.add_option("--database", default=section.database)
- self.add_option("--brokers", default=section.brokers)
- self.add_option("--log-file", default=section.log_file)
- self.add_option("--log-level", default=section.log_level)
- self.add_option("--debug", action="store_true", default=section.debug)
+ self.add_option("--section", default=default_section)
+
+ # We don't know defaults yet...
+ self.add_option("--database")
+ self.add_option("--brokers")
+ self.add_option("--log-file")
+ self.add_option("--log-level")
+ self.add_option("--debug", action="store_true")
self.add_option("--init-only", action="store_true")
+ def _apply_default(self, section, name):
+ try:
+ val = getattr(section, name)
+ self.set_default(name, val)
+ except:
+ pass
+
+ def set_defaults_from_section(self, section):
+ # It is possible that a section does not contain an updated
+ # default value for a particular option. Skip exception.
+ self._apply_default(section, "database")
+ self._apply_default(section, "brokers")
+ self._apply_default(section, "log_file")
+ self._apply_default(section, "log_level")
+ self._apply_default(section, "debug")
+
+class CuminWebOptionParser(CuminMultiOptionParser):
+ def __init__(self, default_section="web"):
+ super(CuminWebOptionParser, self).__init__(default_section)
+
+ # Add additional parameters for web
+ self.add_option("--host")
+ self.add_option("--port", type=int)
+
+ def set_defaults_from_section(self, section):
+ super(CuminWebOptionParser, self).set_defaults_from_section(section)
+
+ # For web, these options also have defaults in config files
+ self._apply_default(section, "host")
+ self._apply_default(section, "port")
+
+class CuminDataOptionParser(CuminMultiOptionParser):
+ def __init__(self, default_section="data"):
+ super(CuminDataOptionParser, self).__init__(default_section)
+
+ # Add additional parameters for data
+ self.add_option("--print-stats", action="store_true")
+ self.add_option("--print-events", type="int", default=0, metavar="LEVEL")
+
+def get_configuration(parser, ConfigType):
+ # First parse should return a "section" value
+ opts, args = parser.parse_args()
+ if hasattr(opts, "section"):
+ # Create config object with the given section and parse config files
+ config = ConfigType(opts.section)
+ values = config.parse()
+ if hasattr(values, opts.section):
+ # Reset defaults and parse again
+ values = getattr(values, opts.section)
+ parser.set_defaults_from_section(values)
+ opts, args = parser.parse_args()
+ else:
+ raise Exception("Section ["+opts.section+"] not present in configuration files.")
+ else:
+ raise Exception("No '--section' value available as option or default")
+ return config, values, opts, args
+
# These logging functions address a logging bootstrap problem. Before
# configuration is read, we can't configure logging. Nonetheless,
# we'd like to log errors or warnings encountered before that point.
13 years, 2 months
r4517 - in trunk/cumin/python/cumin: . grid
by eallen@fedoraproject.org
Author: eallen
Date: 2011-02-10 19:40:58 +0000 (Thu, 10 Feb 2011)
New Revision: 4517
Modified:
trunk/cumin/python/cumin/grid/job.py
trunk/cumin/python/cumin/grid/limit.py
trunk/cumin/python/cumin/grid/negotiator.py
trunk/cumin/python/cumin/grid/negotiator.strings
trunk/cumin/python/cumin/grid/quota.py
trunk/cumin/python/cumin/model.py
trunk/cumin/python/cumin/objectframe.py
trunk/cumin/python/cumin/objectframe.strings
trunk/cumin/python/cumin/objectselector.strings
trunk/cumin/python/cumin/objecttask.py
trunk/cumin/python/cumin/task.py
Log:
Provide instant feedback for qmf forms when submitted.
Remove sleep when retrieving qmf data and allow page to show loading immediately.
Modified: trunk/cumin/python/cumin/grid/job.py
===================================================================
--- trunk/cumin/python/cumin/grid/job.py 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/grid/job.py 2011-02-10 19:40:58 UTC (rev 4517)
@@ -558,6 +558,7 @@
self.add_child(self.edit_button)
self.defer_enabled = True
+ self.update_enabled = True
def render_edit_button(self, session):
_, error = self.do_get_items(session)
@@ -862,14 +863,8 @@
def do_invoke(self, invoc, scheduler, job_id, reason):
assert self.method
- action = QmfCall(self.app)
- results = action.execute(scheduler, self.method, job_id, reason)
+ self.qmf_call(invoc, scheduler, self.method, job_id, reason)
- if results.error:
- raise results.error
- invoc.status_code = results.status
- invoc.end()
-
class JobActionForm(ObjectFrameTaskForm):
def __init__(self, app, name, task, verb):
super(JobActionForm, self).__init__(app, name, task)
Modified: trunk/cumin/python/cumin/grid/limit.py
===================================================================
--- trunk/cumin/python/cumin/grid/limit.py 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/grid/limit.py 2011-02-10 19:40:58 UTC (rev 4517)
@@ -4,7 +4,8 @@
from cumin.model import QmfCall
from cumin.objectselector import ObjectTableColumn, ObjectQmfSelector,\
ObjectQmfTable
-from cumin.objectframe import ObjectFrame, ObjectFrameTask, ObjectFrameTaskForm
+from cumin.objectframe import ObjectFrame, ObjectFrameTask,\
+ ObjectFrameTaskFeedbackForm
from cumin.qmfadapter import ObjectQmfAdapter
from cumin.widgets import CuminView
@@ -58,8 +59,6 @@
if limits is None or len(limits) == 0:
return {}
- #limits = {'a':{'CURRENT': 11, 'MAX': 22},
- # 'b':{'CURRENT': 3, 'MAX': 44}}
return limits
@@ -165,7 +164,7 @@
invoc.end()
-class NegotiatorLimitForm(ObjectFrameTaskForm):
+class NegotiatorLimitForm(ObjectFrameTaskFeedbackForm):
def __init__(self, app, name, task):
super(NegotiatorLimitForm, self).__init__(app, name, task)
@@ -187,6 +186,9 @@
self.task.invoke(session, negotiator, limit_name, limit_max)
self.task.exit_with_redirect(session)
+ def render_form_class(self, session):
+ return " ".join((super(NegotiatorLimitForm, self).render_form_class(session), "mform"))
+
class LimitName(StringField):
def __init__(self, app, name):
super(NegotiatorLimitForm.LimitName, self).__init__(app, name)
Modified: trunk/cumin/python/cumin/grid/negotiator.py
===================================================================
--- trunk/cumin/python/cumin/grid/negotiator.py 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/grid/negotiator.py 2011-02-10 19:40:58 UTC (rev 4517)
@@ -1,6 +1,7 @@
from wooly.util import StringCatalog, Writer
import logging
-from cumin.objectframe import ObjectFrame, ObjectFrameTaskForm, ObjectFrameTask
+from cumin.objectframe import ObjectFrame, ObjectFrameTask,\
+ ObjectFrameTaskFeedbackForm
from cumin.objectselector import ObjectSelector, ObjectLinkColumn,\
MonitorSelfStatColumn
from wooly import Widget, Attribute, Parameter
@@ -137,9 +138,9 @@
info = self.get_config_info(session)
siblings = list()
- (ng, s, nn) = rpartition(node, ".")
+ (ng, _, _) = rpartition(node, ".")
for group in info:
- (g, s, n) = rpartition(group, ".")
+ (g, _, _) = rpartition(group, ".")
if g == ng:
siblings.append(group)
@@ -150,7 +151,6 @@
return list(info)
def split_group_names(self, group_string):
- groups = []
g_string = group_string.replace(", ", ",")
if not "," in g_string:
return g_string.split()
@@ -207,7 +207,7 @@
val = min(1.0, val)
return val
-class GroupForm(ObjectFrameTaskForm):
+class GroupForm(ObjectFrameTaskFeedbackForm):
def __init__(self, app, name, task):
super(GroupForm, self).__init__(app, name, task)
@@ -219,19 +219,17 @@
self.add_child(self.group_helper)
group_name = Parameter(app, "gn")
- self.add_parameter(group_name)
-
self.group_names = ListParameter(app, "group_names", group_name)
self.add_parameter(self.group_names)
original_value = Parameter(app, "ov")
- self.add_parameter(original_value)
-
self.original_values = ListParameter(app, "original_values", original_value)
self.add_parameter(self.original_values)
- #self.defer_enabled = True
+ self.buttons = list()
+ self.defer_enabled = True
+
def render_group_name(self, session, group):
return group
@@ -241,9 +239,6 @@
def render_original_value_path(self, session, group):
return self.original_values.path
- def render_form_class(self, session):
- return ""
-
class EditDynamicQuotaForm(GroupForm):
def __init__(self, app, name, task):
super(EditDynamicQuotaForm, self).__init__(app, name, task)
Modified: trunk/cumin/python/cumin/grid/negotiator.strings
===================================================================
--- trunk/cumin/python/cumin/grid/negotiator.strings 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/grid/negotiator.strings 2011-02-10 19:40:58 UTC (rev 4517)
@@ -30,7 +30,7 @@
elem.focus();
cumin.init_sliders("{chart_id}");
- var myFormValidator = new FormValidator($('{id}'), {
+ var myFormValidator = new Form.Validator($('{id}'), {
onElementPass: function (el) {
var val = el.value.toFloat();
var sliderDiv = el.getParent("tr").getElement(".slider");
Modified: trunk/cumin/python/cumin/grid/quota.py
===================================================================
--- trunk/cumin/python/cumin/grid/quota.py 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/grid/quota.py 2011-02-10 19:40:58 UTC (rev 4517)
@@ -47,7 +47,7 @@
return fmt_link(href, content, "", "", self.fmt_hover(data[0]))
def fmt_hover(self, group):
- return "Edit the %s" % self.title
+ return "Edit the %s" % self.field.name
class QuotaAdapter(ObjectQmfAdapter):
def get_data(self, values, options):
Modified: trunk/cumin/python/cumin/model.py
===================================================================
--- trunk/cumin/python/cumin/model.py 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/model.py 2011-02-10 19:40:58 UTC (rev 4517)
@@ -89,14 +89,16 @@
store.start_updates()
self.group_names_by_negotiator[negotiator._qmf_agent_id] = store
- #sleep(1)
return store
finally:
self.lock.release()
def get_negotiator_config_values(self, negotiator, needed_groups, config):
- assert negotiator
+ if not negotiator:
+ store = NegotiatorGroupConfigValuesStore(self, None)
+ store.exception = Exception("Missing Negotiator")
+ return store
self.lock.acquire()
@@ -109,28 +111,25 @@
added += store.add_group_config(group, config)
if added > 0:
store.update_new(None)
- #sleep(1)
except KeyError:
store = NegotiatorGroupConfigValuesStore(self, negotiator, needed_groups, config)
store.start_updates()
self.group_config_values_by_negotiator[negotiator._qmf_agent_id] = store
- #sleep(1)
-
return store
finally:
self.lock.release()
def update_negotiator_config_value(self, negotiator):
- assert negotiator
+ if not negotiator:
+ return
self.lock.acquire()
try:
store = self.group_config_values_by_negotiator[negotiator._qmf_agent_id]
store.update(None)
- sleep(1)
finally:
self.lock.release()
@@ -157,7 +156,10 @@
self.lock.release()
def get_submission_job_summaries(self, submission):
- assert submission
+ if not submission:
+ store = SubmissionJobSummaryStore(self, None)
+ store.exception = Exception("Missing Submission")
+ return store
self.lock.acquire()
@@ -171,12 +173,6 @@
self.job_summaries_by_submission[submission._id] = store
- for i in range(5):
- if store.data or store.status:
- break
-
- sleep(1)
-
return store
finally:
self.lock.release()
@@ -1089,6 +1085,8 @@
self.status = status
try:
self.data = data["Limits"]
+ #self.data = {'a':{'CURRENT': 11, 'MAX': 22},
+ # 'b':{'CURRENT': 3, 'MAX': 44}}
except KeyError:
pass
@@ -1173,6 +1171,12 @@
for config in self.configs:
action = FetchRawConfigSet(self.model.app)
raw_configs = action.execute(self.negotiator, self.configs[config], config+"_")
+ #for group in raw_configs:
+ # qmfc = raw_configs[group]
+ # qmfc.data = {'Value': 0.1}
+ # qmfc.error = None
+ # qmfc.got_data = True
+ # qmfc.status = "OK"
self.data[config] = raw_configs
def update_new(self, cursor):
Modified: trunk/cumin/python/cumin/objectframe.py
===================================================================
--- trunk/cumin/python/cumin/objectframe.py 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/objectframe.py 2011-02-10 19:40:58 UTC (rev 4517)
@@ -410,3 +410,20 @@
obj = self.object.get(session)
return obj.get_title()
+
+class ObjectFrameTaskFeedbackForm(ObjectFrameTaskForm):
+ # substitute our submit button because it has feedback when it is clicked
+ def create_submit(self, app):
+ self.submit_button = self.SubmitFeedbackButton(self.app, "submit")
+ self.submit_button.tab_index = 201
+ self.add_button(self.submit_button)
+
+ class SubmitFeedbackButton(FormButton):
+ def render_class(self, session):
+ return "submit"
+
+ def render_content(self, session):
+ return self.parent.render_submit_content(session)
+
+ def render_onclick(self, session):
+ return "feedback_click_button"
Modified: trunk/cumin/python/cumin/objectframe.strings
===================================================================
--- trunk/cumin/python/cumin/objectframe.strings 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/objectframe.strings 2011-02-10 19:40:58 UTC (rev 4517)
@@ -179,3 +179,47 @@
{props}
{stats}
+
+[SubmitFeedbackButton.css]
+div.FeedBackGlass {
+ visibility: visible;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: 200;
+ cursor: wait;
+ top: 0;
+ margin: auto;
+}
+
+div.FeedBackPopup {
+ visibility: visible;
+ position: relative;
+ width: 9em;
+ z-index: 201;
+ cursor: wait;
+ top: 15em;
+ margin: auto;
+ padding: 2em 1em;
+ -moz-border-radius: 0.5em;
+ -webkit-border-radius: 0.5em;
+ background-color: #FFFFFF;
+ border: 2px solid black;
+}
+
+[SubmitFeedbackButton.javascript]
+function feedback_click_button(name, value) {
+ var mbody = $(document.body);
+ var glass = new Element('div');
+ glass.addClass("FeedBackGlass");
+
+ var popup = new Element('div');
+ popup.addClass("FeedBackPopup");
+ popup.set('text', "Sending request.");
+ popup.inject(glass, "top");
+
+ $(document.forms[0]).setStyle('opacity', 0.9)
+ glass.inject(mbody, "top");
+
+ return click_button(name, value);
+}
Modified: trunk/cumin/python/cumin/objectselector.strings
===================================================================
--- trunk/cumin/python/cumin/objectselector.strings 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/objectselector.strings 2011-02-10 19:40:58 UTC (rev 4517)
@@ -124,9 +124,6 @@
</div>
[QmfDetails.css]
-div.QmfDetails {
- margin: 1em;
-}
div.QmfDetails span.DetailsLink {
font-size: 80%;
}
@@ -158,7 +155,6 @@
details.style.display = "block";
branch["{details_path}"] = "1";
}
- //updateUrl["session"] = branch;
wooly.restartIntervalUpdate(updateUrl.marshal());
}
]]>
Modified: trunk/cumin/python/cumin/objecttask.py
===================================================================
--- trunk/cumin/python/cumin/objecttask.py 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/objecttask.py 2011-02-10 19:40:58 UTC (rev 4517)
@@ -79,6 +79,10 @@
return invoc
+ def end(self):
+ # called by the invokation when call completes
+ pass
+
def invoke(self, session, obj, *args, **kwargs):
if obj:
assert isinstance(obj, RosemaryObject), obj
Modified: trunk/cumin/python/cumin/task.py
===================================================================
--- trunk/cumin/python/cumin/task.py 2011-02-10 19:36:43 UTC (rev 4516)
+++ trunk/cumin/python/cumin/task.py 2011-02-10 19:40:58 UTC (rev 4517)
@@ -40,7 +40,7 @@
form_session = wooly.Session(self.app.form_page)
if obj:
- self.form.id.set(nsession, obj._id)
+ self.form.id.set(form_session, obj._id)
self.form.return_url.set(form_session, session.marshal())
self.form.show(form_session)
@@ -162,8 +162,12 @@
return "%s (%s)" % (self.status, str(self.exception))
#return "%s: %s" % (self.status, str(self.output_args))
+ if self.status is self.OK:
+ if self.status_code:
+ return self.status_code
+
return self.status
-
+
def end(self):
log.debug("Ending %s", self.task)
@@ -174,6 +178,7 @@
self.end_time = now
self.update_time = now
+ self.task.end()
log.info("Ended %s", self.task)
13 years, 2 months
r4516 - trunk/wooly/resources
by eallen@fedoraproject.org
Author: eallen
Date: 2011-02-10 19:36:43 +0000 (Thu, 10 Feb 2011)
New Revision: 4516
Modified:
trunk/wooly/resources/mootools.js
Log:
Version of mootools-more with FormValidate object.
Modified: trunk/wooly/resources/mootools.js
===================================================================
--- trunk/wooly/resources/mootools.js 2011-02-10 19:35:50 UTC (rev 4515)
+++ trunk/wooly/resources/mootools.js 2011-02-10 19:36:43 UTC (rev 4516)
@@ -3113,4 +3113,634 @@
yearUntil: '1 year from now',
yearsUntil: '{delta} years from now'
-});
\ No newline at end of file
+});
+
+
+/*
+---
+
+script: Form.Validator.js
+
+name: Form.Validator
+
+description: A css-class based form validation system.
+
+license: MIT-style license
+
+authors:
+ - Aaron Newton
+
+requires:
+ - Core/Options
+ - Core/Events
+ - Core/Selectors
+ - Core/Element.Event
+ - Core/Element.Style
+ - Core/JSON
+ - /Lang
+ - /Class.Binds
+ - /Date
+ - /Element.Forms
+ - /Form.Validator.English
+ - /Element.Shortcuts
+
+provides: [Form.Validator, InputValidator, FormValidator.BaseValidators]
+
+...
+*/
+if (!window.Form) window.Form = {};
+
+var InputValidator = new Class({
+
+ Implements: [Options],
+
+ options: {
+ errorMsg: 'Validation failed.',
+ test: function(field){return true;}
+ },
+
+ initialize: function(className, options){
+ this.setOptions(options);
+ this.className = className;
+ },
+
+ test: function(field, props){
+ if (document.id(field)) return this.options.test(document.id(field), props||this.getProps(field));
+ else return false;
+ },
+
+ getError: function(field, props){
+ var err = this.options.errorMsg;
+ if ($type(err) == 'function') err = err(document.id(field), props||this.getProps(field));
+ return err;
+ },
+
+ getProps: function(field){
+ if (!document.id(field)) return {};
+ return field.get('validatorProps');
+ }
+
+});
+
+Element.Properties.validatorProps = {
+
+ set: function(props){
+ return this.eliminate('validatorProps').store('validatorProps', props);
+ },
+
+ get: function(props){
+ if (props) this.set(props);
+ if (this.retrieve('validatorProps')) return this.retrieve('validatorProps');
+ if (this.getProperty('validatorProps')){
+ try {
+ this.store('validatorProps', JSON.decode(this.getProperty('validatorProps')));
+ }catch(e){
+ return {};
+ }
+ } else {
+ var vals = this.get('class').split(' ').filter(function(cls){
+ return cls.test(':');
+ });
+ if (!vals.length){
+ this.store('validatorProps', {});
+ } else {
+ props = {};
+ vals.each(function(cls){
+ var split = cls.split(':');
+ if (split[1]) {
+ try {
+ props[split[0]] = JSON.decode(split[1]);
+ } catch(e) {}
+ }
+ });
+ this.store('validatorProps', props);
+ }
+ }
+ return this.retrieve('validatorProps');
+ }
+
+};
+
+Form.Validator = new Class({
+
+ Implements:[Options, Events],
+
+ Binds: ['onSubmit'],
+
+ options: {/*
+ onFormValidate: $empty(isValid, form, event),
+ onElementValidate: $empty(isValid, field, className, warn),
+ onElementPass: $empty(field),
+ onElementFail: $empty(field, validatorsFailed) */
+ fieldSelectors: 'input, select, textarea',
+ ignoreHidden: true,
+ ignoreDisabled: true,
+ useTitles: false,
+ evaluateOnSubmit: true,
+ evaluateFieldsOnBlur: true,
+ evaluateFieldsOnChange: true,
+ serial: true,
+ stopOnFailure: true,
+ warningPrefix: function(){
+ return Form.Validator.getMsg('warningPrefix') || 'Warning: ';
+ },
+ errorPrefix: function(){
+ return Form.Validator.getMsg('errorPrefix') || 'Error: ';
+ }
+ },
+
+ initialize: function(form, options){
+ this.setOptions(options);
+ this.element = document.id(form);
+ this.element.store('validator', this);
+ this.warningPrefix = $lambda(this.options.warningPrefix)();
+ this.errorPrefix = $lambda(this.options.errorPrefix)();
+ if (this.options.evaluateOnSubmit) this.element.addEvent('submit', this.onSubmit);
+ if (this.options.evaluateFieldsOnBlur || this.options.evaluateFieldsOnChange) this.watchFields(this.getFields());
+ },
+
+ toElement: function(){
+ return this.element;
+ },
+
+ getFields: function(){
+ return (this.fields = this.element.getElements(this.options.fieldSelectors));
+ },
+
+ watchFields: function(fields){
+ fields.each(function(el){
+ if (this.options.evaluateFieldsOnBlur)
+ el.addEvent('blur', this.validationMonitor.pass([el, false], this));
+ if (this.options.evaluateFieldsOnChange)
+ el.addEvent('change', this.validationMonitor.pass([el, true], this));
+ }, this);
+ },
+
+ validationMonitor: function(){
+ $clear(this.timer);
+ this.timer = this.validateField.delay(50, this, arguments);
+ },
+
+ onSubmit: function(event){
+ if (!this.validate(event) && event) event.preventDefault();
+ else this.reset();
+ },
+
+ reset: function(){
+ this.getFields().each(this.resetField, this);
+ return this;
+ },
+
+ validate: function(event){
+ var result = this.getFields().map(function(field){
+ return this.validateField(field, true);
+ }, this).every(function(v){ return v;});
+ this.fireEvent('formValidate', [result, this.element, event]);
+ if (this.options.stopOnFailure && !result && event) event.preventDefault();
+ return result;
+ },
+
+ validateField: function(field, force){
+ if (this.paused) return true;
+ field = document.id(field);
+ var passed = !field.hasClass('validation-failed');
+ var failed, warned;
+ if (this.options.serial && !force){
+ failed = this.element.getElement('.validation-failed');
+ warned = this.element.getElement('.warning');
+ }
+ if (field && (!failed || force || field.hasClass('validation-failed') || (failed && !this.options.serial))){
+ var validators = field.className.split(' ').some(function(cn){
+ return this.getValidator(cn);
+ }, this);
+ var validatorsFailed = [];
+ field.className.split(' ').each(function(className){
+ if (className && !this.test(className, field)) validatorsFailed.include(className);
+ }, this);
+ passed = validatorsFailed.length === 0;
+ if (validators && !field.hasClass('warnOnly')){
+ if (passed){
+ field.addClass('validation-passed').removeClass('validation-failed');
+ this.fireEvent('elementPass', field);
+ } else {
+ field.addClass('validation-failed').removeClass('validation-passed');
+ this.fireEvent('elementFail', [field, validatorsFailed]);
+ }
+ }
+ if (!warned){
+ var warnings = field.className.split(' ').some(function(cn){
+ if (cn.test('^warn-') || field.hasClass('warnOnly'))
+ return this.getValidator(cn.replace(/^warn-/,''));
+ else return null;
+ }, this);
+ field.removeClass('warning');
+ var warnResult = field.className.split(' ').map(function(cn){
+ if (cn.test('^warn-') || field.hasClass('warnOnly'))
+ return this.test(cn.replace(/^warn-/,''), field, true);
+ else return null;
+ }, this);
+ }
+ }
+ return passed;
+ },
+
+ test: function(className, field, warn){
+ field = document.id(field);
+ if((this.options.ignoreHidden && !field.isVisible()) || (this.options.ignoreDisabled && field.get('disabled'))) return true;
+ var validator = this.getValidator(className);
+ warn = $pick(warn, false);
+ if (field.hasClass('warnOnly')) warn = true;
+ var isValid = field.hasClass('ignoreValidation') || (validator ? validator.test(field) : true);
+ if (validator && field.isVisible()) this.fireEvent('elementValidate', [isValid, field, className, warn]);
+ if (warn) return true;
+ return isValid;
+ },
+
+ resetField: function(field){
+ field = document.id(field);
+ if (field){
+ field.className.split(' ').each(function(className){
+ if (className.test('^warn-')) className = className.replace(/^warn-/, '');
+ field.removeClass('validation-failed');
+ field.removeClass('warning');
+ field.removeClass('validation-passed');
+ }, this);
+ }
+ return this;
+ },
+
+ stop: function(){
+ this.paused = true;
+ return this;
+ },
+
+ start: function(){
+ this.paused = false;
+ return this;
+ },
+
+ ignoreField: function(field, warn){
+ field = document.id(field);
+ if (field){
+ this.enforceField(field);
+ if (warn) field.addClass('warnOnly');
+ else field.addClass('ignoreValidation');
+ }
+ return this;
+ },
+
+ enforceField: function(field){
+ field = document.id(field);
+ if (field) field.removeClass('warnOnly').removeClass('ignoreValidation');
+ return this;
+ }
+
+});
+
+Form.Validator.getMsg = function(key){
+ return MooTools.lang.get('Form.Validator', key);
+};
+
+Form.Validator.adders = {
+
+ validators:{},
+
+ add : function(className, options){
+ this.validators[className] = new InputValidator(className, options);
+ //if this is a class (this method is used by instances of Form.Validator and the Form.Validator namespace)
+ //extend these validators into it
+ //this allows validators to be global and/or per instance
+ if (!this.initialize){
+ this.implement({
+ validators: this.validators
+ });
+ }
+ },
+
+ addAllThese : function(validators){
+ $A(validators).each(function(validator){
+ this.add(validator[0], validator[1]);
+ }, this);
+ },
+
+ getValidator: function(className){
+ return this.validators[className.split(':')[0]];
+ }
+
+};
+
+$extend(Form.Validator, Form.Validator.adders);
+
+Form.Validator.implement(Form.Validator.adders);
+
+Form.Validator.add('IsEmpty', {
+
+ errorMsg: false,
+ test: function(element){
+ if (element.type == 'select-one' || element.type == 'select')
+ return !(element.selectedIndex >= 0 && element.options[element.selectedIndex].value != '');
+ else
+ return ((element.get('value') == null) || (element.get('value').length == 0));
+ }
+
+});
+
+Form.Validator.addAllThese([
+
+ ['required', {
+ errorMsg: function(){
+ return Form.Validator.getMsg('required');
+ },
+ test: function(element){
+ return !Form.Validator.getValidator('IsEmpty').test(element);
+ }
+ }],
+
+ ['minLength', {
+ errorMsg: function(element, props){
+ if ($type(props.minLength))
+ return Form.Validator.getMsg('minLength').substitute({minLength:props.minLength,length:element.get('value').length });
+ else return '';
+ },
+ test: function(element, props){
+ if ($type(props.minLength)) return (element.get('value').length >= $pick(props.minLength, 0));
+ else return true;
+ }
+ }],
+
+ ['maxLength', {
+ errorMsg: function(element, props){
+ //props is {maxLength:10}
+ if ($type(props.maxLength))
+ return Form.Validator.getMsg('maxLength').substitute({maxLength:props.maxLength,length:element.get('value').length });
+ else return '';
+ },
+ test: function(element, props){
+ //if the value is <= than the maxLength value, element passes test
+ return (element.get('value').length <= $pick(props.maxLength, 10000));
+ }
+ }],
+
+ ['validate-integer', {
+ errorMsg: Form.Validator.getMsg.pass('integer'),
+ test: function(element){
+ return Form.Validator.getValidator('IsEmpty').test(element) || (/^(-?[1-9]\d*|0)$/).test(element.get('value'));
+ }
+ }],
+
+ ['validate-numeric', {
+ errorMsg: Form.Validator.getMsg.pass('numeric'),
+ test: function(element){
+ return Form.Validator.getValidator('IsEmpty').test(element) ||
+ (/^-?(?:0$0(?=\d*\.)|[1-9]|0)\d*(\.\d+)?$/).test(element.get('value'));
+ }
+ }],
+
+ ['validate-digits', {
+ errorMsg: Form.Validator.getMsg.pass('digits'),
+ test: function(element){
+ return Form.Validator.getValidator('IsEmpty').test(element) || (/^[\d() .:\-\+#]+$/.test(element.get('value')));
+ }
+ }],
+
+ ['validate-alpha', {
+ errorMsg: Form.Validator.getMsg.pass('alpha'),
+ test: function(element){
+ return Form.Validator.getValidator('IsEmpty').test(element) || (/^[a-zA-Z]+$/).test(element.get('value'));
+ }
+ }],
+
+ ['validate-alphanum', {
+ errorMsg: Form.Validator.getMsg.pass('alphanum'),
+ test: function(element){
+ return Form.Validator.getValidator('IsEmpty').test(element) || !(/\W/).test(element.get('value'));
+ }
+ }],
+
+ ['validate-date', {
+ errorMsg: function(element, props){
+ if (Date.parse){
+ var format = props.dateFormat || '%x';
+ return Form.Validator.getMsg('dateSuchAs').substitute({date: new Date().format(format)});
+ } else {
+ return Form.Validator.getMsg('dateInFormatMDY');
+ }
+ },
+ test: function(element, props){
+ if (Form.Validator.getValidator('IsEmpty').test(element)) return true;
+ var d;
+ if (Date.parse){
+ var format = props.dateFormat || '%x';
+ d = Date.parse(element.get('value'));
+ var formatted = d.format(format);
+ if (formatted != 'invalid date') element.set('value', formatted);
+ return !isNaN(d);
+ } else {
+ var regex = /^(\d{2})\/(\d{2})\/(\d{4})$/;
+ if (!regex.test(element.get('value'))) return false;
+ d = new Date(element.get('value').replace(regex, '$1/$2/$3'));
+ return (parseInt(RegExp.$1, 10) == (1 + d.getMonth())) &&
+ (parseInt(RegExp.$2, 10) == d.getDate()) &&
+ (parseInt(RegExp.$3, 10) == d.getFullYear());
+ }
+ }
+ }],
+
+ ['validate-email', {
+ errorMsg: Form.Validator.getMsg.pass('email'),
+ test: function(element){
+ return Form.Validator.getValidator('IsEmpty').test(element) || (/^[A-Z0-9._%+-]+(a)[A-Z0-9.-]+\.[A-Z]{2,4}$/i).test(element.get('value'));
+ }
+ }],
+
+ ['validate-url', {
+ errorMsg: Form.Validator.getMsg.pass('url'),
+ test: function(element){
+ return Form.Validator.getValidator('IsEmpty').test(element) || (/^(https?|ftp|rmtp|mms):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i).test(element.get('value'));
+ }
+ }],
+
+ ['validate-currency-dollar', {
+ errorMsg: Form.Validator.getMsg.pass('currencyDollar'),
+ test: function(element){
+ // [$]1[##][,###]+[.##]
+ // [$]1###+[.##]
+ // [$]0.##
+ // [$].##
+ return Form.Validator.getValidator('IsEmpty').test(element) || (/^\$?\-?([1-9]{1}[0-9]{0,2}(\,[0-9]{3})*(\.[0-9]{0,2})?|[1-9]{1}\d*(\.[0-9]{0,2})?|0(\.[0-9]{0,2})?|(\.[0-9]{1,2})?)$/).test(element.get('value'));
+ }
+ }],
+
+ ['validate-one-required', {
+ errorMsg: Form.Validator.getMsg.pass('oneRequired'),
+ test: function(element, props){
+ var p = document.id(props['validate-one-required']) || element.getParent(props['validate-one-required']);
+ return p.getElements('input').some(function(el){
+ if (['checkbox', 'radio'].contains(el.get('type'))) return el.get('checked');
+ return el.get('value');
+ });
+ }
+ }]
+
+]);
+
+Element.Properties.validator = {
+
+ set: function(options){
+ var validator = this.retrieve('validator');
+ if (validator) validator.setOptions(options);
+ return this.store('validator:options', options);
+ },
+
+ get: function(options){
+ if (options || !this.retrieve('validator')){
+ if (options || !this.retrieve('validator:options')) this.set('validator', options);
+ this.store('validator', new Form.Validator(this, this.retrieve('validator:options')));
+ }
+ return this.retrieve('validator');
+ }
+
+};
+
+Element.implement({
+
+ validate: function(options){
+ if (options) this.set('validator', options);
+ return this.get('validator', options).validate();
+ }
+
+});
+//legacy
+var FormValidator = Form.Validator;
+
+
+/*
+---
+
+script: Date.English.US.js
+
+name: Date.English.US
+
+description: Date messages for US English.
+
+license: MIT-style license
+
+authors:
+ - Aaron Newton
+
+requires:
+ - /Lang
+
+provides: [Date.English.US]
+
+...
+*/
+
+MooTools.lang.set('en-US', 'Date', {
+
+ months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+
+ // Culture's date order: MM/DD/YYYY
+ dateOrder: ['month', 'date', 'year'],
+ shortDate: '%m/%d/%Y',
+ shortTime: '%I:%M%p',
+ AM: 'AM',
+ PM: 'PM',
+
+ // Date.Extras
+ ordinal: function(dayOfMonth){
+ // 1st, 2nd, 3rd, etc.
+ return (dayOfMonth > 3 && dayOfMonth < 21) ? 'th' : ['th', 'st', 'nd', 'rd', 'th'][Math.min(dayOfMonth % 10, 4)];
+ },
+
+ lessThanMinuteAgo: 'less than a minute ago',
+ minuteAgo: 'about a minute ago',
+ minutesAgo: '{delta} minutes ago',
+ hourAgo: 'about an hour ago',
+ hoursAgo: 'about {delta} hours ago',
+ dayAgo: '1 day ago',
+ daysAgo: '{delta} days ago',
+ weekAgo: '1 week ago',
+ weeksAgo: '{delta} weeks ago',
+ monthAgo: '1 month ago',
+ monthsAgo: '{delta} months ago',
+ yearAgo: '1 year ago',
+ yearsAgo: '{delta} years ago',
+
+ lessThanMinuteUntil: 'less than a minute from now',
+ minuteUntil: 'about a minute from now',
+ minutesUntil: '{delta} minutes from now',
+ hourUntil: 'about an hour from now',
+ hoursUntil: 'about {delta} hours from now',
+ dayUntil: '1 day from now',
+ daysUntil: '{delta} days from now',
+ weekUntil: '1 week from now',
+ weeksUntil: '{delta} weeks from now',
+ monthUntil: '1 month from now',
+ monthsUntil: '{delta} months from now',
+ yearUntil: '1 year from now',
+ yearsUntil: '{delta} years from now'
+
+});
+
+
+/*
+---
+
+script: Form.Validator.English.js
+
+name: Form.Validator.English
+
+description: Form Validator messages for English.
+
+license: MIT-style license
+
+authors:
+ - Aaron Newton
+
+requires:
+ - /Lang
+
+provides: [Form.Validator.English]
+
+...
+*/
+
+MooTools.lang.set('en-US', 'Form.Validator', {
+
+ required: 'This field is required.',
+ minLength: 'Please enter at least {minLength} characters (you entered {length} characters).',
+ maxLength: 'Please enter no more than {maxLength} characters (you entered {length} characters).',
+ integer: 'Please enter an integer in this field. Numbers with decimals (e.g. 1.25) are not permitted.',
+ numeric: 'Please enter only numeric values in this field (i.e. "1" or "1.1" or "-1" or "-1.1").',
+ digits: 'Please use numbers and punctuation only in this field (for example, a phone number with dashes or dots is permitted).',
+ alpha: 'Please use only letters (a-z) within this field. No spaces or other characters are allowed.',
+ alphanum: 'Please use only letters (a-z) or numbers (0-9) in this field. No spaces or other characters are allowed.',
+ dateSuchAs: 'Please enter a valid date such as {date}',
+ dateInFormatMDY: 'Please enter a valid date such as MM/DD/YYYY (i.e. "12/31/1999")',
+ email: 'Please enter a valid email address. For example "fred(a)domain.com".',
+ url: 'Please enter a valid URL such as http://www.google.com.',
+ currencyDollar: 'Please enter a valid $ amount. For example $100.00 .',
+ oneRequired: 'Please enter something for at least one of these inputs.',
+ errorPrefix: 'Error: ',
+ warningPrefix: 'Warning: ',
+
+ // Form.Validator.Extras
+ noSpace: 'There can be no spaces in this input.',
+ reqChkByNode: 'No items are selected.',
+ requiredChk: 'This field is required.',
+ reqChkByName: 'Please select a {label}.',
+ match: 'This field needs to match the {matchName} field',
+ startDate: 'the start date',
+ endDate: 'the end date',
+ currendDate: 'the current date',
+ afterDate: 'The date should be the same or after {label}.',
+ beforeDate: 'The date should be the same or before {label}.',
+ startMonth: 'Please select a start month',
+ sameMonth: 'These two dates must be in the same month - you must change one or the other.',
+ creditcard: 'The credit card number entered is invalid. Please check the number and try again. {length} digits entered.'
+
+});
+
+
13 years, 2 months
r4515 - trunk/wooly/python/wooly
by eallen@fedoraproject.org
Author: eallen
Date: 2011-02-10 19:35:50 +0000 (Thu, 10 Feb 2011)
New Revision: 4515
Modified:
trunk/wooly/python/wooly/table.py
Log:
Don't put html comments in the css when in debug mode as this causes WebKit based browsers to freak out.
Modified: trunk/wooly/python/wooly/table.py
===================================================================
--- trunk/wooly/python/wooly/table.py 2011-02-10 19:31:19 UTC (rev 4514)
+++ trunk/wooly/python/wooly/table.py 2011-02-10 19:35:50 UTC (rev 4515)
@@ -138,6 +138,12 @@
pass
class TableColumnCss(TableChild):
+ def __init__(self, app, name):
+ super(TableColumnCss, self).__init__(app, name)
+
+ # html comments in the css freaks out webkit browsers
+ self.comments_enabled = False
+
def render_class(self, session):
return self.parent.name
@@ -207,13 +213,13 @@
def __init__(self, app, name):
super(NewCheckboxColumn, self).__init__(app, name)
- self.selection = selection # XXX huh?
+ #self.selection = selection # XXX huh?
self.header = CheckboxColumnHeader(app, "header")
self.replace_child(self.header)
- self.cell = CheckboxColumnCell(app, "cell", selection)
- self.replace_child(self.cell)
+ #self.cell = CheckboxColumnCell(app, "cell", selection)
+ #self.replace_child(self.cell)
self.sortable = False
self.width = "1%"
13 years, 2 months
r4514 - trunk/wooly/python/wooly
by eallen@fedoraproject.org
Author: eallen
Date: 2011-02-10 19:31:19 +0000 (Thu, 10 Feb 2011)
New Revision: 4514
Modified:
trunk/wooly/python/wooly/forms.py
trunk/wooly/python/wooly/forms.strings
Log:
Allow forms to easily override the submit button.
Allow forms to easily override their css class name.
Modified: trunk/wooly/python/wooly/forms.py
===================================================================
--- trunk/wooly/python/wooly/forms.py 2011-02-10 19:28:37 UTC (rev 4513)
+++ trunk/wooly/python/wooly/forms.py 2011-02-10 19:31:19 UTC (rev 4514)
@@ -480,9 +480,6 @@
assert self.param is not None
assert isinstance(self.param, Parameter)
- def get(self, session):
- return self.param.get(session)
-
def add_option(self, option):
assert isinstance(option, RadioFieldOption)
@@ -572,6 +569,9 @@
return writer.to_string()
+ def render_form_class(self, session, *args):
+ return "ButtonForm"
+
class SubmitForm(ButtonForm):
def __init__(self, app, name):
super(SubmitForm, self).__init__(app, name)
@@ -579,14 +579,18 @@
self.return_url = Parameter(app, "return")
self.add_parameter(self.return_url)
- self.submit_button = self.Submit(app, "submit")
- self.submit_button.tab_index = 201
- self.add_button(self.submit_button)
+ self.create_submit(app)
self.cancel_button = self.Cancel(app, "cancel")
self.cancel_button.tab_index = 200
self.add_button(self.cancel_button)
+ # this is a method so it can be overridden
+ def create_submit(self, app):
+ self.submit_button = self.Submit(app, "submit")
+ self.submit_button.tab_index = 201
+ self.add_button(self.submit_button)
+
def submit(self, session):
self.submit_button.set(session, True)
Modified: trunk/wooly/python/wooly/forms.strings
===================================================================
--- trunk/wooly/python/wooly/forms.strings 2011-02-10 19:28:37 UTC (rev 4513)
+++ trunk/wooly/python/wooly/forms.strings 2011-02-10 19:31:19 UTC (rev 4514)
@@ -302,7 +302,7 @@
}
[ButtonForm.html]
-<form id="{id}" class="ButtonForm" method="post" action="?">
+<form id="{id}" class="{form_class}" method="post" action="?">
<div class="title">{title}</div>
<div class="content">
{errors}
13 years, 2 months
r4513 - trunk/wooly/python/wooly
by eallen@fedoraproject.org
Author: eallen
Date: 2011-02-10 19:28:37 +0000 (Thu, 10 Feb 2011)
New Revision: 4513
Modified:
trunk/wooly/python/wooly/__init__.py
trunk/wooly/python/wooly/profile.py
Log:
Don't put html comments in the css when in debug mode as this causes WebKit based browsers to freak out.
Modified: trunk/wooly/python/wooly/__init__.py
===================================================================
--- trunk/wooly/python/wooly/__init__.py 2011-02-09 18:45:32 UTC (rev 4512)
+++ trunk/wooly/python/wooly/__init__.py 2011-02-10 19:28:37 UTC (rev 4513)
@@ -113,6 +113,7 @@
self.html_class = None
self.update_enabled = False
self.defer_enabled = False
+ self.comments_enabled = True
# These are computed later in the init pass
self.ancestors = None
Modified: trunk/wooly/python/wooly/profile.py
===================================================================
--- trunk/wooly/python/wooly/profile.py 2011-02-09 18:45:32 UTC (rev 4512)
+++ trunk/wooly/python/wooly/profile.py 2011-02-10 19:28:37 UTC (rev 4513)
@@ -168,7 +168,7 @@
assert self.start < self.end
- if self.widget.parent:
+ if self.widget.parent and self.widget.comments_enabled:
cls = self.widget.__class__
name = self.widget.__repr__()
fmt = "<!-- \nopen: %s\n-->%s<!-- \nclose: %s\n-->"
13 years, 2 months
r4512 - branches/scale_testing/mech_scripts
by tmckay@fedoraproject.org
Author: tmckay
Date: 2011-02-09 18:45:32 +0000 (Wed, 09 Feb 2011)
New Revision: 4512
Modified:
branches/scale_testing/mech_scripts/process_times.py
branches/scale_testing/mech_scripts/test_driver.py
Log:
Handle blank lines in link files, sort pages by longest time in process_times.py
Modified: branches/scale_testing/mech_scripts/process_times.py
===================================================================
--- branches/scale_testing/mech_scripts/process_times.py 2011-02-09 14:41:07 UTC (rev 4511)
+++ branches/scale_testing/mech_scripts/process_times.py 2011-02-09 18:45:32 UTC (rev 4512)
@@ -9,6 +9,7 @@
import os
import csv
import optparse
+from operator import itemgetter
def dump(stats):
try:
@@ -80,11 +81,12 @@
f = open(p, "r")
for line in f:
line_count += 1
+ line = line.strip()
# Look for page end marker, will contain total size for that page for new stuff.
- # Old stuff might still have a blank line
- times = line.strip().split(",")
- if times[0] == "page_break" or times[0] == "":
+ # Old stuff might still have a blank line
+ times = line.split(",")
+ if times[0] == "page_break" or len(line) == 0:
last_page_size = page_size
if len(times) > 1:
page_size = int(times[1])
@@ -170,28 +172,43 @@
parser = optparse.OptionParser()
parser.add_option("--csvfile", dest="csvfile", default="")
parser.add_option("--threads", dest="threads", type=int, default=0)
+ parser.add_option("--sort", dest="sort", action="store_true", default=False)
opts, args = parser.parse_args()
- if len(sys.argv) < 2:
+ if len(args) < 1:
print "Forgot the file, eh?"
return
# Allowing processing of a single page file, possibly with multiple iterations
# or an entire directory of page files, each with multiple iterations
file_list = []
- if os.path.isdir(sys.argv[1]):
- for i in os.listdir(sys.argv[1]):
- file_list.append(os.path.join(sys.argv[1], i))
+ if os.path.isdir(args[0]):
+ for i in os.listdir(args[0]):
+ file_list.append(os.path.join(args[0], i))
else:
- file_list = [sys.argv[1]]
+ file_list = [args[0]]
+ # Process all pages, keep a running list of segment totals
+ # Optionally set up for sorting by longest processing time
totals = get_zero_list()
- for i in file_list:
- page_values = process_file(i)
+ sorted_times = dict()
+ for f in file_list:
+ page_values = process_file(f)
for i in range(len(page_values)):
totals[i] += page_values[i]
+ if opts.sort:
+ sorted_times[f] = page_values
+ # element 5 will be the total for each page
+ if opts.sort:
+ def get(it):
+ return it[1][5]
+
+ a = sorted(sorted_times.items(), key=get)
+ for i in a:
+ print i[0], i[1][5]
+
# Find average of all pages processed over multiple iterations.
for i in range(len(totals)):
totals[i] = totals[i] / len(file_list)
Modified: branches/scale_testing/mech_scripts/test_driver.py
===================================================================
--- branches/scale_testing/mech_scripts/test_driver.py 2011-02-09 14:41:07 UTC (rev 4511)
+++ branches/scale_testing/mech_scripts/test_driver.py 2011-02-09 18:45:32 UTC (rev 4512)
@@ -35,6 +35,41 @@
self.file_iterations = 0
self.last_page_total = 0
+ def init(self):
+ parser = optparse.OptionParser()
+ parser.add_option('--machine', dest='machine', type='str', default="localhost:45672")
+ parser.add_option('--file', dest='file', type='str', default="./links")
+ parser.add_option('--iter', dest='iter', type='int', default=1)
+ parser.add_option('--info', dest='info', action="store_true", default=False)
+ parser.add_option('--noskip', dest='skip', action="store_false", default=True)
+
+ # combine these to create an output file name for web server internal stats
+ parser.add_option('--group', dest='group', type='str', default="headers")
+ parser.add_option('--thread', dest='thread', type='str', default="")
+ parser.add_option('--outdir', dest='outdir', type='str', default=".")
+
+
+ if type(self.script_options) == str:
+ self.script_options = self.script_options.split()
+
+ opts, args = parser.parse_args(self.script_options)
+
+ self.machine = opts.machine
+ self.link_file = os.path.abspath(opts.file)
+ self.iteration_limit = opts.iter
+ self.print_info = opts.info
+ self.skip = opts.skip
+ self.group = opts.group
+ self.thread = opts.thread
+ self.outdir = opts.outdir
+ if self.outdir != "." and not os.path.isdir(self.outdir):
+ try:
+ # in a multi thread environment, we may
+ # actually collide here in the creation.
+ os.mkdir(self.outdir)
+ except:
+ pass
+
def next_file(self):
if not self.file_init:
self.file_init = True
@@ -80,42 +115,10 @@
# Sum the times for individual gets to look just at page requests
start_timer = time_time()
- # parse options on first invocation
+ # set up on first invocation. It's done this way rather than in __init
+ # because of the way internals in multi-mechanize work.
if self.machine == None:
-
- parser = optparse.OptionParser()
- parser.add_option('--machine', dest='machine', type='str', default="localhost:45672")
- parser.add_option('--file', dest='file', type='str', default="./links")
- parser.add_option('--iter', dest='iter', type='int', default=1)
- parser.add_option('--info', dest='info', action="store_true", default=False)
- parser.add_option('--noskip', dest='skip', action="store_false", default=True)
-
- # combine these to create an output file name for web server internal stats
- parser.add_option('--group', dest='group', type='str', default="headers")
- parser.add_option('--thread', dest='thread', type='str', default="")
- parser.add_option('--outdir', dest='outdir', type='str', default=".")
-
-
- if type(self.script_options) == str:
- self.script_options = self.script_options.split()
-
- opts, args = parser.parse_args(self.script_options)
-
- self.machine = opts.machine
- self.link_file = os.path.abspath(opts.file)
- self.iteration_limit = opts.iter
- self.print_info = opts.info
- self.skip = opts.skip
- self.group = opts.group
- self.thread = opts.thread
- self.outdir = opts.outdir
- if self.outdir != "." and not os.path.isdir(self.outdir):
- try:
- # in a multi thread environment, we may
- # actually collide here in the creation.
- os.mkdir(self.outdir)
- except:
- pass
+ self.init()
curr_file = self.next_file()
f = open(curr_file, "r")
@@ -125,8 +128,12 @@
total_gets = total_size = get_latency = 0
for link in f:
+ # skip blank lines
+ link = link.strip()
+ if len(link) == 0:
+ continue
link_info = link.split(',')
-
+
# Check for initial Boolean, indicates whether "If_Modified" header was used
if len(link_info) == 3:
# Newest file type. If_Modified header, then Cache-Control
13 years, 2 months