diff options
author | Patrick J Cherry <patrick@bytemark.co.uk> | 2011-04-13 17:03:16 +0100 |
---|---|---|
committer | Patrick J Cherry <patrick@bytemark.co.uk> | 2011-04-13 17:03:16 +0100 |
commit | 89a67770e66d11740948e90a41db6cee0482cf8e (patch) | |
tree | be858515fb789a89d68f94975690ab019813726c |
new version.
133 files changed, 14550 insertions, 0 deletions
diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..5afd643 --- /dev/null +++ b/.hgignore @@ -0,0 +1,4 @@ +~$ +test.log$ +test.db$ +test.conf$ diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..2b04e22 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,21 @@ +... Because it is needed. + +Version 1.0.13 + * Buttons now appear in a line. + * Function were called in wrong order on buttons. + * Alerts no longer sorted by source, just by subject. + * Alerts sorted by subject then summary. + * Page no longer needs to reload on status changes. No new conglomerate created. + * Bug #1111 fix: Alerts do not acknowledge for longer than maximum time. + * Bug #1116 fix: Ajax requests take too long, now spawn a new thread -- + see feature 1131 for full fix. + + +Version 1.0.12 + * Added time scales to bulk alerts acknowledgement. + * Change the web page header to be simpler and nicer. + * Added link to alert history. + + +Version 1.0.11 and below. + * Nothing documented. @@ -0,0 +1,164 @@ +** Mauve Alert + + a messaging system for system and network administrators to help you sleep + better, and be attentive to your computers. + + Holly: Purple alert! Purple alert! + + Lister: What's a 'Purple Alert'? + + Holly: Well, it's sort of like, not as bad as a red alert, but a bit worse + than a blue alert. Kind of like a mauve alert, don't want to say + 'mauve alert.' + + Rimmer: Holly, wipe the rabid foam from your chin and start again. + +** Introduction + +If you run a network where too many scripts know your mobile phone number, +or you run up huge text messaging bills, your inbox is filled with +wall-to-wall red alerts of questionable urgency, or you want to be able +to make use of SNMP without a huge effort up-front, mauve may be for you. + +Mauve is not a network monitoring system, but it makes it very easy to +construct a reliable one from only shell scripts and cron. If you have a +network monitoring system (or more likely, several), mauve lets you consolidate +or reorganise how it alerts you and your team. + +Many monitoring systems only deal in red alerts, and put a lot of design +effort into screaming like a baby. The result of having multiple monitoring +systems all screaming at once, is that it's difficult to keep on top of what +alerts are actually important. The weary sysadmin learns to ignore their +text message noise in certain patterns, at certain times of day, filters their +email and so on, at the risk of ignoring something critical. + +Mauve is intended to be a bucket into which the sysadmin can easily throw +alerts. He can then decide on a central policy for distribution to his team, +saving the screaming red lights for really urgent problems. + +Many controls are built into mauve to ensure alerts are responded to by team +members. If it doesn't know any better, the system will send SMS messages to +a mobile phone, emails, or instant messages, and wait for the user to +acknowledge its alert(s), reminding the user at configured intervals. + +The user can also run a special client which suppresses the normal alerts +clogging up her email or phone. This mode of operation is intended for users +with 24/7 connected devices with richer alerting functions than a simple +text message beep or email chime. They are by their desks running a particular +program. Mauve comes with a simple prototype for Linux, and a smartphone +client for Android phones. [TODO] + +** Important data in mauve + +An alert is an event signalled by a system in your network, and it suggests +that something needs attention within a timeframe of a few minutes to a few +days. Alerts are not intended to be useful for helping you organise jobs +that can take longer than this, and are not intended to organise multi-stage +complicated jobs. + +Alerts are raised and cleared only by the system, and acknowledged by people. + +An acknowledgement is a message to the server that someone is attending to +the alert, and that it can expect the alert to be cleared within a particular +time frame. mauveserver provides a web interface through which users can +acknowledge alerts. + +Alerts are sent in a group, in a single packet called an "alert update". +Individual alert systems can send a complete update, specifying all the current +alerts that they are aware of - or they can send updates consisting of alert/ +clear messages as each change occurs. + +The information flow looks like this: + + (single UDP packets) + alert notifi (SMS/email etc.) + updates cations +[alert source 1] ------> [ ] -------> [person 1] +[alert source 2] ------> [alert station] -------> [person 2] +[alert source 3] ------> [ ] ... | + ... | | + \-----------<<--------+ + acknowledgements + (web interface) + +** Alert updates + +The alert update is the most important data structure and is described fully +in the mauve.proto file. See later on in this document for how these fields +are use to implement various monitoring patterns in Bytemark's network. + +Alert updates are not intended to span more than a single UDP packet, and +are therefore limited to 64KB. Their format is a packed binary defined by +Google's protobuf tools, and a single alert describing typical network events +will be in the order of 100s or even 10s of bytes. + +** Alert transmission and network architecture [NOT IMPLEMENTED, BAD IDEA?] + +Alert Updates are intended to be transmitted several times simultaneously +through a network of relays, each of which will forward to the intended +destination (the Alert Station), i.e. + + --------> relay ----\ + / \ +Alert source ---------> relay ----\ \ + \ \ \ + \ \ \ + --------------------v v v + Alert station + + +The canny administrator of a multi-homed mauve network is anticipating total +network failure and/or heavily congested links. Relay stations should be +positioned such that simultaneous transmission from a single source in the +network will result in different paths being taken to relays and/or final +destination. + +Alerts signal often fleeting events, and may not be of any significance if +they do not arrive at the alert station within a minute of transmission. +Transmission of an alert should be a "fire and forget" event, and the onus +of reliability of transmission lies with the network designer rather than +any protocol or queuing. + +Therefore we allow simple proxying of alerts, and the alert sender should +send alerts to all proxies and the final destination at once. The +destination will only process the same update once, so it's safe to try to +transmit through many different paths. + +As currently implemented, the alert station in a mauve system cannot be +duplicated or replicated reliably, and I'm not keen to try. A failover alert +station must be part of your planning! + +** Alert stations [TO REVISE] + +Alert stations are the central point in mauve's alert system. They are the +destinations for devices to send their alerts. It is a feature of the mauve +alert protocols that many alert stations can be configured, and they should +all contain the same information. + +Devices send their alerts to every configured alert station. + +Monitors can connect to multiple alert stations to find out which is the most +up-to-date. + + send/clear alerts copy alerts + devices --------------------> alert station -------------> alert monitor + <------------- + acknowledge alerts + +** Software components of mauve + +Mauve is a set of tools to construct an alert system that suits your network. +It has several front-end tools: + + mauvesend - a command line tool to send alert updates to a server + + mauveserver - the alert receiver, which stores incoming alerts in a database, + consults a configuration to decide who needs to know about which alerts, + and sends out texts, emails or jabber messages. + + mauveclient - a prototype client program allowing a person to acknowledge + alerts. + +** Installation + + @@ -0,0 +1,146 @@ + TODO list for mauve + ---=== o O o ===--- + + +1 Unit test. +------------- + +We need a lot more. Currently, the testing is woefully inadequate. In +specific: + +1.1 test_cases.rb + + This needs a little work so that the tests in there run faster. The +test currently run in nearly four minutes which is too long. The +main problem appears to be the starting and stopping of the server. +Using a server for all the tests should work provided that we remove the +fake alert created by each test in the database in the teardown method. + + This could be linked to 2.3 + +1.2 The rest... + + Too many classes have no unit tests. This has to change. + + Everything new must have unit test associated with it. + + +2 Code refactor. +----------------- + + The idea here is to have a mauve server that receive new Alerts and +process them as state machines. A brain within this should determine +which Person gets a notification. This brain needs to be accessible via +the UI since events within the UI will impact the Alert -- such as +acknowledge events. + + The UI should be separate from the mauve server. The UI either needs +a hook into the backend database or send updated alerts. I prefer the +latter. + + +2.1 Timers class. + + This needs ripping out and replacing with event machine. + + +2.2 Alert class. + + This needs ripping out and replacing with a state machine. Something +like http://slagyr.github.com/statemachine/ maybe useful. + + A brain need creating as well. + + Mauvesend may need to be refactored to conform to the state machine +model. It should not be much work but will require major work to deploy +on all systems. + + This is lots of work. + + +2.3 Configuration class. + + Nothing here is essential but would be nice to have. + + This is fine but needs more error checking of the loaded configuration +file. + + A method to reload the configuration file gracefully -- aka without a +restart. This could be linked to 1.1. + + +2.4 UI, the web interface. + + This needs ripping out and separated as a new process. We can have a +thread in the main mauve server process write the date to a file every X +seconds. The UI can display this date and thus any check can be done on +that date. If it is older than X, then the main server has stopped and +we should alert everybody. + + + +3 General work. +---------------- + +3.1 Move to apt-ruby. + + We must get ride of the external directories and move fully to +apt-ruby. This should lead to better deployment on Debian. + + +3.2 Rubinius. + + Make sure we can run the code in Rubinius. Not urgent until the GIL +is fixed. Note that this may entails some new code for the XMPP +notification class. + + I tried to do this on Tue Jan 18 with rbx-head hydra (no GIL devel +branch) and it did not work well at all. The unit test ran slower than +with mri and failed on multiple occations (140 tests, 248 assertions, 0 +failures, 122 errors, 0 pendings, 0 omissions, 0 notifications 47.8571% +passed). I suspect rr mock and log4r gems are not playing well with +rbx as most errors were due to interactions between those two. It could +be that the rbx-head version is really not ready to be used in +production. + + +3.3 Documentation. + + We need some more in code documentation although it is now adequate. + + User documentation, especially with regards to the configuration file, +need a lot of work. Not sure where is the best: Redmine wiki, Bytemark +wiki, text file in repository... + + + + +4 New features. +--------------- + +4.1 XMPP/email interface. + + The ability to manipulate Alerts via XMPP and eamil. + + +4.2 Suppress Alerts. + + The ability to suppress alerts for a short time. + + +4.3 Notes for Alerts. + + The ability to create notes on alerts. + + +4.4 Better alert history. + + Either just cosmetic or require code changes to have a better timeline +of alerts status. + + +4.5 Really SMTP + + Move new smtp code so that we do not use the local mailer but send the +email directly. The code (with documentation and tests) is written but +not incorporated yet. diff --git a/bin/jconsole b/bin/jconsole new file mode 100755 index 0000000..6e638ad --- /dev/null +++ b/bin/jconsole @@ -0,0 +1,23 @@ +#!/usr/bin/env jruby + +require 'pp' +pp $: + +Thread.abort_on_exception = true +require 'irb' +require 'thread' + +# hack for delving into test-generated configurations +class AlertAndNotificationLogic; Alerts = Queue.new; end + +require 'mauve/configuration' +include Mauve +#raise "must specify config file" unless ARGV.length > 0 +unless ARGV.length > 0 + STDERR.print("You must specify a configuration file as $arg[1]\n") + STDERR.print(" eg: ./mauve_starter ./bin/console ./test/local.conf\n") + exit 1 +end +Configuration.current = ConfigurationBuilder.load(ARGV.shift) +IRB.start + diff --git a/bin/jmauveserver b/bin/jmauveserver new file mode 100755 index 0000000..9cdd26f --- /dev/null +++ b/bin/jmauveserver @@ -0,0 +1,65 @@ +#!/usr/bin/env jruby + +begin + eval "Proc.new { |a,&b| }" +rescue SyntaxError => no_blocks_with_procs + STDERR.print "mauveserver must have Ruby 1.8.7 or later, sorry (if you "+ + "try, you'll get a SyntaxError trying to load one of its libraries)\n" + exit 1 +end + +require 'mauve/configuration' +include Mauve + +configuration_file = ARGV[0] +if !configuration_file + if File.exists?("/etc/mauvealert/mauveserver.conf") + configuration_file = "/etc/mauvealert/mauveserver.conf" + else + STDERR.print "Syntax: #{$0} <configuration filename>\n" + exit 1 + end +end +Configuration.current = ConfigurationBuilder.load(configuration_file) + +class RestartSignalReceived < Exception; end +trap("HUP") do + # this blows up if you do it twice in quick succession, but don't really + # care about that case as it's only for log rotation. + Thread.main.raise(RestartSignalReceived.new) +end + +# Start canary thread. +#require 'canary' +#Canary.start(1, Log) + +# Main loop +@logger = Log4r::Logger.new "mauve::server<#{Process.pid}>" +loop do + begin + Configuration.current.server.run + rescue Interrupt + @logger.info "Interrupted by user, exiting" + Configuration.current.close + exit 0 + rescue RestartSignalReceived => ex + @logger.info "Restart signal received, reloading the configuration" + begin + new_configuration = ConfigurationBuilder.load(configuration_file) + rescue Exception => ex + @logger.error "Error reloading configuration, reusing old one: #{ex}" + @logger.debug ex.backtrace.join("\n") + end + + Configuration.current.close + Configuration.current = new_configuration + rescue Exception => ex + @logger.fatal("Uncaught #{ex.class} exception, will try to log") + @logger.fatal("Class: #{ex.class} "+ + "Message: #{ex.message} "+ + "Backtrace: #{ex.backtrace.join("\n")}") + exit 9 + end +end + + diff --git a/bin/mauveclient b/bin/mauveclient new file mode 100755 index 0000000..d513926 --- /dev/null +++ b/bin/mauveclient @@ -0,0 +1,172 @@ +#! /usr/bin/ruby1.8 +# == Synopsis +# +# mauvesend: send alert(s) to a given alert station +# +# == Usage +# +# mauvesend [destination] +# [--source | -o <source>] [--replace | -p] [--verbose | -v] +# [--id <alert ID> [alert options] ... ] +# +# <destination>: +# where the alert should go, can be one of: +# SRV record from DNS (we add _mauvealert._udp to record name) +# normal hostname (i.e. A record) +# IP address:port number +# +# if no destination is supplied, reads parameter from file +# /etc/mauvealert/mauvesend.destination (otherwise throws an error). +# +# --source | -o <source>: +# identify the source of the alert (defaults to hostname, but you might +# want to name your monitoring systems more explicitly). +# +# --replace | -p: +# Send an update replacing all other alerts for this source - any previous +# alerts not specified in this update are assumed to be cleared. If you +# specify this option, you don't have to supply *any* alerts to raise or +# clear (in which case all alerts from that source will be cleared). +# +# --verbose | -v: +# If you specify this option once, it will print the transmission ID +# of the packet for debugging. If you specify it twice, it will print +# the entire data structure. +# +# You can specify any number of alerts in an update - every time you specify +# --id starts a new alert. +# +# --id | -i <alert ID>: +# alert ID; unique specified for each alert raised. +# +# --summary | -s <summary>: +# text for humans describing the nature of the alert, first 100 characters +# are only ones guaranteed to make it to pagers, twitter, SMS etc. +# +# --detail | -d <detail>: +# HTML fragment describing the alert in more detail, no limit on length. +# +# --subject | -u <subject>: +# set the subject of the alert (i.e. the server/entity that this alert +# concerns). +# +# --clear | -c <time>: +# mark the alert to be cleared at the given time, or +N where N is a number +# of seconds, or 'now'. If not supplied, the alert is deemed to be raised +# until explicitly cleared. +# +# --raise | -r <time>: +# mark the alert to be (re)raised at the given time. If not supplied, the +# alert will be raised immediately. +# + +require 'getoptlong' +require 'mauve/sender' +require 'mauve/proto' +require 'mauve/mauve_time' +require 'pp' + +def error(msg) + STDERR.print "*** Error: #{msg}\n" + STDERR.print "*** For help, type: #{$0} -h\n" + exit 1 +end + +def parse_time_spec(spec) + now = Mauve::MauveTime.now + + return now if spec == 'now' + + case spec[0] + when ?+ then multiplier = 1 + when ?- then multiplier = -1 + else + return Mauve::MauveTime.parse(spec) + end + multiplier *= case spec[-1] + when ?m then 60 + when ?h then 3600 + when ?d then 86400 + else + 1 + end + + now + spec[1..-1].to_i * multiplier +end + +update = Mauve::Proto::AlertUpdate.new +update.replace = false +update.alert = [] +message = nil +verbose = 0 +help = false + +GetoptLong.new( + ['-h', '--help', GetoptLong::NO_ARGUMENT], + ['-o', '--source', GetoptLong::REQUIRED_ARGUMENT], + ['-p', '--replace', GetoptLong::NO_ARGUMENT], + ['-i', '--id', GetoptLong::REQUIRED_ARGUMENT], + ['-s', '--summary', GetoptLong::REQUIRED_ARGUMENT], + ['-u', '--subject', GetoptLong::REQUIRED_ARGUMENT], + ['-c', '--clear', GetoptLong::REQUIRED_ARGUMENT], + ['-r', '--raise', GetoptLong::REQUIRED_ARGUMENT], + ['-d', '--detail', GetoptLong::REQUIRED_ARGUMENT], + ['-v', '--verbose', GetoptLong::NO_ARGUMENT] +).each do |opt,arg| + case opt + when '-h' + help = true + when '-p' + update.replace = true + when '-i' + message = Mauve::Proto::Alert.new + message.id = arg + update.alert << message + when '-o' + error "Can only specify one source" if update.source + update.source = arg + when '-v' + verbose += 1 + else + error "Must specify --id before message" unless message + case opt + when '-s' then message.summary = arg + when '-u' then message.subject = arg + when '-d' then message.detail = arg + when '-c' then message.clear_time = parse_time_spec(arg).to_i + when '-r' then message.raise_time = parse_time_spec(arg).to_i + else + error "Unknown option #{opt}" + end + end +end + + +# CAUTION! Kwality kode. +# +if help + # Open the file, stripping the shebang line + lines = File.open(__FILE__){|fh| fh.readlines}[2..-1] + + lines.each do |line| + line.chomp! + break if line.empty? + puts line[2..-1].to_s + end + + exit 0 +end + + +error "No alerts specified" unless !update.alert.empty? || update.replace + +update.transmission_id = rand(2**63) + +begin + Mauve::Sender.new(ARGV).send(update, verbose) +rescue ArgumentError => ae + error ae.message +rescue Protobuf::NotInitializedError => bad + error "Alert not initialized fully - you must supply an ID" +end + diff --git a/bin/mauveconsole b/bin/mauveconsole new file mode 100755 index 0000000..d375139 --- /dev/null +++ b/bin/mauveconsole @@ -0,0 +1,23 @@ +#!/usr/bin/ruby1.8 + +require 'pp' +pp $: + +Thread.abort_on_exception = true +require 'irb' +require 'thread' + +# hack for delving into test-generated configurations +class AlertAndNotificationLogic; Alerts = Queue.new; end + +require 'mauve/configuration' +include Mauve +#raise "must specify config file" unless ARGV.length > 0 +unless ARGV.length > 0 + STDERR.print("You must specify a configuration file as $arg[1]\n") + STDERR.print(" eg: ./mauve_starter ./bin/console ./test/local.conf\n") + exit 1 +end +Configuration.current = ConfigurationBuilder.load(ARGV.shift) +IRB.start + diff --git a/bin/mauveserver b/bin/mauveserver new file mode 100755 index 0000000..c576c85 --- /dev/null +++ b/bin/mauveserver @@ -0,0 +1,71 @@ +#! /usr/bin/ruby1.8 + +begin + eval "Proc.new { |a,&b| }" +rescue SyntaxError => no_blocks_with_procs + STDERR.print "mauveserver must have Ruby 1.8.7 or later, sorry (if you "+ + "try, you'll get a SyntaxError trying to load one of its libraries)\n" + exit 1 +end + +require 'mauve/configuration' +include Mauve + +configuration_file = ARGV[0] +if configuration_file.nil? + %w(/etc/mauvealert/mauveserver.conf mauveserver.conf).each do |configuration_file| + break if File.exists?(configuration_file) + end +end + +unless File.exists?(configuration_file) + if ARGV[0] + STDERR.print "Configuration file #{configuration_file} not found" + else + STDERR.print "Syntax: #{$0} <configuration filename>\n" + end + exit 1 +end + +Configuration.current = ConfigurationBuilder.load(configuration_file) + +class RestartSignalReceived < Exception; end +class TerminateSignalReceived < Exception; end + +%w(HUP).each do |sig| + trap("HUP") do + # this blows up if you do it twice in quick succession, but don't really + # care about that case as it's only for log rotation. + Configuration.current.logger.warn "#{sig} signal received. Restarting." + Configuration.current.server.stop + # + # Reload configuration + # + Configuration.current = ConfigurationBuilder.load(configuration_file) + Configuration.current.server.start + end +end + +%w(QUIT TERM INT).each do |sig| + trap(sig) do + Configuration.current.logger.warn "#{sig} signal received. Exiting." + Configuration.current.server.stop + exit 0 + end +end + +#begin + Mauve::Server.instance.run +#rescue SystemExit + # Woo! +# exit 0 +#rescue Exception => ex +# [ex.class.to_s, ex.to_s, ex.backtrace.join("\n")].each do |s| +# Configuration.current.logger.fatal s +# warn s +# end +# +# exit 1 +#end +# + diff --git a/bytemark_example_alerts.sh b/bytemark_example_alerts.sh new file mode 100755 index 0000000..e0d9205 --- /dev/null +++ b/bytemark_example_alerts.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +PRE="./mauve_starter.rb ./bin/mauvesend 127.0.0.1 " + +$PRE -o supportbot -i 173123 \ + -s "My server is not responding" \ + -d "<strong>From:</strong> John Smith <john@smith.name><br/> +<strong>To:</strong> support@support.bytemark.co.uk</br/> +<br/> +<pre>It has been several hours now since I have been able to contact my server +foo.bar.bytemark.co.uk. I am very upset that blah blah blah blah +and furthermore by business is under threat because £15.00 per month +is far too much blah blah blah</pre> +" + +$PRE -o networkmonitor -i 1 -u cr01.man.bytemark.co.uk \ + -s "cr01.man.bytemark.co.uk did not respond to pings" + +$PRE -o networkmonitor -i 2 -u cr01.thn.bytemark.co.uk \ + -s "cr02.man.bytemark.co.uk refused SSH connection" \ + -d "<pre>ssh: connect to host localhost port 1212: Connection refused</pre>" + +$PRE -o vmhs -i 12346 -u ventham.bytemark.co.uk \ + -s "ventham.bytemark.co.uk heartbeat not received" -r +5 + +$PRE -o vmhs -i 12345 -u partridge.bytemark.co.uk \ + -s "partridge.bytemark.co.uk heartbeat not received" -r +2 + +$PRE -o vmhs -i 12347 -u eider.bytemark.co.uk \ + -s "eider.bytemark.co.uk heartbeat not received" -r +2 + +$PRE -o thresholds -i 1 -u bl1-1.bytemark.co.uk \ + -s "bl1-1 exceeded 10Mb/s on bond0" \ + -d "<h1>Hello there</h1><p>Here is a paragraph</p><p>And another one</p>" + +$PRE -o thresholds -i 2 -u bl1-11.bytemark.co.uk \ + -s "bl1-11 has less than 1GB free memory" + +$PRE -o thresholds -i 3 -u rom.sh.bytemark.co.uk \ + -s "rom.sh.bytemark.co.uk has 1/2 discs available in /dev/md0" \ + -d "<pre>Personalities : +unused devices: <none> +</pre> +" + diff --git a/bytemark_policy.txt b/bytemark_policy.txt new file mode 100644 index 0000000..22378ae --- /dev/null +++ b/bytemark_policy.txt @@ -0,0 +1,125 @@ +server { + ip "0.0.0.0" + port 32741 + log_file STDOUT + log_level 0 + database "sqlite3:///tmp/mauve_test.db" + transmission_id_expire_time 600 + + web_interface { + port 1288 + } +} + +notification_method("xmpp") { + jid "mauveserv@chat.bytemark.co.uk" + password "foo" +} + +notification_method("email") { + # add in SMTP server, username, password etc. + # default to sending through localhost + from "matthew@bytemark.co.uk" + server "bytemail.bytemark.co.uk" + subject_prefix "[Bytemark alerts] " + #deliver_to_file "/tmp/alerts.txt" +} + +notification_method("sms") { + provider "AQL" + + username "bytemark" + password "foo" + from "01904890890" + max_messages_per_alert 3 +} + +person("office") { + #all { xmpp "office@conference.chat.bytemark.co.uk" } + all { email "matthew@bytemark.co.uk" } +} + +person("mbloch") { + urgent { sms("x") } + normal { xmpp("mbloch@chat.bytemark.co.uk") || email("matthew@bytemark.co.uk") } + low { email("matthew@bytemark.co.uk") } +} + +person("ptaphouse") { + urgent { sms("x") } + normal { xmpp("ptaphouse@chat.bytemark.co.uk") || email("pete@bytemark.co.uk") } + low { email("pete@bytemark.co.uk") } +} + +person("chris") { +} + +alert_group { + includes { + source == "supportbot" || source == "managed-monitor" + } + + acknowledgement_time 90.minutes + + level URGENT + + notify("office") { + every 30.minutes + } + + notify("mbloch") { + every 60.minutes + hours_in_day(0..8, 17..23) + unacknowledged(120.minutes) + } +} + +alert_group { + includes { source == "network-crit" } + level URGENT + acknowledgement_time 90.minutes + + notify("office") { every 15.minutes } + notify("ptaphouse") { every 30.minutes } + notify("mbloch") { + every 30.minutes + unacknowledged(95.minutes) + } +} + +alert_group { + includes { source == "networkmonitor" } + acknowledgement_time 7.days + + notify("office") { every 120.minutes } +} + +alert_group { + includes { source == "disks" } + acknowledgement_time 1.day + + notify("chris") { every 4.hours } + notify("mbloch","ptaphouse") { + every 12.hours + unacknowledged 24.hours + } + +} + +alert_group { + includes { source == "pxefs" } + level URGENT + + notify("office") { every 1.hour } + notify("mbloch") { + every 3.hours + unacknowledged 24.hours + } +} + +alert_group("default") { + level LOW + + notify("office") { every 3.hours } +} + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..f65d084 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,11 @@ +mauvealert (3.0.0) stable; urgency=low + + * New version. + + -- Patrick J Cherry <patrick@bytemark.co.uk> Wed, 13 Apr 2011 17:02:22 +0100 + +mauvealert (0.1-1) stable; urgency=low + + * First release. + + -- Patrick J Cherry <patrick@bytemark.co.uk> Wed, 28 Oct 2009 10:34:17 +0000 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7ed6ff8 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +5 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..cd2bcd8 --- /dev/null +++ b/debian/control @@ -0,0 +1,57 @@ +Source: mauvealert +Maintainer: Matthew Bloch <matthew@bytemark.co.uk> +Uploaders: Patrick J Cherry <patrick@bytemark.co.uk> +Section: ruby +Priority: optional +Build-Depends: ruby1.8, debhelper, cdbs, rdoc +Standards-Version: 3.8.0 + +Package: mauvealert-client +Architecture: all +Depends: ruby1.8, + mauvealert-common, + ${misc:Depends} +Description: Mauve network alert system -- client + mauve is a network alert system for system and network administrators. You + can use it to quickly set up ad-hoc monitoring for a variety of services, and + to confidently replace all scripts that might otherwise send floods of emails + or text messages. + . + This is the client package. + +Package: mauvealert-server +Architecture: all +Depends: mauvealert-common, + ruby1.8, + libhaml-ruby1.8, + liblog4r-ruby1.8, + libredcloth-ruby1.8, + librmail-ruby1.8, + libsinatra-ruby1.8, + libuuidtools-ruby1.8, + libwebrick-ruby1.8, + libdm-sqlite-adapter-ruby1.8, + thin1.8, + libxmpp4r-ruby1.8, + ${misc:Depends} +Suggests: mauvealert-client +Description: Mauve network alert system -- server + mauve is a network alert system for system and network administrators. You can + use it to quickly set up ad-hoc monitoring for a variety of services, and to + confidently replace all scripts that might otherwise send floods of emails or + text messages. + . + This is the server package. + +Package: mauvealert-common +Architecture: all +Depends: ruby1.8, ruby-protobuf, ${misc:Depends} +Suggests: mauvealert-client +Description: Mauve network alert system -- common libraries + mauve is a network alert system for system and network administrators. You can + use it to quickly set up ad-hoc monitoring for a variety of services, and to + confidently replace all scripts that might otherwise send floods of emails or + text messages. + . + This is the package of libraries in common with both the client and server. + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..e0fe391 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,19 @@ +This is Mauve Alert, Bytemark's networked alert system. + +Copyright © 2009 Bytemark Computer Consulting Ltd <support@support.bytemark.co.uk> + +Licence: + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +On Debian GNU/Linux systems, the complete text of the GNU General Public +License can be found in ‘/usr/share/common-licenses/GPL’ or in the dpkg +source as the file ‘COPYING’ diff --git a/debian/mauvealert-client.install b/debian/mauvealert-client.install new file mode 100644 index 0000000..9a28237 --- /dev/null +++ b/debian/mauvealert-client.install @@ -0,0 +1,3 @@ +bin/mauveclient usr/bin/ +lib/mauve/sender.rb usr/lib/ruby/1.8/mauve/ + diff --git a/debian/mauvealert-common.install b/debian/mauvealert-common.install new file mode 100644 index 0000000..024d922 --- /dev/null +++ b/debian/mauvealert-common.install @@ -0,0 +1,3 @@ +mauve.proto usr/lib/mauvealert/ +lib/mauve/proto.rb usr/lib/ruby/1.8/mauve/ + diff --git a/debian/mauvealert-server.install b/debian/mauvealert-server.install new file mode 100644 index 0000000..630990d --- /dev/null +++ b/debian/mauvealert-server.install @@ -0,0 +1,31 @@ +bin/mauveserver usr/bin/ +lib/mauve/notifiers.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/notification.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/alert.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/time.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/alert_changed.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/source_list.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/alert_group.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/web_sinatra.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/configuration.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/calendar_interface.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/auth_bytemark.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/people_list.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/person.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/web_interface.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/timers.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/server.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/datamapper.rb usr/lib/ruby/1.8/mauve/ +lib/mauve/notifiers/sms_aql.rb usr/lib/ruby/1.8/mauve/notifiers/ +lib/mauve/notifiers/xmpp-smack.rb usr/lib/ruby/1.8/mauve/notifiers/ +lib/mauve/notifiers/debug.rb usr/lib/ruby/1.8/mauve/notifiers/ +lib/mauve/notifiers/email.rb usr/lib/ruby/1.8/mauve/notifiers/ +lib/mauve/notifiers/xmpp.rb usr/lib/ruby/1.8/mauve/notifiers/ +lib/mauve/notifiers/sms_default.rb usr/lib/ruby/1.8/mauve/notifiers/ +lib/dm-sqlite-adapter-with-mutex.rb usr/lib/ruby/1.8/ +lib/sinatra-partials.rb usr/lib/ruby/1.8/ +lib/object_builder.rb usr/lib/ruby/1.8/ +lib/rack-flash.rb usr/lib/ruby/1.8/ +static usr/share/mauvealert +views usr/share/mauvealert + diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..1a62ae6 --- /dev/null +++ b/debian/rules @@ -0,0 +1,9 @@ +#!/usr/bin/make -f +# + +include /usr/share/cdbs/1/rules/debhelper.mk +# include /usr/share/ruby-pkg-tools/1/class/ruby-setup-rb.mk + +# in which directory to build +# BUILDDIR = debian/tmp + diff --git a/lib/canary.rb b/lib/canary.rb new file mode 100644 index 0000000..afe3a16 --- /dev/null +++ b/lib/canary.rb @@ -0,0 +1,55 @@ +# encoding: UTF-8 +require 'logger' + +# A little canary class to make sure that threads are are overloaded. +class Canary + + # Accessor. + attr_reader :sleep_time + + # Accessor. + attr_reader :threshold + + # Default constructor. + def initialize (st=1, log=nil) + if Float != st.class and Fixnum != st.class + raise ArgumentError.new( + "Expected either Fixnum or Float for time to sleep, got #{st.class}.") + end + @sleep_time = st + @threshold = (0.05 * @sleep_time) + @sleep_time + @logger = log + end + + # Runs the check. + def run + loop do + self.do_test() + end + end + + def do_test + time_start = Time.now + sleep(@sleep_time) + time_end = Time.now + time_elapsed = (time_end - time_start).abs + if @threshold < time_elapsed + @logger.fatal("Time elapsed is #{time_elapsed} > #{@threshold} therefore Canary is dead.") + return false + else + @logger.debug("Time elapsed is #{time_elapsed} < #{@threshold} therefore Canary is alive.") + return true + end + end + + # Starts a canary in a thread. + def self.start (st=1, log=nil) + #Thread.abort_on_exception = true + thr = Thread.new() do + Thread.current[:name] = "Canary Thread" + twiti = Canary.new(st, log) + twiti.run() + end + end + +end diff --git a/lib/dm-sqlite-adapter-with-mutex.rb b/lib/dm-sqlite-adapter-with-mutex.rb new file mode 100644 index 0000000..2842c5e --- /dev/null +++ b/lib/dm-sqlite-adapter-with-mutex.rb @@ -0,0 +1,28 @@ +# +# Add a mutex so that we can avoid the 'database is locked' Sqlite3Error +# exception. +# +require 'dm-sqlite-adapter' +require 'monitor' + +ADAPTER = DataMapper::Adapters::SqliteAdapter + +# better way to alias a private method? (other than "don't"? :) ) +ADAPTER.__send__(:alias_method, :initialize_old, :initialize) +ADAPTER.__send__(:undef_method, :initialize) +ADAPTER.__send__(:alias_method, :with_connection_old, :with_connection) +ADAPTER.__send__(:undef_method, :with_connection) + +class ADAPTER + + def initialize(*a) + extend(MonitorMixin) + initialize_old(*a) + end + + private + + def with_connection(&block) + synchronize { with_connection_old(&block) } + end +end diff --git a/lib/mauve/alert.rb b/lib/mauve/alert.rb new file mode 100644 index 0000000..374762d --- /dev/null +++ b/lib/mauve/alert.rb @@ -0,0 +1,399 @@ +require 'mauve/proto' +require 'mauve/alert_changed' +require 'mauve/datamapper' + + +module Mauve + class AlertEarliestDate + + include DataMapper::Resource + + property :id, Serial + property :alert_id, Integer + property :earliest, DateTime + belongs_to :alert, :model => "Alert" + + # 1) Shame we can't get this called automatically from DataMapper.auto_upgrade! + # + # 2) Can't use a neater per-connection TEMPORARY VIEW because the pooling + # function causes the connection to get dropped occasionally, and we can't + # hook the reconnect function (that I know of). + # + # http://www.mail-archive.com/datamapper@googlegroups.com/msg02314.html + # + def self.create_view! + the_distant_future = MauveTime.now + 86400000 # it is the year 2000 - the humans are dead + ["BEGIN TRANSACTION", + "DROP VIEW IF EXISTS mauve_alert_earliest_dates", + "CREATE VIEW + mauve_alert_earliest_dates + AS + SELECT + id AS alert_id, + NULLIF( + MIN( + IFNULL(will_clear_at, '#{the_distant_future}'), + IFNULL(will_raise_at, '#{the_distant_future}'), + IFNULL(will_unacknowledge_at, '#{the_distant_future}') + ), + '#{the_distant_future}' + ) AS earliest + FROM mauve_alerts + WHERE + will_clear_at IS NOT NULL OR + will_raise_at IS NOT NULL OR + will_unacknowledge_at IS NOT NULL + ", + "END TRANSACTION"].each do |statement| + repository(:default).adapter.execute(statement.gsub(/\s+/, " ")) + end + end + + end + + class Alert + def bytesize; 99; end + def size; 99; end + + include DataMapper::Resource + + property :id, Serial + property :alert_id, String, :required => true, :unique_index => :alert_index, :length=>256 + property :source, String, :required => true, :unique_index => :alert_index, :length=>512 + property :subject, String, :length=>512, :length=>512 + property :summary, String, :length=>1024 + property :detail, Text, :length=>65535 + property :importance, Integer, :default => 50 + + property :raised_at, DateTime + property :cleared_at, DateTime + property :updated_at, DateTime + property :acknowledged_at, DateTime + property :acknowledged_by, String + property :update_type, String + + property :will_clear_at, DateTime + property :will_raise_at, DateTime + property :will_unacknowledge_at, DateTime +# property :will_unacknowledge_after, Integer + + has n, :changes, :model => AlertChanged + has 1, :alert_earliest_date + + validates_with_method :check_dates + + def to_s + "#<Alert:#{id} #{alert_id} from #{source} update_type #{update_type}>" + end + + def check_dates + bad_dates = self.attributes.find_all do |key, value| + value.is_a?(DateTime) and not (DateTime.new(2000,1,1,0,0,0)..DateTime.new(2020,1,1,0,0,0)).include?(value) + end + + if bad_dates.empty? + true + else + [ false, "The dates "+bad_dates.collect{|k,v| "#{v.to_s} (#{k})"}.join(", ")+" are invalid." ] + end + end + + default_scope(:default).update(:order => [:source, :importance]) + + def logger + Log4r::Logger.new(self.class.to_s) + end + + def time_relative(secs) + secs = secs.to_i.abs + case secs + when 0..59 then "just now" + when 60..3599 then "#{secs/60}m ago" + when 3600..86399 then "#{secs/3600}h ago" + else + days = secs/86400 + days == 1 ? "yesterday" : "#{secs/86400} days ago" + end + end + + def summary_one_line + subject ? "#{subject} #{summary}" : "#{source} #{summary}" + end + + def summary_two_lines + msg = "" + msg += "from #{source} " if source != subject + if cleared_at + msg += "cleared #{time_relative(MauveTime.now - cleared_at.to_time)}" + elsif acknowledged_at + msg += "acknowledged #{time_relative(MauveTime.now - acknowledged_at.to_time)} by #{acknowledged_by}" + else + msg += "raised #{time_relative(MauveTime.now - raised_at.to_time)}" + end + [summary_one_line, msg] + end + + # Returns a better array with information about the alert. + # + # @return [Array] An array of three elements: status, message, source. + def summary_three_lines + status = String.new + if "cleared" == update_type + status += "CLEARED #{time_relative(MauveTime.now - cleared_at.to_time)}" + elsif "acknowledged" == update_type + status += "ACKNOWLEDGED #{time_relative(MauveTime.now - acknowledged_at.to_time)} by #{acknowledged_by}" + elsif "changed" == update_type + status += "CHANGED #{time_relative(MauveTime.now - updated_at.to_time)}" + else + status += "RAISED #{time_relative(MauveTime.now - raised_at.to_time)}" + end + src = (source != subject)? "from #{source}" : nil + return [status, summary_one_line, src] +=begin + status = String.new + if cleared_at + status += "CLEARED #{time_relative(MauveTime.now - cleared_at.to_time)}" + elsif acknowledged_at + status += "ACKNOWLEDGED #{time_relative(MauveTime.now - acknowledged_at.to_time)} by #{acknowledged_by}" + else + status += "RAISED #{time_relative(MauveTime.now - raised_at.to_time)}" + end + src = (source != subject)? "from #{source}" : nil + return [status, summary_one_line, src] +=end + end + + + def alert_group + AlertGroup.matches(self)[0] + end + + def subject + attribute_get(:subject) || source + end + + def subject=(subject); set_changed_if_different(:subject, subject); end + def summary=(summary); set_changed_if_different(:summary, summary); end + def detail=(detail); set_changed_if_different(:detail, detail); end + + protected + def set_changed_if_different(attribute, value) + return if self.__send__(attribute) == value + self.update_type ||= :changed + attribute_set(attribute.to_sym, value) + end + + public + + def acknowledge!(person) + self.acknowledged_by = person.username + self.acknowledged_at = MauveTime.now + self.update_type = :acknowledged + self.will_unacknowledge_at = MauveTime.parse(acknowledged_at.to_s) + + logger.error("Couldn't save #{self}") unless save + AlertGroup.notify([self]) + end + + def unacknowledge! + self.acknowledged_by = nil + self.acknowledged_at = nil + self.update_type = :raised + logger.error("Couldn't save #{self}") unless save + AlertGroup.notify([self]) + end + + def raise! + already_raised = raised? && !acknowledged? + self.acknowledged_by = nil + self.acknowledged_at = nil + self.will_unacknowledge_at = nil + self.will_raise_at = nil + self.update_type = :raised + self.raised_at = MauveTime.now + self.cleared_at = nil + logger.error("Couldn't save #{self}") unless save + AlertGroup.notify([self]) unless already_raised + end + + def clear!(notify=true) + already_cleared = cleared? + self.cleared_at = MauveTime.now + self.will_clear_at = nil + self.update_type = :cleared + logger.error("Couldn't save #{self}") unless save + AlertGroup.notify([self]) unless !notify || already_cleared + end + + # Returns the time at which a timer loop should call poll_event to either + # raise, clear or unacknowldge this event. + # + def due_at + o = [will_clear_at, will_raise_at, will_unacknowledge_at].compact.sort[0] + o ? o.to_time : nil + end + + def poll + raise! if will_unacknowledge_at && will_unacknowledge_at.to_time <= MauveTime.now || + will_raise_at && will_raise_at.to_time <= MauveTime.now + clear! if will_clear_at && will_clear_at.to_time <= MauveTime.now + end + + def raised? + !raised_at.nil? && cleared_at.nil? + end + + def acknowledged? + !acknowledged_at.nil? + end + + def cleared? + new? || !cleared_at.nil? + end + + class << self + + def all_current + all(:cleared_at => nil) + end + + # Returns the next Alert that will have a timed action due on it, or nil + # if none are pending. + # + def find_next_with_event + earliest_alert = AlertEarliestDate.first(:order => [:earliest]) + earliest_alert ? earliest_alert.alert : nil + end + + def all_overdue(at = MauveTime.now) + AlertEarliestDate.all(:earliest.lt => at, :order => [:earliest]).collect do |earliest_alert| + earliest_alert ? earliest_alert.alert : nil + end + end + + # Receive an AlertUpdate buffer from the wire. + # + def receive_update(update, reception_time = MauveTime.now) + update = Proto::AlertUpdate.parse_from_string(update) unless + update.kind_of?(Proto::AlertUpdate) + alerts_updated = [] + + logger.debug("Alert update received from wire: #{update.inspect.split.join(", ")}") + + # + # Transmission time helps us determine any time offset + # + if update.transmission_time + transmission_time = MauveTime.at(update.transmission_time) + else + transmission_time = reception_time + end + + time_offset = (reception_time - transmission_time).round + logger.debug("Update received from a host #{time_offset}s behind") if time_offset.abs > 0 + + # Update each alert supplied + # + update.alert.each do |alert| + # Infer some actions from our pure data structure (hmm, wonder if + # this belongs in our protobuf-derived class? + # + raise_time = alert.raise_time == 0 ? nil : MauveTime.at(alert.raise_time + time_offset) + clear_time = alert.clear_time == 0 ? nil : MauveTime.at(alert.clear_time + time_offset) + + logger.debug("received at #{reception_time}, transmitted at #{transmission_time}, raised at #{raise_time}, clear at #{clear_time}") + + do_clear = clear_time && clear_time <= reception_time + do_raise = raise_time && raise_time <= reception_time + + alert_db = first(:alert_id => alert.id, :source => update.source) || + new(:alert_id => alert.id, :source => update.source) + + pre_raised = alert_db.raised? + pre_cleared = alert_db.cleared? + pre_acknowledged = alert_db.acknowledged? + + alert_db.update_type = nil + + ## + # + # Allow a 15s offset in timings. + # + if raise_time + if raise_time <= (reception_time + 15) + alert_db.raised_at = raise_time + else + alert_db.will_raise_at = raise_time + end + end + + if clear_time + if clear_time <= (reception_time + 15) + alert_db.cleared_at = clear_time + else + alert_db.will_clear_at = clear_time + end + end + + # re-raise + if alert_db.cleared_at && alert_db.raised_at && alert_db.cleared_at < alert_db.raised_at + alert_db.cleared_at = nil + end + + if pre_cleared && alert_db.raised? + alert_db.update_type = :raised + elsif pre_raised && alert_db.cleared? + alert_db.update_type = :cleared + end + + # Changing any of these attributes causes the alert to be sent back + # out to the notification system with an update_type of :changed. + # + alert_db.subject = alert.subject if alert.subject && !alert.subject.empty? + alert_db.summary = alert.summary if alert.summary && !alert.summary.empty? + alert_db.detail = alert.detail if alert.detail && !alert.detail.empty? + + # These updates happen but do not sent the alert back to the + # notification system. + # + alert_db.importance = alert.importance if alert.importance != 0 + + # FIXME: this logic ought to be clearer as it may get more complicated + # + if alert_db.update_type + if alert_db.update_type.to_sym == :changed && !alert_db.raised? + # do nothing + else + alerts_updated << alert_db + end + else + alert_db.update_type = :changed + end + + logger.error "Couldn't save update #{alert} because of #{alert_db.errors}" unless alert_db.save + end + + # If this is a complete replacement update, find the other alerts + # from this source and clear them. + # + if update.replace + alert_ids_mentioned = update.alert.map { |alert| alert.id } + logger.debug "Replacing all alerts from #{update.source} except "+alert_ids_mentioned.join(",") + all(:source => update.source, + :alert_id.not => alert_ids_mentioned, + :cleared_at => nil + ).each do |alert_db| + logger.debug "Replace: clearing #{alert_db.id}" + alert_db.clear!(false) + alerts_updated << alert_db + end + end + + AlertGroup.notify(alerts_updated) + end + + def logger + Log4r::Logger.new("Mauve::Alert") + end + end + end +end diff --git a/lib/mauve/alert_changed.rb b/lib/mauve/alert_changed.rb new file mode 100644 index 0000000..4668fd7 --- /dev/null +++ b/lib/mauve/alert_changed.rb @@ -0,0 +1,144 @@ +# encoding: UTF-8 +require 'mauve/datamapper' +require 'log4r' + +module Mauve + class AlertChanged + include DataMapper::Resource + + # so .first always returns the most recent update + default_scope(:default).update(:order => [:at.desc, :id.desc]) + + property :id, Serial + property :alert_id, Integer, :required => true + property :person, String, :required => true + property :at, DateTime, :required => true + property :was_relevant, Boolean, :required => true, :default => true + property :level, String, :required => true + property :update_type, String, :required => true + property :remind_at, DateTime + property :updated_at, DateTime + + + def to_s + "#<AlertChanged:#{id} of #{alert_id} for #{person} update_type #{update_type}>" + end + + belongs_to :alert + + # There is a bug there. You could have two reminders for the same + # person if that person has two different notify clauses. + # + # See the test cases test_Bug_reminders_get_trashed() in ./test/ + after :create do + old_changed = AlertChanged.first( + :alert_id => alert_id, + :person => person, + :id.not => id, + :remind_at.not => nil + ) + if old_changed + if !old_changed.update(:remind_at => nil) + logger.error "Couldn't save #{old_changed}, will get duplicate reminders" + end + end + end + + def was_relevant=(value) + attribute_set(:was_relevant, value) + end + + def logger + Log4r::Logger.new self.class.to_s + end + +## def initialize +# logger = Log4r::Logger.new self.class.to_s +# end + + ## Checks to see if a raise was send to the person. + # + # @TODO: Recurence is broken in ruby, change this so that it does not + # use it. + # + # @author Matthew Bloch + # @return [Boolean] true if it was relevant, false otherwise. + def was_relevant_when_raised? + if :acknowledged == update_type.to_sym and true == was_relevant + return true + end + return was_relevant if update_type.to_sym == :raised + previous = AlertChanged.first(:id.lt => id, + :alert_id => alert_id, + :person => person) + if previous + previous.was_relevant_when_raised? + else + # a bug, but hardly inconceivable :) + logger.warn("Could not see that #{alert} was raised with #{person} "+ + "but further updates exist (e.g. #{self}) "+ + "- you may see spurious notifications as a result") + true + end + end + + # Sends a reminder about this alert state change, or forget about it if + # the alert has been acknowledged + # + def remind + logger.debug "Reminding someone about #{self.inspect}" + + alert_group = AlertGroup.matches(alert)[0] + + if !alert_group || alert.acknowledged? + logger.debug((alert_group ? + "Alert already acknowledged" : + "No alert group matches any more" + ) + ", no reminder due" + ) + self.remind_at = nil + save + else + saved = false + + alert_group.notifications.each do |notification| + notification.people.each do |person| + if person.username == self.person + person.remind(alert, level) + self.remind_at = notification.remind_at_next(alert) + save + saved = true + end + end + end + + if !saved + logger.warn("#{self.inspect} did not match any people, maybe configuration has changed but I'm going to delete this and not try to remind anyone again") + destroy! + end + end + end + + def due_at # mimic interface from Alert + remind_at ? remind_at.to_time : nil + end + + def poll # mimic interface from Alert + remind if remind_at.to_time <= MauveTime.now + end + + class << self + def next_reminder + first(:remind_at.not => nil, :order => [:remind_at]) + end + + def find_next_with_event # mimic interface from Alert + next_reminder + end + + def all_overdue(at = MauveTime.now) + all(:remind_at.not => nil, :remind_at.lt => at, :order => [:remind_at]).to_a + end + end + end +end diff --git a/lib/mauve/alert_group.rb b/lib/mauve/alert_group.rb new file mode 100644 index 0000000..d8156fa --- /dev/null +++ b/lib/mauve/alert_group.rb @@ -0,0 +1,104 @@ +# encoding: UTF-8 +require 'mauve/alert' +require 'log4r' + +module Mauve + class AlertGroup < Struct.new(:name, :includes, :acknowledgement_time, :level, :notifications) + def to_s + "#<AlertGroup:#{name} (level #{level})>" + end + + class << self + def matches(alert) + all.select { |alert_group| alert_group.matches_alert?(alert) } + end + + # If there is any significant change to a set of alerts, the Alert + # class sends the list here so that appropriate action can be taken + # for each one. We scan the list of alert groups to find out which + # alerts match which groups, then send a notification to each group + # object in turn. + # + def notify(alerts) + alerts.each do |alert| + groups = matches(alert) + + # + # Make sure we've got a matching group + # + logger.warn "no groups found for #{alert.id}" if groups.empty? + + # + # Notify each group. + # + groups.each do |grp| + logger.info("notifying group #{groups[0]} of AlertID.#{alert.id}.") + grp.notify(alert) + end + end + end + + def logger + Log4r::Logger.new self.to_s + end + + def all + Configuration.current.alert_groups + end + + # Find all alerts that match + # + # @deprecated Buggy method, use Alert.get_all(). + # + # This method returns all the alerts in all the alert_groups. Only + # the first one should be returned thus making this useless. If you want + # a list of all the alerts matching a level, use Alert.get_all(). + # + def all_alerts_by_level(level) + Configuration.current.alert_groups.map do |alert_group| + alert_group.level == level ? alert_group.current_alerts : [] + end.flatten.uniq + end + + end + + def initialize(name) + self.name = name + self.level = :normal + self.includes = Proc.new { true } + end + + # The list of current raised alerts in this group. + # + def current_alerts + Alert.all(:cleared_at => nil, :raised_at.not => nil).select { |a| matches_alert?(a) } + end + + # Decides whether a given alert belongs in this group according to its + # includes { } clause + # + # @param [Alert] alert An alert to test for belongness to group. + # @return [Boolean] Success or failure. + def matches_alert?(alert) + result = alert.instance_eval(&self.includes) + if true == result or + true == result.instance_of?(MatchData) + return true + end + return false + end + + def logger ; self.class.logger ; end + + # Signals that a given alert (which is assumed to belong in this group) + # has undergone a significant change. We resend this to every notify list. + # + def notify(alert) + notifications.each do |notification| + notification.alert_changed(alert) + end + end + + end + +end diff --git a/lib/mauve/auth_bytemark.rb b/lib/mauve/auth_bytemark.rb new file mode 100644 index 0000000..7419d10 --- /dev/null +++ b/lib/mauve/auth_bytemark.rb @@ -0,0 +1,47 @@ +# encoding: UTF-8 +require 'sha1' +require 'xmlrpc/client' +require 'timeout' + +class AuthSourceBytemark + + def initialize (srv='auth.bytemark.co.uk', port=443) + raise ArgumentError.new("Server must be a String, not a #{srv.class}") if String != srv.class + raise ArgumentError.new("Port must be a Fixnum, not a #{port.class}") if Fixnum != port.class + @srv = srv + @port = port + @timeout = 7 + end + + ## Not really needed. + def ping () + begin + MauveTimeout.timeout(@timeout) do + s = TCPSocket.open(@srv, @port) + s.close() + return true + end + rescue MauveTimeout::Error => ex + return false + rescue => ex + return false + end + return false + end + + def authenticate(login, password) + raise ArgumentError.new("Login must be a string, not a #{login.class}") if String != login.class + raise ArgumentError.new("Password must be a string, not a #{password.class}") if String != password.class + raise ArgumentError.new("Login or/and password is/are empty.") if login.empty? || password.empty? + client = XMLRPC::Client.new(@srv,"/",@port,nil,nil,nil,nil,true,@timeout).proxy("bytemark.auth") + begin + challenge = client.getChallengeForUser(login) + response = Digest::SHA1.new.update(challenge).update(password).hexdigest + client.login(login, response) + rescue XMLRPC::FaultException => fault + return "Fault code is #{fault.faultCode} stating #{fault.faultString}" + end + return true + end + +end diff --git a/lib/mauve/calendar_interface.rb b/lib/mauve/calendar_interface.rb new file mode 100644 index 0000000..08cfab3 --- /dev/null +++ b/lib/mauve/calendar_interface.rb @@ -0,0 +1,146 @@ +# encoding: UTF-8 +require 'log4r' +require 'net/http' +require 'net/https' +require 'uri' + +module Mauve + + # Interface to the Bytemark calendar. + # + # Nota Bene that this does not include a chaching mechanism. Some caching + # is implemented in the Person object. + # + # @see Mauve::Person + # @author yann Golanski. + class CalendarInterface + + TIMEOUT = 7 + + public + + # Gets a list of ssologin on support. + # + # Class method. + # + # @param [String] url A Calendar API url. + # @return [Array] A list of all the username on support. + def self.get_users_on_support(url) + result = get_URL(url) + logger = Log4r::Logger.new "mauve::CalendarInterface" + logger.debug("Cheching who is on support: #{result}") + return result + end + + # Check to see if the user is on support. + # + # Class method. + # + # @param [String] url A Calendar API url. + # @param [String] usr User single sign on. + # @return [Boolean] True if on support, false otherwise. + def self.is_user_on_support?(url, usr) + logger = Log4r::Logger.new "mauve::CalendarInterface" + list = get_URL(url) + if true == list.include?("nobody") + logger.error("Nobody is on support thus alerts are ignored.") + return false + end + result = list.include?(usr) + logger.debug("Cheching if #{usr} is on support: #{result}") + return result + end + + # Check to see if the user is on holiday. + # + # Class method. + # + # @param [String] url A Calendar API url. + # @param [String] usr User single sign on. + # @return [Boolean] True if on holiday, false otherwise. + def self.is_user_on_holiday?(url, usr) + list = get_URL(url) + return false if true == list.nil? or true == list.empty? + pattern = /[\d]{4}-[\d]{2}-[\d]{2}\s[\d]{2}:[\d]{2}:[\d]{2}/ + result = (list[0].match(pattern))? true : false + logger = Log4r::Logger.new "mauve::CalendarInterface" + logger.debug("Cheching if #{usr} is on holiday: #{result}") + return result + end + + + private + + # Gets a URL from the wide web. + # + # Must NOT crash Mauveserver. + # + # Class method. + # + # @TODO: boot this in its own class since list of ips will need it too. + # + # @param [String] url A Calendar API url. + # @retur [Array] An array of strings, each newline creates an new item. + def self.get_URL (uri_str, limit = 11) + + logger = Log4r::Logger.new "mauve::CalendarInterface" + + if 0 == limit + logger.warn("HTTP redirect deeper than 11 on #{uri_str}.") + return false + end + + begin + uri_str = 'http://' + uri_str unless uri_str.match(uri_str) + url = URI.parse(uri_str) + http = Net::HTTP.new(url.host, url.port) + http.open_timeout = TIMEOUT + http.read_timeout = TIMEOUT + if (url.scheme == "https") + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + response = nil + if nil == url.query + response = http.start { http.get(url.path) } + else + response = http.start { http.get("#{url.path}?#{url.query}") } + end + case response + when Net::HTTPRedirection + then + newURL = response['location'].match(/^http/)? + response['Location']: + uri_str+response['Location'] + self.getURL(newURL, limit-1) + else + return response.body.split("\n") + end + rescue Errno::EHOSTUNREACH => ex + logger.warn("no route to host.") + return Array.new + rescue MauveTimeout::Error => ex + logger.warn("time out reached.") + return Array.new + rescue ArgumentError => ex + unless uri_str.match(/\/$/) + logger.debug("Potential missing '/' at the end of hostname #{uri_str}") + uri_str += "/" + retry + else + str = "ArgumentError raise: #{ex.message} #{ex.backtrace.join("\n")}" + logger.fatal(str) + return Array.new + #raise ex + end + rescue => ex + str = "ArgumentError raise: #{ex.message} #{ex.backtrace.join("\n")}" + logger.fatal(str) + return Array.new + #raise ex + end + + end + end + +end diff --git a/lib/mauve/configuration.rb b/lib/mauve/configuration.rb new file mode 100644 index 0000000..b11e1b5 --- /dev/null +++ b/lib/mauve/configuration.rb @@ -0,0 +1,484 @@ +# encoding: UTF-8 +require 'object_builder' +require 'mauve/server' +require 'mauve/web_interface' +require 'mauve/person' +require 'mauve/notification' +require 'mauve/alert_group' +require 'mauve/people_list' +require 'mauve/source_list' + +# Seconds, minutes, hours, days, and weeks... More than that, we +# really should not need it. +class Integer + def seconds; self; end + def minutes; self*60; end + def hours; self*3600; end + def days; self*86400; end + def weeks; self*604800; end + alias_method :day, :days + alias_method :hour, :hours + alias_method :minute, :minutes + alias_method :week, :weeks +end + +module Mauve + + ## Configuration object for Mauve. + # + # + # @TODO Write some more documentation. This is woefully inadequate. + # + # + # == How to add a new class to the configuration? + # + # - Add a method to ConfigurationBuilder such that your new object + # maybe created. Call it created_NEW_OBJ. + # + # - Create a new class inheriting from ObjectBuilder with at least a + # builder_setup() method. This should create the new object you want. + # + # - Define attributes for the new class are defined as "is_attribute". + # + # - Methods for the new class are defined as methods or missing_method + # depending on what one wishes to do. Remember to define a method + # with "instance_eval(&block)" if you want to call said block within + # the new class. + # + # - Add a "is_builder "<name>", BuilderCLASS" clause in the + # ConfigurationBuilder class. + # + # That should be it. + # + # @author Matthew Bloch, Yann Golanski + class Configuration + + class << self + attr_accessor :current + end + + attr_accessor :server + attr_accessor :last_alert_group + attr_reader :notification_methods + attr_reader :people + attr_reader :alert_groups + attr_reader :people_lists + attr_reader :source_lists + attr_reader :logger + + def initialize + @notification_methods = {} + @people = {} + @people_lists = {} + @alert_groups = [] + @source_lists = SourceList.new() + @logger = Log4r::Logger.new("Mauve") + + end + + def close + server.close + end + + end + + class LoggerOutputterBuilder < ObjectBuilder + + def builder_setup(outputter) + @outputter = outputter.capitalize+"Outputter" + + begin + Log4r.const_get(@outputter) + rescue + require "log4r/outputter/#{@outputter.downcase}" + end + + @args = {} + end + + def result + @result ||= Log4r.const_get(@outputter).new("Mauve", @args) + end + + def format(f) + result.formatter = Log4r::PatternFormatter.new(:pattern => f) + end + + def method_missing(name, value=nil) + if value.nil? + result.send(name.to_sym) + else + @args[name.to_sym] = value + end + end + + end + + class LoggerBuilder < ObjectBuilder + + is_builder "outputter", LoggerOutputterBuilder + + def builder_setup + logger = Log4r::Logger.new("Mauve") + @default_format = nil + @default_level = Log4r::RootLogger.instance.level + end + + def result + @result = Log4r::Logger['Mauve'] + end + + def default_format(f) + @default_formatter = Log4r::PatternFormatter.new(:pattern => f) + # + # Set all current outputters + # + result.outputters.each do |o| + o.formatter = @default_formatter if o.formatter.is_a?(Log4r::DefaultFormatter) + end + end + + def default_level(l) + if Log4r::Log4rTools.valid_level?(l) + @default_level = l + else + raise "Bad default level set for the logger #{l}.inspect" + end + + result.outputters.each do |o| + o.level = @default_level if o.level == Log4r::RootLogger.instance.level + end + end + + def created_outputter(outputter) + # + # Set the formatter and level for any newly created outputters + # + if @default_formatter + outputter.formatter = @default_formatter if outputter.formatter.is_a?(Log4r::DefaultFormatter) + end + + if @default_level + outputter.level = @default_level if outputter.level == Log4r::RootLogger.instance.level + end + + result.outputters << outputter + end + + end + + class ProcessorBuilder < ObjectBuilder + is_attribute "sleep_interval" + + def builder_setup + @result = Processor.instance + end + + def method_missing(name, value) + @args[name] = value + end + end + + class UDPServerBuilder < ObjectBuilder + is_attribute "port" + is_attribute "ip" + is_attribute "sleep_interval" + + def builder_setup + @result = UDPServer.instance + end + + def method_missing(name, value) + @args[name] = value + end + end + + class TimerBuilder < ObjectBuilder + is_attribute "sleep_interval" + + def builder_setup + @result = Timer.instance + end + + def method_missing(name, value) + @args[name] = value + end + + + end + + class HTTPServerBuilder < ObjectBuilder + + is_attribute "port" + is_attribute "ip" + is_attribute "document_root" + + def builder_setup + @result = HTTPServer.instance + end + + def method_missing(name, value) + @args[name] = value + end + + end + + class NotifierBuilder < ObjectBuilder + is_attribute "sleep_interval" + + def builder_setup + @result = Notifier.instance + end + + def method_missing(name, value) + @args[name] = value + end + + end + + class ServerBuilder < ObjectBuilder + + is_builder "web_interface", HTTPServerBuilder + is_builder "listener", UDPServerBuilder + is_builder "processor", ProcessorBuilder + is_builder "timer", TimerBuilder + is_builder "notifier", NotifierBuilder + + def builder_setup + @args = {} + end + + def result + @result = Mauve::Server.instance + @result.configure(@args) + @result.web_interface = @web_interface + @result + end + + def method_missing(name, value) + @args[name] = value + end + + def created_web_interface(web_interface) + @web_interface = web_interface + end + + def created_listener(listener) + @listener = listener + end + + def created_processor(processor) + @processor = processor + end + + def created_notifier(notifier) + @notifier = notifier + end + end + + class NotificationMethodBuilder < ObjectBuilder + + def builder_setup(name) + @notification_type = name.capitalize + @name = name + provider("Default") + end + + + def provider(name) + notifiers_base = Mauve::Notifiers + notifiers_type = notifiers_base.const_get(@notification_type) + @provider_class = notifiers_type.const_get(name) + end + + def result + @result ||= @provider_class.new(@name) + end + + def method_missing(name, value=nil) + if value + result.send("#{name}=".to_sym, value) + else + result.send(name.to_sym) + end + end + + end + + class PersonBuilder < ObjectBuilder + + def builder_setup(username) + @result = Person.new(username) + end + + is_block_attribute "urgent" + is_block_attribute "normal" + is_block_attribute "low" + + def all(&block); urgent(&block); normal(&block); low(&block); end + + def password (pwd) + @result.password = pwd.to_s + end + + def holiday_url (url) + @result.holiday_url = url + end + + def suppress_notifications_after(h) + raise ArgumentError.new("notification_threshold must be specified as e.g. (10 => 1.minute)") unless + h.kind_of?(Hash) && h.keys[0].kind_of?(Integer) && h.values[0].kind_of?(Integer) + @result.notification_thresholds[h.values[0]] = Array.new(h.keys[0]) + end + + end + + class NotificationBuilder < ObjectBuilder + + def builder_setup(*who) + who = who.map do |username| + #raise BuildException.new("You haven't declared who #{username} is") unless + # @context.people[username] + #@context.people[username] + if @context.people[username] + @context.people[username] + elsif @context.people_lists[username] + @context.people_lists[username] + else + raise BuildException.new("You have not declared who #{username} is") + end + end + @result = Notification.new(who, @context.last_alert_group.level) + end + + is_attribute "every" + is_block_attribute "during" + ##is_attribute "hours_in_day" + ##is_attribute "unacknowledged" + + end + + class AlertGroupBuilder < ObjectBuilder + + def builder_setup(name=anonymous_name) + @result = AlertGroup.new(name) + @context.last_alert_group = @result + end + + is_block_attribute "includes" + is_attribute "acknowledgement_time" + is_attribute "level" + + is_builder "notify", NotificationBuilder + + def created_notify(notification) + @result.notifications ||= [] + @result.notifications << notification + end + + end + + # New list of persons. + # @author Yann Golanski + class PeopleListBuilder < ObjectBuilder + + # Create a new instance and adds it. + def builder_setup(label) + pp label + @result = PeopleList.new(label) + end + + is_attribute "list" + + end + + # New list of sources. + # @author Yann Golanski + class AddSourceListBuilder < ObjectBuilder + + # Create the temporary object. + def builder_setup(label) + @result = AddSoruceList.new(label) + end + + # List of IP addresses or hostnames. + is_attribute "list" + + end + + + # this should live in AlertGroupBuilder but can't due to + # http://briancarper.net/blog/ruby-instance_eval_constant_scoping_broken + # + module ConfigConstants + URGENT = :urgent + NORMAL = :normal + LOW = :low + end + + class ConfigurationBuilder < ObjectBuilder + + include ConfigConstants + + is_builder "server", ServerBuilder + is_builder "notification_method", NotificationMethodBuilder + is_builder "person", PersonBuilder + is_builder "alert_group", AlertGroupBuilder + is_builder "people_list", PeopleListBuilder + is_builder "add_source_list", AddSourceListBuilder + is_builder "logger", LoggerBuilder + + def initialize + @context = @result = Configuration.new + # FIXME: need to test blocks that are not immediately evaluated + end + + def created_server(server) + raise ArgumentError.new("Only one 'server' clause can be specified") if + @result.server + @result.server = server + end + + def created_notification_method(notification_method) + name = notification_method.name + raise BuildException.new("Duplicate notification '#{name}'") if + @result.notification_methods[name] + @result.notification_methods[name] = notification_method + end + + def created_person(person) + name = person.username + raise BuildException.new("Duplicate person '#{name}'") if + @result.people[name] + @result.people[person.username] = person + end + + def created_alert_group(alert_group) + name = alert_group.name + raise BuildException.new("Duplicate alert_group '#{name}'") unless + @result.alert_groups.select { |g| g.name == name }.empty? + @result.alert_groups << alert_group + end + + # Create a new instance of people_list. + # + # @param [PeopleList] people_list The new list of persons. + # @return [NULL] nada. + def created_people_list(people_list) + label = people_list.label + raise BuildException.new("Duplicate people_list '#{label}'") if @result.people_lists[label] + @result.people_lists[label] = people_list + end + + # Create a new list of sources. + # + # @param [] add_source_list + # @return [NULL] nada. + def created_add_source_list(add_source_list) + @result.source_lists.create_new_list(add_source_list.label, + add_source_list.list) + end + + end + +end diff --git a/lib/mauve/datamapper.rb b/lib/mauve/datamapper.rb new file mode 100644 index 0000000..27d89a3 --- /dev/null +++ b/lib/mauve/datamapper.rb @@ -0,0 +1,13 @@ +# +# +# Small loader to put all our Datamapper requirements together +# +# +require 'dm-core' +require 'dm-sqlite-adapter-with-mutex' +require 'dm-types' +require 'dm-serializer' +require 'dm-migrations' +require 'dm-validations' +require 'dm-timestamps' + diff --git a/lib/mauve/http_server.rb b/lib/mauve/http_server.rb new file mode 100644 index 0000000..b4ced32 --- /dev/null +++ b/lib/mauve/http_server.rb @@ -0,0 +1,106 @@ +# encoding: UTF-8 +# +# Bleuurrgggggh! Bleurrrrrgghh! +# +require 'digest/sha1' +require 'log4r' +require 'thin' +require 'rack' +require 'rack-flash' +require 'rack/handler/webrick' +require 'mauve/auth_bytemark' +require 'mauve/web_interface' +require 'mauve/mauve_thread' + +################################################################################ +# +# Bodge up thin logging. +# +module Thin + module Logging + + def log(m=nil) + # return if Logging.silent? + logger = Log4r::Logger.new "Mauve::HTTPServer" + logger.info(m || yield) + end + + def debug(m=nil) + # return unless Logging.debug? + logger = Log4r::Logger.new "Mauve::HTTPServer" + logger.debug(m || yield) + end + + def trace(m=nil) + return unless Logging.trace? + logger = Log4r::Logger.new "Mauve::HTTPServer" + logger.debug(m || yield) + end + + def log_error(e=$!) + logger = Log4r::Logger.new "Mauve::HTTPServer" + logger.error(e) + logger.debug(e.backtrace.join("\n")) + end + + end +end + +################################################################################ +# +# More logging hacks for Rack +# +# @see http://stackoverflow.com/questions/2239240/use-rackcommonlogger-in-sinatra +# +class RackErrorsProxy + + def initialize(l); @logger = l; end + + def write(msg) + #@logger.debug "NEXT LOG LINE COURTESY OF: "+caller.join("\n") + case msg + when String then @logger.info(msg.chomp) + when Array then @logger.info(msg.join("\n")) + else + @logger.error(msg.inspect) + end + end + + alias_method :<<, :write + alias_method :puts, :write + def flush; end +end + + + +################################################################################ +module Mauve + + # + # API to control the web server + # + class HTTPServer < MauveThread + + include Singleton + + attr_accessor :port, :ip, :document_root + attr_accessor :session_secret # not used yet + + def initialize + @port = 32761 + @ip = "127.0.0.1" + @document_root = "." + @session_secret = rand(2**100).to_s + end + + def main_loop + @server = ::Thin::Server.new(@ip, @port, Rack::CommonLogger.new(Rack::Chunked.new(Rack::ContentLength.new(WebInterface.new)), RackErrorsProxy.new(@logger)), :signals => false) + @server.start + end + + def stop + @server.stop + super + end + end +end diff --git a/lib/mauve/mauve_thread.rb b/lib/mauve/mauve_thread.rb new file mode 100644 index 0000000..f40c79c --- /dev/null +++ b/lib/mauve/mauve_thread.rb @@ -0,0 +1,133 @@ +require 'thread' +require 'singleton' + +module Mauve + + class MauveThread + + def initialize + end + + def logger + Log4r::Logger.new(self.class.to_s) + end + + def run_thread(interval = 0.2) + # + # Good to go. + # + @frozen = false + @stop = false + + logger.debug("Started") + + @sleep_interval ||= interval + + while !@stop do + # + # Schtop! + # + if @frozen + logger.debug("Frozen") + Thread.stop + logger.debug("Thawed") + end + + yield + + next if self.should_stop? + + Kernel.sleep(@sleep_interval) + end + + logger.debug("Stopped") + end + + def should_stop? + @frozen or @stop + end + + def freeze + logger.debug("Freezing") + + @frozen = true + + 20.times { Kernel.sleep 0.1 ; break if @thread.stop? } + + logger.debug("Thread has not frozen!") unless @thread.stop? + end + + def thaw + logger.debug("Thawing") + + @frozen = false + + @thread.wakeup if @thread.stop? + end + + def start + logger.debug("Starting") + @thread = Thread.new{ self.run_thread { self.main_loop } } + end + + def run + if self.alive? + self.thaw + else + self.start + end + end + + def alive? + @thread.is_a?(Thread) and @thread.alive? + end + + def join(ok_exceptions=[]) + begin + @thread.join if @thread.is_a?(Thread) + rescue StandardError => err + logger.debug "#{err.to_s} #{err.class}" + Kernel.raise err unless ok_exceptions.any?{|e| err.is_a?(e)} + end + end + + def raise(ex) + @thread.raise(ex) + end + + def restart + self.stop + self.start + end + + def stop + logger.debug("Stopping") + + @stop = true + + 10.times do + break unless self.alive? + Kernel.sleep 1 if self.alive? + end + + # + # OK I've had enough now. + # + self.kill if self.alive? + + self.join + end + + alias exit stop + + def kill + logger.debug("Killing") + @frozen = true + @thread.kill + logger.debug("Killed") + end + + end + +end + diff --git a/lib/mauve/mauve_time.rb b/lib/mauve/mauve_time.rb new file mode 100644 index 0000000..bf69d1b --- /dev/null +++ b/lib/mauve/mauve_time.rb @@ -0,0 +1,16 @@ + +require 'time' + +module Mauve + + class MauveTime < Time + + def to_s + self.iso8601 + end + + end + +end + + diff --git a/lib/mauve/notification.rb b/lib/mauve/notification.rb new file mode 100644 index 0000000..2220211 --- /dev/null +++ b/lib/mauve/notification.rb @@ -0,0 +1,165 @@ +# encoding: UTF-8 +require 'mauve/person' +require 'mauve/notifiers' + +module Mauve + # This class provides an execution context for the code found in 'during' + # blocks in the configuration file. This code specifies when an alert + # should cause notifications to be generated, and can access @time and + # @alert variables. There are also some helper methods to provide + # oft-needed functionality such as hours_in_day. + # + # e.g. to send alerts only between 10 and 11 am: + # + # during = Proc.new { @time.hour == 10 } + # + # ... later on ... + # + # DuringRunner.new(MauveTime.now, my_alert, &during).inside? + # + # ... or to ask when an alert will next be cued ... + # + # DuringRunner.new(MauveTime.now, my_alert, &during).find_next + # + # which will return a MauveTime object, or nil if the time period will + # not be valid again, at least not in the next week. + # + class DuringRunner + + def initialize(time, alert=nil, &during) + raise ArgumentError.new("Must supply time (not #{time.inspect})") unless time.is_a?(Time) + @time = time + @alert = alert + @during = during || Proc.new { true } + @logger = Log4r::Logger.new "mauve::DuringRunner" + end + + def now? + instance_eval(&@during) + end + + def find_next(interval) + interval = 300 if true == interval.nil? + offset = (@time.nil?)? MauveTime.now : @time + plus_one_week = MauveTime.now + 604800 # ish + while offset < plus_one_week + offset += interval + if DuringRunner.new(offset, @alert, &@during).now? + @logger.debug("Found reminder time of #{offset}") + return offset + end + end + @logger.info("Could not find a reminder time less than a week "+ + "for #{@alert}.") + nil # never again + end + + protected + def hours_in_day(*hours) + x_in_list_of_y(@time.hour, hours.flatten) + end + + def days_in_week(*days) + x_in_list_of_y(@time.wday, days.flatten) + end + + ## Return true if the alert has not been acknowledged within a certain time. + # + def unacknowledged(seconds) + @alert && + @alert.raised? && + !@alert.acknowledged? && + (@time - @alert.raised_at.to_time) > seconds + end + + def x_in_list_of_y(x,y) + y.any? do |range| + if range.respond_to?("include?") + range.include?(x) + else + range == x + end + end + end + + def working_hours? + now = (@time || MauveTime.now) + (8..17).include?(now.hour) and (1..5).include?(now.wday) + end + + # Return true in the dead zone between 3 and 7 in the morning. + # + # Nota bene that this is used with different times in the reminder section. + # + # @return [Boolean] Whether now is a in the dead zone or not. + def dead_zone? + now = (@time || MauveTime.now) + (3..6).include?(now.hour) + end + + end + + # A Notification is an instruction to notify a list of people, at a + # particular alert level, on a periodic basis, and optionally under + # certain conditions specified by a block of code. + # + class Notification < Struct.new(:people, :level, :every, :during, :list) + + def to_s + "#<Notification:of #{people.map { |p| p.username }.join(',')} at level #{level} every #{every}>" + end + + attr_reader :thread_list + + def initialize(people, level) + + self.level = level + self.every = 300 + self.people = people + end + + def logger ; Log4r::Logger.new self.class.to_s ; end + + + # Updated code, now takes account of lists of people. + # + # @TODO refactor so we can test this more easily. + # + # @TODO Make sure that if no notifications is send at all, we log this + # as an error so that an email is send to the developers. Hum, we + # could have person.alert_changed return true if a notification was + # send (false otherwise) and add it to a queue. Then, dequeue till + # we see a "true" and abort. However, this needs a timeout loop + # around it and we will slow down the whole notificatin since it + # will have to wait untill such a time as it gets a true or timeout. + # Not ideal. A quick fix is to make sure that the clause in the + # configuration has a fall back that will send an alert in all cases. + # + def alert_changed(alert) + + # Should we notificy at all? + is_relevant = DuringRunner.new(MauveTime.now, alert, &during).now? + + to_notify = people.collect do |person| + case person + when Person + person + when PeopleList + person.people + else + logger.warn "Unable to notify #{person} (unrecognised class #{person.class})" + [] + end + end.flatten.uniq.each do |person| + person.alert_changed(level, alert, is_relevant, remind_at_next(alert)) + end + end + + def remind_at_next(alert) + return nil unless alert.raised? + DuringRunner.new(MauveTime.now, alert, &during).find_next(every) + end + + end + +end diff --git a/lib/mauve/notifier.rb b/lib/mauve/notifier.rb new file mode 100644 index 0000000..e0692f6 --- /dev/null +++ b/lib/mauve/notifier.rb @@ -0,0 +1,66 @@ +require 'mauve/mauve_thread' +require 'mauve/notifiers' +require 'mauve/notifiers/xmpp' + +module Mauve + + class Notifier < MauveThread + + DEFAULT_XMPP_MESSAGE = "Mauve server started." + + include Singleton + + attr_accessor :buffer, :sleep_interval + + def initialize + @buffer = Queue.new + end + + def main_loop + + # + # Cycle through the buffer. + # + sz = @buffer.size + + logger.debug("Notifier buffer is #{sz} in length") if sz > 1 + + (sz > 10 ? 10 : sz).times do + person, level, alert = @buffer.pop + begin + person.do_send_alert(level, alert) + rescue StandardError => ex + logger.debug ex.to_s + logger.debug ex.backtrace.join("\n") + end + end + + end + + def start + super + + Configuration.current.notification_methods['xmpp'].connect if Configuration.current.notification_methods['xmpp'] + end + + def stop + Configuration.current.notification_methods['xmpp'].close + + super + end + + class << self + + def enq(a) + instance.buffer.enq(a) + end + + alias push enq + + end + + end + +end + + diff --git a/lib/mauve/notifiers.rb b/lib/mauve/notifiers.rb new file mode 100644 index 0000000..7ba1d71 --- /dev/null +++ b/lib/mauve/notifiers.rb @@ -0,0 +1,6 @@ +# encoding: UTF-8 +require 'mauve/notifiers/email' +require 'mauve/notifiers/sms_default' +require 'mauve/notifiers/sms_aql' +require 'mauve/notifiers/xmpp' + diff --git a/lib/mauve/notifiers/debug.rb b/lib/mauve/notifiers/debug.rb new file mode 100644 index 0000000..889a428 --- /dev/null +++ b/lib/mauve/notifiers/debug.rb @@ -0,0 +1,68 @@ +require 'fileutils' + +module Mauve + module Notifiers + # + # The Debug module adds two extra parameters to a notification method + # for debugging and testing. + # + module Debug + class << self + def included(base) + base.class_eval do + alias_method :send_alert_without_debug, :send_alert + alias_method :send_alert, :send_alert_to_debug_channels + + # Specifying deliver_to_file allows the administrator to ask for alerts + # to be delivered to a particular file, which is assumed to be perused + # by a person rather than a machine. + # + attr :deliver_to_file, true + + # Specifying deliver_to_queue allows a tester to ask for the send_alert + # parameters to be appended to a Queue object (or anything else that + # responds to <<). + # + attr :deliver_to_queue, true + end + end + end + + def disable_normal_delivery! + @disable_normal_delivery = true + end + + def send_alert_to_debug_channels(destination, alert, all_alerts, conditions = nil) + message = if respond_to?(:prepare_message) + prepare_message(destination, alert, all_alerts, conditions) + else + [destination, alert, all_alerts].inspect + end + + if deliver_to_file + #lock_file = "#{deliver_to_file}.lock" + #while File.exists?(lock_file) + # sleep 0.1 + #end + #FileUtils.touch(lock_file) + File.open("#{deliver_to_file}", "a+") do |fh| + fh.flock(File::LOCK_EX) + fh.print("#{MauveTime.now} from #{self.class}: " + message + "\n") + fh.flush() + end + #FileUtils.rm(lock_file) + end + + deliver_to_queue << [destination, alert, all_alerts, conditions] if deliver_to_queue + + if @disable_normal_delivery + true # pretend it happened OK if we're just testing + else + send_alert_without_debug(destination, alert, all_alerts, conditions) + end + end + + end + end +end + diff --git a/lib/mauve/notifiers/email.rb b/lib/mauve/notifiers/email.rb new file mode 100644 index 0000000..c445e09 --- /dev/null +++ b/lib/mauve/notifiers/email.rb @@ -0,0 +1,138 @@ +require 'time' +require 'net/smtp' +require 'rmail' +require 'mauve/notifiers/debug' + +module Mauve + module Notifiers + module Email + + + class Default + attr_reader :name + attr :server, true + attr :port, true + attr :username, true + attr :password, true + attr :login_method, true + attr :from, true + attr :subject_prefix, true + attr :email_suffix, true + + def username=(username) + @login_method ||= :plain + @username = username + end + + def initialize(name) + @name = name + @server = '127.0.0.1' + @port = 25 + @username = nil + @password = nil + @login_method = nil + @from = "mauve@localhost" + @hostname = "localhost" + @signature = "This is an automatic mailing, please do not reply." + @subject_prefix = "" + @suppressed_changed = nil + end + + def send_alert(destination, alert, all_alerts, conditions = nil) + message = prepare_message(destination, alert, all_alerts, conditions) + args = [@server, @port] + args += [@username, @password, @login_method.to_sym] if @login_method + begin + Net::SMTP.start(*args) do |smtp| + smtp.send_message(message, @from, destination) + end + rescue Errno::ECONNREFUSED => e + @logger = Log4r::Logger.new "mauve::email_send_alert" + @logger.error("#{e.class}: #{e.message} raised. " + + "args = #{args.inspect} " + ) + raise e + rescue => e + raise e + end + end + + protected + + def prepare_message(destination, alert, all_alerts, conditions = nil) + if conditions + @suppressed_changed = conditions[:suppressed_changed] + end + + other_alerts = all_alerts - [alert] + + m = RMail::Message.new + + m.header.subject = subject_prefix + + case @suppressed_changed + when true + "Suppressing notifications (#{all_alerts.length} total)" + + else + alert.summary_one_line.to_s + end + m.header.to = destination + m.header.from = @from + m.header.date = MauveTime.now + + summary_formatted = "" +# summary_formatted = " * "+alert.summary_two_lines.join("\n ") + + case alert.update_type.to_sym + when :cleared + m.body = "An alert has been cleared:\n"+summary_formatted+"\n\n" + when :raised + m.body = "An alert has been raised:\n"+summary_formatted+"\n\n" + when :acknowledged + m.body = "An alert has been acknowledged by #{alert.acknowledged_by}:\n"+summary_formatted+"\n\n" + when :changed + m.body = "An alert has changed in nature:\n"+summary_formatted+"\n\n" + else + raise ArgumentError.new("Unknown update_type #{alert.update_type}") + end + + # FIXME: include alert.detail as multipart mime + ##Thread.abort_on_exception = true + m.body += "\n" + '-'*10 + " This is the detail field " + '-'*44 + "\n\n" + #m.body += alert.get_details() + #m.body += alert.get_details_plain_text() + m.body += "\n" + '-'*80 + "\n\n" + + if @suppressed_changed == true + m.body += <<-END +IMPORTANT: I've been configured to suppress notification of individual changes +to alerts until their rate decreases. If you still need notification of evrey +single alert, you must watch the web front-end instead. + + END + elsif @suppressed_changed == false + m.body += "(Notifications have slowed down - you will now be notified of every change)\n\n" + end + + if other_alerts.empty? + m.body += (alert.update_type == :cleared ? "That was" : "This is")+ + " currently the only alert outstanding\n\n" + else + m.body += other_alerts.length == 1 ? + "There is currently one other alert outstanding:\n\n" : + "There are currently #{other_alerts.length} other alerts outstanding:\n\n" + +# other_alerts.each do |other| +# m.body += " * "+other.summary_two_lines.join("\n ")+"\n\n" +# end + end + + m.body += @email_suffix + + m.to_s + end + include Debug + end + end + end +end diff --git a/lib/mauve/notifiers/sms_aql.rb b/lib/mauve/notifiers/sms_aql.rb new file mode 100644 index 0000000..1cf8c04 --- /dev/null +++ b/lib/mauve/notifiers/sms_aql.rb @@ -0,0 +1,90 @@ +require 'mauve/notifiers/debug' +require 'cgi' + +module Mauve + module Notifiers + module Sms + + require 'net/https' + class AQL + GATEWAY = "https://gw1.aql.com/sms/sms_gw.php" + + attr :username, true + attr :password, true + attr :from, true + attr :max_messages_per_alert, true + attr_reader :name + + def initialize(name) + @name = name + end + + def send_alert(destination, alert, all_alerts, conditions = nil) + uri = URI.parse(GATEWAY) + + opts_string = { + :username => @username, + :password => @password, + :destination => normalize_number(destination), + :message => prepare_message(destination, alert, all_alerts, conditions), + :originator => @from, + :flash => @flash ? 1 : 0 + }.map { |k,v| "#{k}=#{CGI::escape(v.to_s)}" }.join("&") + + http = Net::HTTP.new(uri.host, uri.port) + if uri.port == 443 + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + response, data = http.post(uri.path, opts_string, { + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Content-Length' => opts_string.length.to_s + }) + + raise response unless response.kind_of?(Net::HTTPSuccess) + end + + protected + def prepare_message(destination, alert, all_alerts, conditions=nil) + if conditions + @suppressed_changed = conditions[:suppressed_changed] + end + + txt = case @suppressed_changed + when true then "TOO MUCH NOISE! Last notification: " + when false then "BACK TO NORMAL: " + else + "" + end + + txt += "#{alert.update_type.upcase}: " + txt += alert.summary_one_line + + others = all_alerts-[alert] + if !others.empty? + txt += (1 == others.length)? + "and a lone other." : + "and #{others.length} others." + #txt += "and #{others.length} others: " + #txt += others.map { |alert| alert.summary_one_line }.join(", ") + end + + txt += "link: https://alert.bytemark.co.uk/alerts" + + ## @TODO: Add a link to acknowledge the alert in the text? + #txt += "Acknoweledge alert: "+ + # "https://alert.bytemark.co.uk/alert/acknowledge/"+ + # "#{alert.id}/#{alert.get_default_acknowledge_time} + + txt + end + + def normalize_number(n) + n.split("").select { |s| (?0..?9).include?(s[0]) }.join.gsub(/^0/, "44") + end + include Debug + end + end + end +end + diff --git a/lib/mauve/notifiers/sms_default.rb b/lib/mauve/notifiers/sms_default.rb new file mode 100644 index 0000000..5afeedd --- /dev/null +++ b/lib/mauve/notifiers/sms_default.rb @@ -0,0 +1,12 @@ +module Mauve + module Notifiers + module Sms + class Default + def initialize(*args) + raise ArgumentError.new("No default SMS provider, you must use the provider command to select one") + end + end + end + end +end + diff --git a/lib/mauve/notifiers/templates/email.html.erb b/lib/mauve/notifiers/templates/email.html.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/mauve/notifiers/templates/email.html.erb diff --git a/lib/mauve/notifiers/templates/email.txt.erb b/lib/mauve/notifiers/templates/email.txt.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/mauve/notifiers/templates/email.txt.erb diff --git a/lib/mauve/notifiers/templates/sms.txt.erb b/lib/mauve/notifiers/templates/sms.txt.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/mauve/notifiers/templates/sms.txt.erb diff --git a/lib/mauve/notifiers/templates/xmpp.html.erb b/lib/mauve/notifiers/templates/xmpp.html.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/mauve/notifiers/templates/xmpp.html.erb diff --git a/lib/mauve/notifiers/templates/xmpp.txt.erb b/lib/mauve/notifiers/templates/xmpp.txt.erb new file mode 100644 index 0000000..881c197 --- /dev/null +++ b/lib/mauve/notifiers/templates/xmpp.txt.erb @@ -0,0 +1 @@ +<%=arse %> diff --git a/lib/mauve/notifiers/xmpp-smack.rb b/lib/mauve/notifiers/xmpp-smack.rb new file mode 100644 index 0000000..a160a35 --- /dev/null +++ b/lib/mauve/notifiers/xmpp-smack.rb @@ -0,0 +1,395 @@ +# encoding: utf-8 + +# Ruby. +require 'pp' +require 'log4r' +require 'monitor' + +# Java. Note that paths are mangeled in jmauve_starter. +require 'java' +require 'smack.jar' +require 'smackx.jar' +include_class "org.jivesoftware.smack.XMPPConnection" +include_class "org.jivesoftware.smackx.muc.MultiUserChat" +include_class "org.jivesoftware.smack.RosterListener" + +module Mauve + + module Notifiers + + module Xmpp + + class XMPPSmackException < StandardError + end + + ## Main wrapper to smack java library. + # + # @author Yann Golanski + # @see http://www.igniterealtime.org/builds/smack/docs/3.1.0/javadoc/ + # + # This is a singleton which is not idea but works well for mauve's + # configuration file set up. + # + # In general, this class is meant to be intialized then the method + # create_slave_thread must be called. The latter will spawn a new + # thread that will do the connecting and sending of messages to + # the XMPP server. Once this is done, messages can be send via the + # send_msg() method. Those will be queued and depending on the load, + # should be send quickly enough. This is done so that the main thread + # can not worry about sending messages and can do important work. + # + # @example + # bot = Mauve::Notifiers::Xmpp::XMPPSmack.new() + # bot.run_slave_thread("chat.bytemark.co.uk", 'mauvealert', 'TopSecret') + # msg = "What fresh hell is this? -- Dorothy Parker." + # bot.send_msg("yann@chat.bytemark.co.uk", msg) + # bot.send_msg("muc:test@conference.chat.bytemark.co.uk", msg) + # + # @FIXME This won't quiet work with how mauve is set up. + # + class XMPPSmack + + # Globals are evil. + @@instance = nil + + # Default constructor. + # + # A queue (@queue) is used to pass information between master/slave. + def initialize () + extend(MonitorMixin) + @logger = Log4r::Logger.new "mauve::XMPP_smack<#{Process.pid}>" + @queue = Queue.new + @xmpp = nil + @name = "mauve alert" + @slave_thread = nil + @regexp_muc = Regexp.compile(/^muc\:/) + @regexp_tail = Regexp.compile(/\/.*$/) + @jid_created_chat = Hash.new() + @separator = '<->' + @logger.info("Created XMPPSmack singleton") + end + + # Returns the instance of the XMPPSmack singleton. + # + # @param [String] login The JID as a full address. + # @param [String] pwd The password corresponding to the JID. + # @return [XMPPSmack] The singleton instance. + def self.instance (login, pwd) + if true == @@instance.nil? + @@instance = XMPPSmack.new + jid, tmp = login.split(/@/) + srv, name = tmp.split(/\//) + name = "Mauve Alert Bot" if true == name.nil? + @@instance.run_slave_thread(srv, jid, pwd, name) + sleep 5 # FIXME: This really should be synced... But how? + end + return @@instance + end + + # Create the thread that sends messages to the server. + # + # @param [String] srv The server address. + # @param [String] jid The JID. + # @param [String] pwd The password corresponding to the JID. + # @param [String] name The bot name. + # @return [NULL] nada + def run_slave_thread (srv, jid, pwd, name) + @srv = srv + @jid = jid + @pwd = pwd + @name = name + @logger.info("Creating slave thread on #{@jid}@#{@srv}/#{@name}.") + @slave_thread = Thread.new do + self.create_slave_thread() + end + return nil + end + + # Returns whether instance is connected and authenticated. + # + # @return [Boolean] True or false. + def is_connected_and_authenticated? () + return false if true == @xmpp.nil? + return (@xmpp.isConnected() and @xmpp.isAuthenticated()) + end + + # Creates the thread that does the actual sending to XMPP. + # @return [NULL] nada + def create_slave_thread () + begin + @logger.info("Slave thread is now alive.") + self.open() + loop do + rcp, msg = @queue.deq().split(@separator, 2) + @logger.debug("New message for '#{rcp}' saying '#{msg}'.") + if rcp.match(@regexp_muc) + room = rcp.gsub(@regexp_muc, '').gsub(@regexp_tail, '') + self.send_to_muc(room, msg) + else + self.send_to_jid(rcp, msg) + end + end + rescue XMPPSmackException + @logger.fatal("Something is wrong") + ensure + @logger.info("XMPP bot disconnect.") + @xmpp.disconnect() + end + return nil + end + + # Send a message to the recipient. + # + # @param [String] rcp The recipent MUC or JID. + # @param [String] msg The message. + # @return [NULL] nada + def send_msg(rcp, msg) + #if @slave_thread.nil? or not self.is_connected_and_authenticated?() + # str = "There is either no slave thread running or a disconnect..." + # @logger.warn(str) + # self.reconnect() + #end + @queue.enq(rcp + @separator + msg) + return nil + end + + # Sends a message to a room. + # + # @param [String] room The name of the room. + # @param [String] mgs The message to send. + # @return [NULL] nada + def send_to_muc (room, msg) + if not @jid_created_chat.has_key?(room) + @jid_created_chat[room] = MultiUserChat.new(@xmpp, room) + @jid_created_chat[room].join(@name) + end + @logger.debug("Sending to MUC '#{room}' message '#{msg}'.") + @jid_created_chat[room].sendMessage(msg) + return nil + end + + # Sends a message to a jid. + # + # Do not destroy the chat, we can reuse it when the user log back in again. + # Maybe? + # + # @param [String] jid The JID of the recipient. + # @param [String] mgs The message to send. + # @return [NULL] nada + def send_to_jid (jid, msg) + if true == jid_is_available?(jid) + if not @jid_created_chat.has_key?(jid) + @jid_created_chat[jid] = @xmpp.getChatManager.createChat(jid, nil) + end + @logger.debug("Sending to JID '#{jid}' message '#{msg}'.") + @jid_created_chat[jid].sendMessage(msg) + end + return nil + end + + # Check to see if the jid is available or not. + # + # @param [String] jid The JID of the recipient. + # @return [Boolean] Whether we can send a message or not. + def jid_is_available?(jid) + if true == @xmpp.getRoster().getPresence(jid).isAvailable() + @logger.debug("#{jid} is available. Status is " + + "#{@xmpp.getRoster().getPresence(jid).getStatus()}") + return true + else + @logger.warn("#{jid} is not available. Status is " + + "#{@xmpp.getRoster().getPresence(jid).getStatus()}") + return false + end + end + + # Opens a connection to the xmpp server at given port. + # + # @return [NULL] nada + def open() + @logger.info("XMPP bot is being created.") + self.open_connection() + self.open_authentication() + self.create_roster() + sleep 5 + return nil + end + + # Connect to server. + # + # @return [NULL] nada + def open_connection() + @xmpp = XMPPConnection.new(@srv) + if false == self.connect() + str = "Connection refused" + @logger.error(str) + raise XMPPSmackException.new(str) + end + @logger.debug("XMPP bot connected successfully.") + return nil + end + + # Authenticat connection. + # + # @return [NULL] nada + def open_authentication() + if false == self.login(@jid, @pwd) + str = "Authentication failed" + @logger.error(str) + raise XMPPSmackException.new(str) + end + @logger.debug("XMPP bot authenticated successfully.") + return nil + end + + # Create a new roster and listener. + # + # @return [NULL] nada + def create_roster + @xmpp.getRoster().addRosterListener(RosterListener.new()) + @xmpp.getRoster().reload() + @xmpp.getRoster().getPresence(@xmpp.getUser).setStatus( + "Purple alert! Purple alert!") + @logger.debug("XMPP bot roster aquired successfully.") + return nil + end + + # Connects to the server. + # + # @return [Boolean] true (aka sucess) or false (aka failure). + def connect () + @xmpp.connect() + return @xmpp.isConnected() + end + + # Login onto the server. + # + # @param [String] jid The JID. + # @param [String] pwd The password corresponding to the JID. + # @return [Boolean] true (aka sucess) or false (aka failure). + def login (jid, pwd) + @xmpp.login(jid, pwd, @name) + return @xmpp.isAuthenticated() + end + + # Reconnects in case of errors. + # + # @return [NULL] nada + def reconnect() + @xmpp.disconnect + @slave_thread = Thread.new do + self.create_slave_thread() + end + return nil + end + + def presenceChanged () + end + + end # XMPPSmack + + + ## This is the class that gets called in person.rb. + # + # This class is a wrapper to XMPPSmack which does the hard work. It is + # done this way to conform to the mauve configuration file way of + # defining notifications. + # + # @author Yann Golanski + class Default + + # Name of the class. + attr_reader :name + + # Atrtribute. + attr_accessor :jid + + # Atrtribute. + attr_accessor :password + + # Atrtribute. + attr_accessor :initial_jid + + # Atrtribute. + attr_accessor :initial_messages + + # Default constructor. + # + # @param [String] name The name of the notifier. + def initialize (name) + extend(MonitorMixin) + @name = name + @logger = Log4r::Logger.new "mauve::XMPP_default<#{Process.pid}>" + end + + # Sends a message to the relevant jid or muc. + # + # We have no way to know if a messages was recieved, only that + # we send it. + # + # @param [String] destionation + # @param [Alert] alert A mauve alert class + # @param [Array] all_alerts subset of current alerts + # @param [Hash] conditions Supported conditions, see above. + # @return [Boolean] Whether a message can be send or not. + def send_alert(destination, alert, all_alerts, conditions = nil) + synchronize { + client = XMPPSmack.instance(@jid, @password) + if not destination.match(/^muc:/) + if false == client.jid_is_available?(destination.gsub(/^muc:/, '')) + return false + end + end + client.send_msg(destination, convert_alert_to_message(alert)) + return true + } + end + + # Takes an alert and converts it into a message. + # + # @param [Alert] alert The alert to convert. + # @return [String] The message, either as HTML. + def convert_alert_to_message(alert) + arr = alert.summary_three_lines + str = arr[0] + ": " + arr[1] + str += " -- " + arr[2] if false == arr[2].nil? + str += "." + return str + #return alert.summary_two_lines.join(" -- ") + #return "<p>" + alert.summary_two_lines.join("<br />") + "</p>" + end + + # This is so unit tests can run fine. + include Debug + + end # Default + + end + end +end + +# This is a simple example of usage. Run with: +# ../../../jmauve_starter.rb xmpp-smack.rb +# Clearly, the mauve jabber password is not correct. +# +# /!\ WARNING: DO NOT COMMIT THE REAL PASSWORD TO MERCURIAL!!! +# +def send_msg() + bot = Mauve::Notifiers::Xmpp::XMPPSmack.instance( + "mauvealert@chat.bytemark.co.uk/testing1234", '') + msg = "What fresh hell is this? -- Dorothy Parker." + bot.send_msg("yann@chat.bytemark.co.uk", msg) + bot.send_msg("muc:test@conference.chat.bytemark.co.uk", msg) + sleep 2 +end + +if __FILE__ == './'+$0 + Thread.abort_on_exception = true + logger = Log4r::Logger.new('mauve') + logger.level = Log4r::DEBUG + logger.add Log4r::Outputter.stdout + send_msg() + send_msg() + logger.info("START") + logger.info("END") +end diff --git a/lib/mauve/notifiers/xmpp.rb b/lib/mauve/notifiers/xmpp.rb new file mode 100644 index 0000000..d216788 --- /dev/null +++ b/lib/mauve/notifiers/xmpp.rb @@ -0,0 +1,296 @@ +require 'log4r' +require 'xmpp4r' +require 'xmpp4r/xhtml' +require 'xmpp4r/roster' +require 'xmpp4r/muc/helper/simplemucclient' +require 'mauve/notifiers/debug' +#Jabber::debug = true + +module Mauve + module Notifiers + module Xmpp + + class CountingMUCClient < Jabber::MUC::SimpleMUCClient + + attr_reader :participants + + def initialize(*a) + super(*a) + @participants = 0 + self.on_join { @participants += 1 } + self.on_leave { @participants -= 1 } + end + + end + + class Default + + include Jabber + + # Atrtribute. + attr_reader :name + + # Atrtribute. + attr_accessor :jid, :password + + # Atrtribute. + attr_accessor :initial_jid + + # Atrtribute. + attr_accessor :initial_messages + + def initialize(name) + @name = name + @mucs = {} + @roster = nil + end + + def logger + @logger ||= Log4r::Logger.new self.class.to_s + end + + def reconnect + if @client + begin + logger.debug "Jabber closing old client connection" + @client.close + @client = nil + @roster = nil + rescue Exception => ex + logger.error "#{ex} when reconnecting" + end + end + + logger.debug "Jabber starting connection to #{@jid}" + @client = Client.new(JID::new(@jid)) + @client.connect + logger.debug "Jabber authentication" + + @client.auth_nonsasl(@password, false) + @roster = Roster::Helper.new(@client) + + # Unconditionally accept all roster add requests, and respond with a + # roster add + subscription request of our own if we're not subscribed + # already + @roster.add_subscription_request_callback do |ri, stanza| + Thread.new do + logger.debug("Accepting subscription request from #{stanza.from}") + @roster.accept_subscription(stanza.from) + ensure_roster_and_subscription!(stanza.from) + end.join + end + + @roster.wait_for_roster + logger.debug "Jabber authenticated, setting presence" + + @client.send(Presence.new.set_type(:available)) + @mucs = {} + + logger.debug "Jabber is ready in theory" + end + + def reconnect_and_retry_on_error + @already_reconnected = false + begin + yield + rescue StandardError => ex + logger.error "#{ex} during notification\n" + logger.debug ex.backtrace + if !@already_reconnected + reconnect + @already_reconnected = true + retry + else + raise ex + end + end + end + + def connect + self.reconnect_and_retry_on_error { self.send_msg(@initial_jid, "Hello!") } + end + + def close + self.send_msg(@initial_jid, "Goodbye!") + @client.close + end + + # Takes an alert and converts it into a message. + # + # @param [Alert] alert The alert to convert. + # @return [String] The message, either as HTML. + def convert_alert_to_message(alert) + arr = alert.summary_three_lines + str = arr[0] + ": " + arr[1] + str += " -- " + arr[2] if false == arr[2].nil? + str += "." + return str + #return alert.summary_two_lines.join(" -- ") + #return "<p>" + alert.summary_two_lines.join("<br />") + "</p>" + end + + # Attempt to send an alert using XMPP. + # +destination+ is the JID you're sending the alert to. This should be + # a bare JID in the case of an individual, or muc:<room>@<server> for + # chatrooms (XEP0045). The +alert+ object is turned into a pretty + # message and sent to the destination as a message, if the +conditions+ + # are met. all_alerts are currently ignored. + # + # The only suported condition at the moment is :if_presence => [choices] + # which checks whether the jid in question has a presence matching one + # or more of the choices - see +check_jid_has_presence+ for options. + + def send_alert(destination, alert, all_alerts, conditions = nil) + #message = Message.new(nil, alert.summary_two_lines.join("\n")) + message = Message.new(nil, convert_alert_to_message(alert)) + + if conditions + @suppressed_changed = conditions[:suppressed_changed] + end + + # MUC JIDs are prefixed with muc: - we need to strip this out. + destination_is_muc, dest_jid = self.is_muc?(destination) + + begin + xhtml = XHTML::HTML.new("<p>" + + convert_alert_to_message(alert)+ +# alert.summary_three_lines.join("<br />") + + #alert.summary_two_lines.join("<br />") + + "</p>") + message.add_element(xhtml) + rescue REXML::ParseException => ex + logger.warn("Can't send XMPP alert as valid XHTML-IM, falling back to plaintext") + logger.debug(ex) + end + + logger.debug "Jabber sending #{message} to #{destination}" + reconnect unless @client + + ensure_roster_and_subscription!(dest_jid) unless destination_is_muc + + if conditions && !check_alert_conditions(dest_jid, conditions) + logger.debug("Alert conditions not met, not sending XMPP alert to #{jid}") + return false + end + + if destination_is_muc + if !@mucs[dest_jid] + @mucs[dest_jid] = CountingMUCClient.new(@client) + @mucs[dest_jid].join(JID.new(dest_jid)) + end + reconnect_and_retry_on_error { @mucs[dest_jid].send(message, nil) ; true } + else + message.to = dest_jid + reconnect_and_retry_on_error { @client.send(message) ; true } + end + end + + # Sends a message to the destionation. + # + # @param [String] destionation The (full) JID to send to. + # @param [String] msg The (formatted) message to send. + # @return [NIL] nada. + def send_msg(destination, msg) + reconnect unless @client + message = Message.new(nil, msg) + destination_is_muc, dest_jid = self.is_muc?(destination) + if destination_is_muc + if !@mucs[dest_jid] + @mucs[dest_jid] = CountingMUCClient.new(@client) + @mucs[dest_jid].join(JID.new(dest_jid)) + end + reconnect_and_retry_on_error { @mucs[dest_jid].send(message, nil) ; true } + else + message.to = dest_jid + reconnect_and_retry_on_error { @client.send(message) ; true } + end + return nil + end + + protected + + # Checks whether the destination JID is a MUC. + # Returns [true/false, destination] + def is_muc?(destination) + if /^muc:(.*)/.match(destination) + [true, $1] + else + [false, destination] + end + end + + # Checks to see if the JID is in our roster, and whether we are + # subscribed to it or not. Will add to the roster and subscribe as + # is necessary to ensure both are true. + def ensure_roster_and_subscription!(jid) + jid = JID.new(jid) + ri = @roster.find(jid)[jid] + if ri.nil? + @roster.add(jid, nil, true) + else + ri.subscribe unless [:to, :both, :remove].include?(ri.subscription) + end + rescue Exception => ex + logger.error("Problem ensuring that #{jid} is subscribed and in mauve's roster: #{ex.inspect}") + end + + def check_alert_conditions(destination, conditions) + any_failed = conditions.keys.collect do |key| + case key + when :if_presence : check_jid_has_presence(destination, conditions[:if_presence]) + else + #raise ArgumentError.new("Unknown alert condition, #{key} => #{conditions[key]}") + # FIXME - clean up this use of :conditions to pass arbitrary + # parameters to notifiers; for now we need to ignore this. + true + end + end.include?(false) + !any_failed + end + + # Checks our roster to see whether the jid has a resource with at least + # one of the included presences. Acceptable +presence+ types and their + # meanings for individuals: + # + # :online, :offline - user is logged in or out + # :available - jabber status is nil (available) or chat + # :unavailable - - jabber status is away, dnd or xa + # :unknown - don't know (not in roster) + # + # For MUCs: TODO + # Returns true if at least one of the presence specifiers for the jid + # is met, false otherwise. Note that if the alerter can't see the alertee's + # presence, only 'unknown' will match - generally, you'll want [:online, :unknown] + def check_jid_has_presence(jid, presence_or_presences) + return true if jid.match(/^muc:/) + + reconnect unless @client + + presences = [presence_or_presences].flatten + roster_item = @roster.find(jid) + roster_item = roster_item[roster_item.keys[0]] + resource_presences = [] + roster_item.each_presence {|p| resource_presences << p.show } if roster_item + + results = presences.collect do |need_presence| + case need_presence + when :online : (roster_item && [:to, :both].include?(roster_item.subscription) && roster_item.online?) + when :offline : (roster_item && [:to, :both].include?(roster_item.subscription) && !roster_item.online?) + when :available : (roster_item && [:to, :both].include?(roster_item.subscription) && (resource_presences.include?(nil) || + resource_presences.include?(:chat))) + # No resources are nil or chat + when :unavailable : (roster_item && [:to, :both].include?(roster_item.subscription) && (resource_presences - [:away, :dnd, :xa]).empty?) + # Not in roster or don't know subscription + when :unknown : (roster_item.nil? || [:none, :from].include?(roster_item.subscription)) + else + raise ArgumentError.new("Unknown presence possibility: #{need_presence}") + end + end + results.include?(true) + end + + end + end + end +end + diff --git a/lib/mauve/people_list.rb b/lib/mauve/people_list.rb new file mode 100644 index 0000000..2e4c737 --- /dev/null +++ b/lib/mauve/people_list.rb @@ -0,0 +1,44 @@ +# encoding: UTF-8 +require 'log4r' +require 'mauve/calendar_interface' + +module Mauve + + # Stores a list of name. + # + # @author Yann Golanski + class PeopleList < Struct.new(:label, :list) + + # Default contrustor. + def initialize (*args) + super(*args) + end + + def label + self[:label] + end + + alias username label + + def list + self[:list] + end + + # + # Set up the logger + def logger + @logger ||= Log4r::Logger.new self.class + end + + # + # Return the array of people + # + def people + list.collect do |name| + Configuration.current.people.has_key?(name) ? Configuration.current.people[name] : nil + end.reject{|person| person.nil?} + end + + end + +end diff --git a/lib/mauve/person.rb b/lib/mauve/person.rb new file mode 100644 index 0000000..6e9fcb4 --- /dev/null +++ b/lib/mauve/person.rb @@ -0,0 +1,230 @@ +# encoding: UTF-8 +require 'timeout' +require 'log4r' + +module Mauve + class Person < Struct.new(:username, :password, :holiday_url, :urgent, :normal, :low) + + attr_reader :notification_thresholds + + def initialize(*args) + @notification_thresholds = { 60 => Array.new(10) } + @suppressed = false + super(*args) + end + + def logger ; @logger ||= Log4r::Logger.new self.class.to_s ; end + + def suppressed? + @suppressed + end + + # This class implements an instance_eval context to execute the blocks + # for running a notification block for each person. + # + class NotificationCaller + + def initialize(alert, other_alerts, notification_methods, base_conditions={}) + logger = Log4r::Logger.new "mauve::NotificationCaller" + @alert = alert + @other_alerts = other_alerts + @notification_methods = notification_methods + @base_conditions = base_conditions + end + + def method_missing(name, destination, *args) + conditions = @base_conditions.merge(args[0] ? args[0] : {}) + notification_method = @notification_methods[name.to_s] + + unless notification_method + raise NoMethodError.new("#{name} not defined as a notification method") + end + # Methods are expected to return true or false so the user can chain + # them together with || as fallbacks. So we have to catch exceptions + # and turn them into false. + # + notification_method.send_alert(destination, @alert, @other_alerts, conditions) + end + + end + + ## Deals with changes in an alert. + # + # == Old comments by Matthew. + # + # An AlertGroup tells a Person that an alert has changed. Within + # this alert group, the alert may or may not be "relevant" to this + # person, but it is ultimately up to the Person to decide whether to + # send a notification. (i.e. notification of acks/clears should + # always go out to a Person who was notified of the original alert, + # even if the alert is no longer relevant to them). + # + # == New comment + # + # The old code works like this: An alert arrives, with a relevance. An + # AlertChanged is created and the alert may or may not be send. The + # problem is that alerts can be relevant AFTER the initial raise and this + # code (due to AlertChange.was_relevant_when_raised?()) will ignore it. + # This is wrong. + # + # + # The Thread.exclusive wrapper around the AlertChanged creation makes + # sure that two AlertChanged are not created at the same time. This + # caused both instances to set the remind_at time of the other to nil. + # Thus reminders were never seen which is clearly wrong. This bug was + # only showing on jruby due to green threads in MRI. + # + # + # @author Matthew Bloch, Yann Golanski + # @param [symb] level Level of the alert. + # @param [Alert] alert An alert object. + # @param [Boolean] Whether the alert is relevant as defined by notification + # class. + # @param [MauveTime] When to send remind. + # @return [NULL] nada + def alert_changed(level, alert, is_relevant=true, remind_at=nil) + # User should get notified but will not since on holiday. + str = String.new +# if is_on_holiday? +# is_relevant = false +# str = ' (user on holiday)' +# end + + # Deals with AlertChange database entry. + last_change = AlertChanged.first(:alert_id => alert.id, :person => username) + if not last_change.nil? + if not last_change.remind_at.nil? and not remind_at.nil? + if last_change.remind_at.to_time < remind_at + remind_at = last_change.remind_at.to_time + end + end + end + + new_change = AlertChanged.create( + :level => level.to_s, + :alert_id => alert.id, + :at => MauveTime.now, + :person => username, + :update_type => alert.update_type, + :remind_at => remind_at, + :was_relevant => is_relevant) + + # We need to look at the AlertChanged objects to reset them to + # the right value. What is the right value? Well... + if true == is_relevant + last_change.was_relevant = true if false == last_change.nil? + end + + # Send the notification is need be. + if !last_change || last_change.update_type.to_sym == :cleared + # Person has never heard of this alert before, or previously cleared. + # + # We don't send any alert if such a change isn't relevant to this + # Person at this time. + send_alert(level, alert) if is_relevant and [:raised, :changed].include?(alert.update_type.to_sym) + + else + # Relevance is determined by whether the user heard of this alert + # being raised. + send_alert(level, alert) if last_change.was_relevant_when_raised? + end + end + end + + def remind(alert, level) + logger.debug("Reminder for #{alert} send at level #{level}.") + send_alert(level, alert) + end + + # + # This just wraps send_alert by sending the job to a queue. + # + def send_alert(level, alert) + Notifier.push([self, level, alert]) + end + + def do_send_alert(level, alert) + now = MauveTime.now + suppressed_changed = nil + threshold_breached = @notification_thresholds.any? do |period, previous_alert_times| + first = previous_alert_times.first + first.is_a?(MauveTime) and (now - first) < period + end + + this_alert_suppressed = false + + if Server.instance.started_at > alert.updated_at.to_time and (Server.instance.started_at + Server.instance.initial_sleep) > MauveTime.now + logger.warn("Alert last updated in prior run of mauve -- ignoring for initial sleep period.") + this_alert_suppressed = true + elsif threshold_breached + unless suppressed? + logger.warn("Suspending notifications to #{username} until further notice.") + suppressed_changed = true + end + @suppressed = true + else + if suppressed? + suppressed_changed = false + logger.warn "Starting to send notifications again for #{username}." + else + logger.info "Notifying #{username} of #{alert} at level #{level}" + end + @suppressed = false + end + + return if suppressed? or this_alert_suppressed + + result = NotificationCaller.new( + alert, + current_alerts, + Configuration.current.notification_methods, + :suppressed_changed => suppressed_changed + ).instance_eval(&__send__(level)) + + if result + # + # Remember that we've sent an alert + # + @notification_thresholds.each do |period, previous_alert_times| + @notification_thresholds[period].replace(previous_alert_times[1..period-1] + [now]) + end + + logger.info("Notification for #{username} of #{alert} at level #{level} has been successful") + else + logger.error("Failed to notify #{username} about #{alert} at level #{level}") + end + end + + # Returns the subset of current alerts that are relevant to this Person. + # + def current_alerts + Alert.all_current.select do |alert| + my_last_update = AlertChanged.first(:person => username, :alert_id => alert.id) + my_last_update && my_last_update.update_type != :cleared + end + end + + protected + # Remembers that an alert has been sent so that we can later check whether + # too many alerts have been sent in a particular period. + # + def remember_alert(now=MauveTime.now) + end + + # Returns time period over which "too many" alerts have been sent, or nil + # if none. + # + def threshold_breached(now=MauveTime.now) + end + + # Whether the person is on holiday or not. + # + # @return [Boolean] True if person on holiday, false otherwise. + def is_on_holiday? () + return false if true == holiday_url.nil? or '' == holiday_url + return CalendarInterface.is_user_on_holiday?(holiday_url, username) + end + + end + +end diff --git a/lib/mauve/processor.rb b/lib/mauve/processor.rb new file mode 100644 index 0000000..414f640 --- /dev/null +++ b/lib/mauve/processor.rb @@ -0,0 +1,109 @@ +# encoding: UTF-8 + +require 'mauve/mauve_thread' + +module Mauve + + class Processor < MauveThread + + include Singleton + + attr_accessor :buffer, :transmission_cache_expire_time, :sleep_interval + + def initialize + # Buffer for UDP socket packets. + @buffer = Queue.new + + # Set the logger up + @logger = Log4r::Logger.new(self.class.to_s) + + # + # Set up the transmission id cache + # + @transmission_id_cache = {} + @transmission_cache_expire_time = 300 + @sleep_interval = 1 + end + + def main_loop + + sz = @buffer.size + + return if sz == 0 + + Timer.instance.freeze + + logger.info("Buffer has #{sz} packets waiting...") + + triplets = [] + # + # Only do the loop a maximum of 10 times every @sleep_interval seconds + # + (sz > 20 ? 20 : sz).times do + triplets += @buffer.deq + end + + triplets.each do |data, client, received_at| + + @logger.debug("Got #{data.inspect} from #{client.inspect}") + + ip_source = "#{client[3]}:#{client[1]}" + update = Proto::AlertUpdate.new + + begin + update.parse_from_string(data) + + if @transmission_id_cache[update.transmission_id.to_s] + @logger.debug("Ignoring duplicate transmission id #{data.transmission_id}") + # + # Continue with next packet. + # + next + end + + @logger.debug "Update #{update.transmission_id} sent at #{update.transmission_time} from "+ + "'#{update.source}'@#{ip_source} alerts #{update.alert.length}" + + Alert.receive_update(update, received_at) + + rescue Protobuf::InvalidWireType, + NotImplementedError, + DataObjects::IntegrityError => ex + + @logger.error "#{ex} (#{ex.class}) while parsing #{data.length} bytes "+ + "starting '#{data[0..16].inspect}' from #{ip_source}" + + @logger.debug ex.backtrace.join("\n") + + ensure + @transmission_id_cache[update.transmission_id.to_s] = MauveTime.now + + end + end + Timer.instance.thaw + end + + def expire_transmission_id_cache + now = MauveTime.now + to_delete = [] + + @transmission_id_cache.each do |tid, received_at| + to_delete << tid if (now - received_at) > @transmission_cache_expire_time + end + + to_delete.each do |tid| + @transmission_id_cache.delete(tid) + end + end + + class << self + + def enq(a) + instance.buffer.enq(a) + end + + alias push enq + + end + end +end diff --git a/lib/mauve/proto.rb b/lib/mauve/proto.rb new file mode 100644 index 0000000..6c22609 --- /dev/null +++ b/lib/mauve/proto.rb @@ -0,0 +1,116 @@ +### Generated by rprotoc. DO NOT EDIT! +### <proto file: mauve.proto> +# package mauve.proto; +# +# // An alert is a notification of an event in your business, project or +# // enterprise for which someone might want to stop what they're doing and +# // attend to. +# // +# // Alerts +# // +# message Alert { +# // Every separate alert must have a unique Id attached. When sending a +# // repeated or altered alert, using the same alert id will overwrite +# // the previous settings. +# // +# required string id = 1; +# +# // The UNIX time at which this alert was or will be raised. If set to zero it +# // means 'this alert is assumed to be raised already'. +# // +# optional uint64 raise_time = 2; +# +# // The UNIX time at which this alert was or will be cleared. If set to zero +# // it means 'do not clear automatically'. Messages with clear times set before +# // alert times are not valid, and will be ignored. +# // +# optional uint64 clear_time = 3; +# +# // The subject is the name of the server/device/entity that is being alerted +# // about. If not supplied, assumed to be the same as source. +# // +# optional string subject = 4; +# +# // The summary is a summary of an alert (100 characters or less) that +# // can be fitted into a pager or SMS message, along with the source & subject. +# // +# optional string summary = 5; +# +# // The detail can be an arbitrary HTML fragment for display on suitable +# // devices giving fuller information about the alert. +# // +# optional string detail = 6; +# +# // The importance of this alert (relative to others from this source). Zero +# // is 'unspecified importance' which will use the server's default. +# // +# optional uint32 importance = 7; +# +# } +# +# // The AlertUpdate is the unit of communication from an alerting source; +# // it consists of one or more alerts, which can either replace, or supplement +# // the alert data for that source. +# // +# message AlertUpdate { +# // Random number with each transmission, so that destinations can easily +# // identify and discard duplicate transmissions that are inherent to the +# // protocol. +# // +# required uint64 transmission_id = 1; +# +# // The source of an alert represents the sender - each possible sender +# // should set this consistently (e.g. the name of the monitoring system +# // that is generating a particular class of alerts). +# // +# required string source = 2; +# +# // When set to true, signals that this update should completely replace +# // all current data for this source (so unlisted previous alerts are deemed +# // to be cleared). +# // +# required bool replace = 3 [ default = false ]; +# +# // Alert data follows +# // +# repeated Alert alert = 4; +# +# // Signature to authenticate this data - no scheme defined currently, maybe +# // SHA1(alert.raw + password) ? +# // +# optional bytes signature = 5; +# +# // The UNIX time at which the packet was sent by the server. +# // +# optional uint64 transmission_time = 8; +# } +# + +require 'protobuf/message/message' +require 'protobuf/message/enum' +require 'protobuf/message/service' +require 'protobuf/message/extend' + +module Mauve + module Proto + class Alert < ::Protobuf::Message + defined_in __FILE__ + required :string, :id, 1 + optional :uint64, :raise_time, 2 + optional :uint64, :clear_time, 3 + optional :string, :subject, 4 + optional :string, :summary, 5 + optional :string, :detail, 6 + optional :uint32, :importance, 7 + end + class AlertUpdate < ::Protobuf::Message + defined_in __FILE__ + required :uint64, :transmission_id, 1 + required :string, :source, 2 + required :bool, :replace, 3, :default => false + repeated :Alert, :alert, 4 + optional :bytes, :signature, 5 + optional :uint64, :transmission_time, 8 + end + end +end
\ No newline at end of file diff --git a/lib/mauve/sender.rb b/lib/mauve/sender.rb new file mode 100644 index 0000000..f27efe5 --- /dev/null +++ b/lib/mauve/sender.rb @@ -0,0 +1,94 @@ +# encoding: UTF-8 +require 'ipaddr' +require 'resolv' +require 'socket' +require 'mauve/mauve_time' + +module Mauve + class Sender + DEFAULT_PORT = 32741 + include Resolv::DNS::Resource::IN + + def initialize(*destinations) + destinations = destinations.flatten + + destinations = begin + File.read("/etc/mauvealert/mauvesend.destination").split(/\s+/) + rescue Errno::ENOENT => notfound + [] + end if destinations.empty? + + if !destinations || destinations.empty? + raise ArgumentError.new("No destinations specified, and could not read any destinations from /etc/mauvealert/mauvesend.destination") + end + + @destinations = destinations.map do |spec| + next_spec = begin + # FIXME: for IPv6 + port = spec.split(":")[1] || DEFAULT_PORT + IPAddr.new(spec.split(":")[0]) + ["#{spec}:#{port}"] + rescue ArgumentError => not_an_ip_address + Resolv::DNS.open do |dns| + srv_spec = spec[0] == ?_ ? spec : "_mauvealert._udp.#{spec}" + list = dns.getresources(srv_spec, SRV).map do |srv| + srv.target.to_s + ":#{srv.port}" + end + list = [spec] if list.empty? + list.map do |spec2| + spec2_addr, spec2_port = spec2.split(":") + spec2_port ||= DEFAULT_PORT + dns.getresources(spec2_addr, A).map do |a| + "#{a.address}:#{spec2_port}" + end + end + end + end.flatten + + error "Can't resolve destination #{spec}" if next_spec.empty? + + next_spec + end. + flatten. + uniq + end + + def send(update, verbose=0) + + # + # Must have a source, so default to hostname if user doesn't care + update.source ||= `hostname -f`.chomp + + # + # Make sure all alerts default to "-r now" + # + update.alert.each do |alert| + next if alert.raise_time || alert.clear_time + alert.raise_time = MauveTime.now.to_i + end + + # + # Make sure we set the transmission time + # + update.transmission_time = MauveTime.now.to_i + + data = update.serialize_to_string + + + if verbose == 1 + print "#{update.transmission_id}\n" + elsif verbose >= 2 + print "Sending #{update.inspect.chomp} to #{@destinations.join(", ")}\n" + end + + @destinations.each do |spec| + UDPSocket.open do |sock| + ip = spec.split(":")[0] + port = spec.split(":")[1].to_i + sock.send(data, 0, ip, port) + end + end + end + end +end + diff --git a/lib/mauve/server.rb b/lib/mauve/server.rb new file mode 100644 index 0000000..30536bb --- /dev/null +++ b/lib/mauve/server.rb @@ -0,0 +1,142 @@ +# encoding: UTF-8 +require 'yaml' +require 'socket' +# require 'mauve/datamapper' +require 'mauve/proto' +require 'mauve/alert' +require 'mauve/mauve_thread' +require 'mauve/mauve_time' +require 'mauve/timer' +require 'mauve/udp_server' +require 'mauve/processor' +require 'mauve/http_server' +require 'log4r' + + +module Mauve + + class Server + + DEFAULT_CONFIGURATION = { + :ip => "127.0.0.1", + :port => 32741, + :database => "sqlite3:///./mauvealert.db", + :log_file => "stdout", + :log_level => 1, + :transmission_cache_expire_time => 600 + } + + + # + # This is the order in which the threads should be started. + # + THREAD_CLASSES = [UDPServer, HTTPServer, Processor, Notifier, Timer] + + attr_accessor :web_interface + attr_reader :stopped_at, :started_at, :initial_sleep + + include Singleton + + def initialize + # Set the logger up + @logger = Log4r::Logger.new(self.class.to_s) + + # Sleep time between pooling the @buffer buffer. + @sleep = 1 + + @freeze = false + @stop = false + + @stopped_at = MauveTime.now + @started_at = MauveTime.now + @initial_sleep = 300 + + @config = DEFAULT_CONFIGURATION + end + + def configure(config_spec = nil) + # + # Update the configuration + # + if config_spec.nil? + # Do nothing + elsif config_spec.kind_of?(String) and File.exists?(config_spec) + @config.update(YAML.load_file(config_spec)) + elsif config_spec.kind_of?(Hash) + @config.update(config_spec) + else + raise ArgumentError.new("Unknown configuration spec "+config_spec.inspect) + end + + # + DataMapper.setup(:default, @config[:database]) + DataObjects::Sqlite3.logger = Log4r::Logger.new("Mauve::DataMapper") + + # + # Update any tables. + # + Alert.auto_upgrade! + AlertChanged.auto_upgrade! + Mauve::AlertEarliestDate.create_view! + + # + # Work out when the server was last stopped + # + @stopped_at = self.last_heartbeat + end + + def last_heartbeat + # + # Work out when the last update was + # + [ Alert.last(:order => :updated_at.asc), + AlertChanged.last(:order => :updated_at.asc) ]. + reject{|a| a.nil?}. + collect{|a| a.updated_at.to_time}. + sort. + last + end + + def freeze + @frozen = true + end + + def thaw + @thaw = true + end + + def stop + @stop = true + + THREAD_CLASSES.reverse.each do |klass| + klass.instance.stop + end + + @logger.info("All threads stopped") + end + + def run + loop do + THREAD_CLASSES.each do |klass| + next if @frozen or @stop + + unless klass.instance.alive? + # ugh something has died. + klass.instance.join + klass.instance.start unless @stop + end + + end + + break if @stop + + sleep 1 + end + logger.debug("Thread stopped") + end + + alias start run + + end + +end diff --git a/lib/mauve/source_list.rb b/lib/mauve/source_list.rb new file mode 100644 index 0000000..4ffef15 --- /dev/null +++ b/lib/mauve/source_list.rb @@ -0,0 +1,105 @@ +# encoding: UTF-8 +require 'log4r' +require 'resolv-replace' + +module Mauve + + # A simple construct to match sources. + # + # This class stores mamed lists of IP addresses. It stores them in a hash + # indexed by the name of the list. One can pass IPv4, IPv6 and hostnames + # as list elements but they will all be converted into IP addresses at + # the time of the list creation. + # + # One can ask if an IPv4, IPv6, hostname or url (match on hostname only) is + # contained within a list. If the query is not an IP address, it will be + # converted into one before the checks are made. + # + # Note that the matching is greedy. When a hostname maps to several IP + # addresses and only one of tbhose is included in the list, a match + # will occure. + # + # @author Yann Golanski + class SourceList + + # Accessor, read only. Use create_new_list() to create lists. + attr_reader :hash + + ## Default contructor. + def initialize () + @logger = Log4r::Logger.new "mauve::SourceList" + @hash = Hash.new + @http_head = Regexp.compile(/^http[s]?:\/\//) + @http_tail = Regexp.compile(/\/.*$/) + end + + ## Return whether or not a list contains a source. + # + # @param [String] lst The list name. + # @param [String] src The hostname or IP of the source. + # @return [Boolean] true if there is such a source, false otherwise. + def does_list_contain_source?(lst, src) + raise ArgumentError.new("List name must be a String, not a #{lst.class}") if String != lst.class + raise ArgumentError.new("Source name must be a String, not a #{src.class}") if String != src.class + raise ArgumentError.new("List #{lst} does not exist.") if false == @hash.has_key?(lst) + if src.match(@http_head) + src = src.gsub(@http_head, '').gsub(@http_tail, '') + end + begin + Resolv.getaddresses(src).each do |ip| + return true if @hash[lst].include?(ip) + end + rescue Resolv::ResolvError, Resolv::ResolvMauveTimeout => e + @logger.warn("#{lst} could not be resolved because #{e.message}.") + return false + rescue => e + @logger.error("Unknown exception raised: #{e.class} #{e.message}") + return false + end + return false + end + + ## Create a list. + # + # Note that is no elements give IP addresses, we have an empty list. + # This gets logged but otherwise does not stop mauve from working. + # + # @param [String] name The name of the list. + # @param [Array] elem A list of source either hostname or IP. + def create_new_list(name, elem) + raise ArgumentError.new("Name of list is not a String but a #{name.class}") if String != name.class + raise ArgumentError.new("Element list is not an Array but a #{elem.class}") if Array != elem.class + raise ArgumentError.new("A list called #{name} already exists.") if @hash.has_key?(name) + arr = Array.new + elem.each do |host| + begin + Resolv.getaddresses(host).each do |ip| + arr << ip + end + rescue Resolv::ResolvError, Resolv::ResolvMauveTimeout => e + @logger.warn("#{host} could not be resolved because #{e.message}.") + rescue => e + @logger.error("Unknown exception raised: #{e.class} #{e.message}") + end + end + @hash[name] = arr.flatten.uniq.compact + if true == @hash[name].empty? + @logger.error("List #{name} is empty! "+ + "Nothing from element list '#{elem}' "+ + "has resolved to anything useable.") + end + end + + end + + ## temporary object to convert from configuration file to the SourceList class + class AddSoruceList < Struct.new(:label, :list) + + # Default constructor. + def initialize (*args) + super(*args) + end + + end + +end diff --git a/lib/mauve/timer.rb b/lib/mauve/timer.rb new file mode 100644 index 0000000..5355dcc --- /dev/null +++ b/lib/mauve/timer.rb @@ -0,0 +1,86 @@ +# encoding: UTF-8 +require 'mauve/alert' +require 'mauve/notifier' +require 'mauve/mauve_thread' +require 'thread' +require 'log4r' + +module Mauve + + class Timer < MauveThread + + include Singleton + + attr_accessor :sleep_interval, :last_run_at + + def initialize + @logger = Log4r::Logger.new self.class.to_s + @logger.info("Timer singleton created.") + @initial_sleep = 300 + @initial_sleep_threshold = 300 + end + + def main_loop + @logger.debug "hello" + # + # Get the next alert. + # + next_alert = Alert.find_next_with_event + + # + # If we didn't find an alert, or the alert we found is due in the future, + # look for the next alert_changed object. + # + if next_alert.nil? or next_alert.due_at > MauveTime.now + @logger.debug("Next alert was #{next_alert} due at #{next_alert.due_at}") unless next_alert.nil? + next_alert_changed = AlertChanged.find_next_with_event + end + + if next_alert_changed.nil? and next_alert.nil? + next_to_notify = nil + + elsif next_alert.nil? or next_alert_changed.nil? + next_to_notify = (next_alert || next_alert_changed) + + else + next_to_notify = ( next_alert.due_at < next_alert_changed.due_at ? next_alert : next_alert_changed ) + + end + + # + # Nothing to notify? + # + if next_to_notify.nil? + # + # Sleep indefinitely + # + @logger.debug("Nothing to notify about -- snoozing indefinitely.") + else + # + # La la la nothing to do. + # + @logger.debug("Next to notify: #{next_to_notify} -- snoozing until #{next_to_notify.due_at}") + end + + # + # Ah-ha! Sleep with a break clause. + # + while next_to_notify.nil? or MauveTime.now <= next_to_notify.due_at + # + # Start again if the situation has changed. + # + break if self.should_stop? + # + # This is a rate-limiting step for alerts. + # + Kernel.sleep 0.2 + end + + return if self.should_stop? or next_to_notify.nil? + + next_to_notify.poll + end + + end + +end diff --git a/lib/mauve/udp_server.rb b/lib/mauve/udp_server.rb new file mode 100644 index 0000000..a570e8a --- /dev/null +++ b/lib/mauve/udp_server.rb @@ -0,0 +1,109 @@ +# encoding: UTF-8 +require 'yaml' +require 'socket' +require 'mauve/datamapper' +require 'mauve/proto' +require 'mauve/alert' +require 'log4r' + +module Mauve + + class UDPServer < MauveThread + + include Singleton + + attr_accessor :ip, :port, :sleep_interval + + def initialize + # + # Set the logger up + # + @logger = Log4r::Logger.new(self.class.to_s) + @ip = "127.0.0.1" + @port = 32741 + @socket = nil + @closing_now = false + @sleep_interval = 0 + end + + def open_socket + @socket = UDPSocket.new + @closing_now = false + + @logger.debug("Trying to increase Socket::SO_RCVBUF to 10M.") + old = @socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF).unpack("i").first + + @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, 10*1024*1024) + new = @socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF).unpack("i").first + + raise "Could not increase Socket::SO_RCVBUF. Had #{old} ended up with #{new}!" if old > new + + @logger.debug("Successfully increased Socket::SO_RCVBUF from #{old} to #{new}.") + + @socket.bind(@ip, @port) + + @logger.debug("Successfully opened UDP socket on #{@ip}:#{@port}") + end + + def close_socket + return if @socket.nil? or @socket.closed? + + begin + @socket.close + rescue IOError => ex + # Just in case there is some sort of explosion! + @logger.debug("Caught IOError #{ex.to_s}") + end + + @logger.debug("Successfully closed UDP socket") + end + + def main_loop + return if self.should_stop? + + open_socket if @socket.nil? or @socket.closed? + + return if self.should_stop? + + # + # TODO: why is/isn't this non-block? + # + begin + # packet = @socket.recvfrom_nonblock(65535) + packet = @socket.recvfrom(65535) + received_at = MauveTime.now + rescue Errno::EAGAIN, Errno::EWOULDBLOCK + IO.select([@socket]) + retry unless self.should_stop? + end + + return if packet.nil? + + @logger.debug("Got new packet: #{packet.inspect}") + + # + # If we get a zero length packet, and we've been flagged to stop, we stop! + # + if packet.first.length == 0 and self.should_stop? + self.close_socket + return + end + + + + Processor.push([[packet[0], packet[1], received_at]]) + end + + def stop + @stop = true + # + # Triggers loop to close socket. + # + UDPSocket.open.send("", 0, @socket.addr[2], @socket.addr[1]) unless @socket.closed? + + super + end + + end + +end diff --git a/lib/mauve/web_interface.rb b/lib/mauve/web_interface.rb new file mode 100644 index 0000000..4569cb6 --- /dev/null +++ b/lib/mauve/web_interface.rb @@ -0,0 +1,334 @@ +# encoding: UTF-8 +require 'sinatra/base' +require 'sinatra-partials' +require 'haml' +require 'rack' +require 'rack-flash' + +if !defined?(JRUBY_VERSION) + require 'thin' +end + +module Mauve + # Our Sinatra app proper + # + class WebInterface < Sinatra::Base + + class PleaseAuthenticate < Exception; end + + use Rack::Session::Cookie, :expire_after => 604800 # 7 days in seconds + + enable :sessions + + use Rack::Flash + + set :root, "/usr/share/mauve" + set :views, "#{root}/views" + set :public, "#{root}/static" + set :static, true + set :show_exceptions, true + + logger = Log4r::Logger.new("Mauve::WebInterface") + + set :logging, true + set :logger, logger + set :dump_errors, true # ...will dump errors to the log + set :raise_errors, false # ...will not let exceptions out to main program + set :show_exceptions, false # ...will not show exceptions + + ######################################################################## + + before do + @person = Configuration.current.people[session['username']] + @title = "Mauve alert panel" + end + + get '/' do + redirect '/alerts' + end + + ######################################################################## + + ## Checks the identity of the person via a password. + # + # The password can be either the SSO or a local one defined + # in the configuration file. + # + post '/login' do + usr = params['username'] + pwd = params['password'] + ret_sso = helper_auth_SSO(usr, pwd) + ret_loc = helper_auth_local(usr, pwd) + if "success" == ret_sso or "success" == ret_loc + session['username'] = usr + else + flash['error'] =<<__MSG +<hr /> <img src="/images/error.png" /> <br /> +ACCESS DENIED <br /> +#{ret_sso} <br /> +#{ret_loc} <hr /> +__MSG + end + redirect '/alerts' + end + + get '/logout' do + session.delete('username') + redirect '/alerts' + end + + get '/alerts' do + #now = MauveTime.now.to_f + please_authenticate() + find_active_alerts() + #pp MauveTime.now.to_f - now + haml(:alerts2) + end + + get '/_alert_summary' do + find_active_alerts; partial("alert_summary") + end + + get '/_alert_counts' do + find_active_alerts; partial("alert_counts") + end + + get '/_head' do + find_active_alerts() + partial("head") + end + + get '/alert/:id/detail' do + please_authenticate + + content_type("text/html") # I think + Alert.get(params[:id]).detail + end + + get '/alert/:id' do + please_authenticate + find_active_alerts + @alert = Alert.get(params['id']) + haml :alert + end + + post '/alert/:id/acknowledge' do + please_authenticate + + alert = Alert.get(params[:id]) + if alert.acknowledged? + alert.unacknowledge! + else + alert.acknowledge!(@person, 0) + end + content_type("application/json") + alert.to_json + end + + # Note that :until must be in seconds. + post '/alert/acknowledge/:id/:until' do + #now = MauveTime.now.to_f + please_authenticate + + alert = Alert.get(params[:id]) + alert.acknowledge!(@person, params[:until].to_i()) + + #print "Acknowledge request was processed in #{MauveTime.now.to_f - now} seconds\n" + content_type("application/json") + alert.to_json + end + + post '/alert/:id/raise' do + #now = MauveTime.now.to_f + please_authenticate + + alert = Alert.get(params[:id]) + alert.raise! + #print "Raise request was processed in #{MauveTime.now.to_f - now} seconds\n" + content_type("application/json") + alert.to_json + end + + post '/alert/:id/clear' do + please_authenticate + + alert = Alert.get(params[:id]) + alert.clear! + content_type("application/json") + alert.to_json + end + + post '/alert/:id/toggleDetailView' do + please_authenticate + + alert = Alert.get(params[:id]) + if nil != alert + id = params[:id].to_i() + session[:display_alerts][id] = (true == session[:display_alerts][id])? false : true + content_type("application/json") + 'all is good'.to_json + end + end + + post '/alert/fold/:subject' do + please_authenticate + + session[:display_folding][params[:subject]] = (true == session[:display_folding][params[:subject]])? false : true + content_type("application/json") + 'all is good'.to_json + end + + ######################################################################## + + get '/preferences' do + please_authenticate + find_active_alerts + haml :preferences + end + + ######################################################################## + + get '/events' do + please_authenticate + find_active_alerts + find_recent_alerts + haml :events + end + + ######################################################################## + + helpers do + include Sinatra::Partials + + def please_authenticate + raise PleaseAuthenticate.new unless @person + end + + def find_active_alerts + + # FIXME: make sure alerts only appear once some better way + #@urgent = AlertGroup.all_alerts_by_level(:urgent) + #@normal = AlertGroup.all_alerts_by_level(:normal) - @urgent + #@low = AlertGroup.all_alerts_by_level(:low) - @normal - @urgent + ook = Alert.get_all() + @urgent = ook[:urgent] + @normal = ook[:normal] + @low = ook[:low] + + # Get groups of alerts and count those acknowledged. + @grouped_ack_urgent = Hash.new() + @grouped_ack_normal = Hash.new() + @grouped_ack_low = Hash.new() + @grouped_new_urgent = Hash.new() + @grouped_new_normal = Hash.new() + @grouped_new_low = Hash.new() + @count_ack = Hash.new() + @count_ack[:urgent] = self.group_alerts(@grouped_ack_urgent, + @grouped_new_urgent, + @urgent) + @count_ack[:normal] = self.group_alerts(@grouped_ack_normal, + @grouped_new_normal, + @normal) + @count_ack[:low] = self.group_alerts(@grouped_ack_low, + @grouped_new_low, + @low) + @grouped_ack = Hash.new() + @grouped_new = Hash.new() + @grouped_ack_urgent.each_pair {|k,v| @grouped_ack[k] = v} + @grouped_ack_normal.each_pair {|k,v| @grouped_ack[k] = v} + @grouped_ack_low.each_pair {|k,v| @grouped_ack[k] = v} + @grouped_new_urgent.each_pair {|k,v| @grouped_new[k] = v} + @grouped_new_normal.each_pair {|k,v| @grouped_new[k] = v} + @grouped_new_low.each_pair {|k,v| @grouped_new[k] = v} + end + + ## Fill two hashs with alerts that are acknowledged or not. + # @param [Hash] ack Acknowledge hash. + # @param [Hash] new Unacknowledged (aka new) hash. + # @param [List] list List of alerts. + # @return [Fixnum] The count of acknowledged alerts. + def group_alerts(ack, new, list) + count = 0 + list.each do |alert| + #key = alert.source + '::' + alert.subject + key = alert.subject + if true == alert.acknowledged? + count += 1 + ack[key] = Array.new() if false == ack.has_key?(key) + ack[key] << alert + else + new[key] = Array.new() if false == new.has_key?(key) + new[key] << alert + end + if false == session[:display_alerts].has_key?(alert.id) + session[:display_alerts][alert.id] = false + end + if false == session[:display_folding].has_key?(key) + session[:display_folding][key] = false + end + #session[:display_alerts][alert.id] = true if false == session[:display_alerts].has_key?(alert.id) + #session[:display_folding][key] = true if false == session[:display_folding].has_key?(key) + new.each_key {|k| new[k].sort!{|a,b| a.summary <=> b.summary} } + ack.each_key {|k| ack[k].sort!{|a,b| a.summary <=> b.summary} } + end + return count + end + + def find_recent_alerts + since = params['since'] ? MauveTime.parse(params['since']) : (MauveTime.now-86400) + @alerts = Alert.all(:updated_at.gt => since, :order => [:raised_at.desc, :cleared_at.desc, :acknowledged_at.desc, :updated_at.desc, ]) + end + + def cycle(*list) + @cycle ||= 0 + @cycle = (@cycle + 1) % list.length + list[@cycle] + end + + ## Test for authentication with SSO. + # + def helper_auth_SSO (usr, pwd) + auth = AuthSourceBytemark.new() + begin + return "success" if true == auth.authenticate(usr,pwd) + return "SSO did not regcognise your login/password combination." + rescue ArgumentError => ex + return "SSO argument error: #{ex.message}" + rescue => ex + return "SSO generic error: #{ex.message}" + end + end + + ## Test for authentication with configuration file parameter. + # + def helper_auth_local (usr, pwd) + person = Configuration.current.people[params['username']] + return "I did not recognise your local login details." if !person + return "I did not recognise your local password." if Digest::SHA1.hexdigest(params['password']) != person.password + return "success" + end + + end + + ######################################################################## + + error PleaseAuthenticate do + status 403 + session[:display_alerts] = Hash.new() + session[:display_folding] = Hash.new() + haml :please_authenticate + end + + ######################################################################## + # @see http://stackoverflow.com/questions/2239240/use-rackcommonlogger-in-sinatra + def call(env) + if true == @logger.nil? + @logger = Log4r::Logger.new("mauve::Rack") + end + env['rack.errors'] = RackErrorsProxy.new(@logger) + super(env) + end + + end + +end diff --git a/lib/object_builder.rb b/lib/object_builder.rb new file mode 100644 index 0000000..7cb808c --- /dev/null +++ b/lib/object_builder.rb @@ -0,0 +1,113 @@ +# ObjectBuilder is a class to help you build Ruby-based configuration syntaxes. +# You can use it to make "builder" classes to help build particular types +# of objects, typically translating simple command-based syntax to creating +# classes and setting attributes. e.g. here is a description of a day at +# the zoo: +# +# person "Alice" +# person "Matthew" +# +# zoo("London") { +# enclosure("Butterfly House") { +# +# has_roof +# allow_visitors +# +# animals("moth", 10) { +# wings 2 +# legs 2 +# } +# +# animals("butterfly", 200) { +# wings 2 +# legs 2 +# } +# } +# +# enclosure("Aquarium") { +# no_roof +# +# animal("killer whale") { +# called "Shamu" +# wings 0 +# legs 0 +# tail +# } +# } +# } +# +# Here is the basic builder class for a Zoo... +# +# TODO: finish this convoluted example, if it kills me +# +class ObjectBuilder + class BuildException < Exception; end + + attr_reader :result + + def initialize(context, *args) + @context = context + builder_setup(*args) + end + + def anonymous_name + @@sequence ||= 0 # not inherited, don't want it to be + @@sequence += 1 + "anon.#{Time.now.to_i}.#{@@sequence}" + end + + class << self + + def is_builder(word, clazz) + define_method(word.to_sym) do |*args, &block| + builder = clazz.new(*([@context] + args)) + builder.instance_eval(&block) if block + ["created_#{word}", "created"].each do |created_method| + created_method = created_method.to_sym + if respond_to?(created_method) + __send__(created_method, builder.result) + break + end + end + end + end + + # FIXME: implement is_builder_deferred to create object at end of block? + + def is_block_attribute(word) + define_method(word.to_sym) do |*args, &block| + @result.__send__("#{word}=".to_sym, block) + end + end + + def is_attribute(word) + define_method(word.to_sym) do |*args, &block| + @result.__send__("#{word}=".to_sym, args[0]) + end + end + + def is_flag_attribute(word) + define_method(word.to_sym) do |*args, &block| + @result.__send__("#{word}=".to_sym, true) + end + end + + def load(file) + builder = self.new + builder.instance_eval(File.read(file), file) + builder.result + end + + def inherited(*args) + initialize_class + end + + def initialize_class + @words = {} + end + end + + initialize_class +end + + diff --git a/lib/rack-flash.rb b/lib/rack-flash.rb new file mode 100644 index 0000000..bc95b21 --- /dev/null +++ b/lib/rack-flash.rb @@ -0,0 +1,171 @@ +# +# borrowed from http://github.com/nakajima/rack-flash - thanks! +# + +require 'rack/builder' + +module Rack + class Builder + attr :ins + def use(middleware, *args, &block) + middleware.instance_variable_set "@rack_builder", self + def middleware.rack_builder + @rack_builder + end + @ins << lambda { |app| + middleware.new(app, *args, &block) + } + end + + def run(app) + klass = app.class + klass.instance_variable_set "@rack_builder", self + def klass.rack_builder + @rack_builder + end + @ins << app #lambda { |nothing| app } + end + + def leaf_app + ins.last + end + end +end + +module Rack + class Flash + # Raised when the session passed to FlashHash initialize is nil. This + # is usually an indicator that session middleware is not in use. + class SessionUnavailable < StandardError; end + + # Implements bracket accessors for storing and retrieving flash entries. + class FlashHash + attr_reader :flagged + + def initialize(store, opts={}) + raise Rack::Flash::SessionUnavailable \ + .new('Rack::Flash depends on session middleware.') unless store + + @opts = opts + @store = store + + if accessors = @opts[:accessorize] + accessors.each { |opt| def_accessor(opt) } + end + end + + # Remove an entry from the session and return its value. Cache result in + # the instance cache. + def [](key) + key = key.to_sym + cache[key] ||= values.delete(key) + end + + # Store the entry in the session, updating the instance cache as well. + def []=(key,val) + key = key.to_sym + cache[key] = values[key] = val + end + + # Store a flash entry for only the current request, swept regardless of + # whether or not it was actually accessed. Useful for AJAX requests, where + # you want a flash message, even though you're response isn't redirecting. + def now + cache + end + + # Checks for the presence of a flash entry without retrieving or removing + # it from the cache or store. + def has?(key) + [cache, values].any? { |store| store.keys.include?(key.to_sym) } + end + alias_method :include?, :has? + + # Mark existing entries to allow for sweeping. + def flag! + @flagged = values.keys + end + + # Remove flagged entries from flash session, clear flagged list. + def sweep! + Array(flagged).each { |key| values.delete(key) } + flagged.clear + end + + # Hide the underlying :__FLASH__ session key and only expose values stored + # in the flash. + def inspect + '#<FlashHash @values=%s @cache=%s>' % [values.inspect, cache.inspect] + end + + # Human readable for logging. + def to_s + values.inspect + end + + private + + # Maintain an instance-level cache of retrieved flash entries. These + # entries will have been removed from the session, but are still available + # through the cache. + def cache + @cache ||= {} + end + + # Helper to access flash entries from :__FLASH__ session value. This key + # is used to prevent collisions with other user-defined session values. + def values + @store[:__FLASH__] ||= {} + end + + # Generate accessor methods for the given entry key if :accessorize is true. + def def_accessor(key) + raise ArgumentError.new('Invalid entry type: %s' % key) if respond_to?(key) + + class << self; self end.class_eval do + define_method(key) { |*args| val = args.first; val ? (self[key]=val) : self[key] } + define_method("#{key}=") { |val| self[key] = val } + define_method("#{key}!") { |val| cache[key] = val } + end + end + end + + # ------------------------------------------------------------------------- + # - Rack Middleware implementation + + def initialize(app, opts={}) + if klass = app_class(app, opts) + klass.class_eval do + def flash; env['x-rack.flash'] end + end + end + + @app, @opts = app, opts + end + + def call(env) + env['x-rack.flash'] ||= Rack::Flash::FlashHash.new(env['rack.session'], @opts) + + if @opts[:sweep] + env['x-rack.flash'].flag! + end + + res = @app.call(env) + + if @opts[:sweep] + env['x-rack.flash'].sweep! + end + + res + end + + private + + def app_class(app, opts) + return nil if opts.has_key?(:helper) and not opts[:helper] + opts[:flash_app_class] || + defined?(Sinatra::Base) && Sinatra::Base || + self.class.rack_builder.leaf_app.class + end + end +end diff --git a/lib/sinatra-partials.rb b/lib/sinatra-partials.rb new file mode 100644 index 0000000..e16313c --- /dev/null +++ b/lib/sinatra-partials.rb @@ -0,0 +1,25 @@ +# +# stolen from http://github.com/cschneid/irclogger/blob/master/lib/partials.rb +# and made a lot more robust by me +# +# this implementation uses erb by default. if you want to use any other template mechanism +# then replace `erb` on line 13 and line 17 with `haml` or whatever +# + +module Sinatra::Partials + def partial(template, *args) + template_array = template.to_s.split('/') + template = template_array[0..-2].join('/') + "/_#{template_array[-1]}" + options = args.last.is_a?(Hash) ? args.pop : {} + options.merge!(:layout => false) + if collection = options.delete(:collection) then + collection.inject([]) do |buffer, member| + buffer << erb(:"#{template}", options.merge(:layout => + false, :locals => {template_array[-1].to_sym => member})) + end.join("\n") + else + haml(:"#{template}", options) + end + end +end + diff --git a/mauve.proto b/mauve.proto new file mode 100644 index 0000000..7bd25e4 --- /dev/null +++ b/mauve.proto @@ -0,0 +1,85 @@ +package mauve.proto; + +// An alert is a notification of an event in your business, project or +// enterprise for which someone might want to stop what they're doing and +// attend to. +// +// Alerts +// +message Alert { + // Every separate alert must have a unique Id attached. When sending a + // repeated or altered alert, using the same alert id will overwrite + // the previous settings. + // + required string id = 1; + + // The UNIX time at which this alert was or will be raised. If set to zero it + // means 'this alert is assumed to be raised already'. + // + optional uint64 raise_time = 2; + + // The UNIX time at which this alert was or will be cleared. If set to zero + // it means 'do not clear automatically'. Messages with clear times set before + // alert times are not valid, and will be ignored. + // + optional uint64 clear_time = 3; + + // The subject is the name of the server/device/entity that is being alerted + // about. If not supplied, assumed to be the same as source. + // + optional string subject = 4; + + // The summary is a summary of an alert (100 characters or less) that + // can be fitted into a pager or SMS message, along with the source & subject. + // + optional string summary = 5; + + // The detail can be an arbitrary HTML fragment for display on suitable + // devices giving fuller information about the alert. + // + optional string detail = 6; + + // The importance of this alert (relative to others from this source). Zero + // is 'unspecified importance' which will use the server's default. + // + optional uint32 importance = 7; + +} + +// The AlertUpdate is the unit of communication from an alerting source; +// it consists of one or more alerts, which can either replace, or supplement +// the alert data for that source. +// +message AlertUpdate { + // Random number with each transmission, so that destinations can easily + // identify and discard duplicate transmissions that are inherent to the + // protocol. + // + required uint64 transmission_id = 1; + + // The source of an alert represents the sender - each possible sender + // should set this consistently (e.g. the name of the monitoring system + // that is generating a particular class of alerts). + // + required string source = 2; + + // When set to true, signals that this update should completely replace + // all current data for this source (so unlisted previous alerts are deemed + // to be cleared). + // + required bool replace = 3 [ default = false ]; + + // Alert data follows + // + repeated Alert alert = 4; + + // Signature to authenticate this data - no scheme defined currently, maybe + // SHA1(alert.raw + password) ? + // + optional bytes signature = 5; + + // The UNIX time at which the packet was sent by the server. + // + optional uint64 transmission_time = 8; +} + diff --git a/mauveserver.conf b/mauveserver.conf new file mode 100644 index 0000000..1000283 --- /dev/null +++ b/mauveserver.conf @@ -0,0 +1,136 @@ +# Example mauveserver.conf file, based on Bytemark's configuration. +# Probably still needs a bit of improvement and explanation. +# + +# The service which listens for alert messages +# +server { + # persistent data store, only sqlite3 supported at the moment + database "sqlite3:///var/lib/mauvealert/alerts.db" + + # you probably want to listen on all addresses + ip "0.0.0.0" + # UDP port number + port 32741 + + # logging options + log_file "/var/log/mauveserver.log" + # 0 is debug, 1 is info, 2 is warning + log_level 1 + + # default is probably more than enough! + #transmission_id_expire_time 600 + + # few options for web interface, just the TCP port number + web_interface { + port 1288 + } +} + + +notification_method("email") { + # email address to send from + from "mauvealert@" + `hostname`.chomp + # smarthost + server "localhost" + # add this to the subject of any emails we send + subject_prefix "[mauve] " + + # for testing, enable this + #deliver_to_file "/tmp/alerts.txt" +} + +# How to log into a jabber server +# +# notification_method("xmpp") { +# jid "mauvealert@jabber.org/olympus" +# password "WojIsEv8ScaufOm1" +# } + +# How to notify by SMS - we use aql.com, you'll need to write a module +# to use any other provider. +# +# notification_method("sms") { +# provider "AQL" +# +# username "x" +# password "x" +# from "01234567890" +# +# # Maximum number of SMS messages to concatenate for one notification +# max_messages_per_alert 3 +# } + +# Simple default notification preference for root at this machine, at all +# alert levels. You probably want more people, see below for a more complete +# example. +# +# Passwords are generated like so: +# ruby-1.8.7-p72 > require 'digest/sha1' +# => true +# ruby-1.8.7-p72 > sha1 = Digest::SHA1.hexdigest('my new super secure and easy to type on phone password') +# => "729cf161621400fa63fcb3b4750441390fbface2" +# +person("root") { + all { email("root@localhost") } +} + +# Johnny is the go-to guy for everything on this network, poor Johnny! +# +# person("johnny") { +# +# # Johnny wants waking up 24/7 if anything urgent happens +# urgent { sms("07111222333") } +# +# # Email him for anything that's not urgent +# normal { email("johnny@example.com") } +# +# # Anything else can just be a jabber message, which he might miss. +# # Email instead if he's unavailable/offline - but give it a try if +# # we don't know his status. +# low { xmpp("johnny@example.com.jabber.org", :if_presence => [:available, unknown]) || email("johnny@example.com") } +# +# # SMS messages are expensive, if we're sending more than 5 per minute, +# # tell the user we're going to stop until it slows down. +# suppress_notifications_after 5 => 1.minute +# # In general, this is too many notifications for Johnny +# suppress_notifications_after 60 => 1.hour +# } + +# Archie is Johnny's boss +# +# person("archie") { +# all { email("archie@example.com") } +# +# # Don't spam Archie, if more than 3 messages per hour come in. +# suppress_notifications_after 3 => 1.hour +# } + +# Here is a group of alerts generated by the core routers. +# +# alert_group { +# level URGENT +# includes { source == "core-router" } +# +# # Johnny should get up and fix these errors very quickly, we will +# # bother him every 15 minutes until he does. +# # +# notify("johnny") { every 15.minutes } +# +# # Archie only wants to know about these alerts if they have gone +# # unacknowledged for a whole hour - Johnny must be slacking! Even +# # then he only needs to know during the working day. +# # +# notify("archie") { +# every 6.hours +# during { unacknowledged(1.hour); hours_in_day(9..17) } +# } +# } + +# Default notification - tell root about all alerts every hour +# +alert_group { + level NORMAL + notify("root") { every 1.hour } +} + diff --git a/static/alerts-mobil.css b/static/alerts-mobil.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/static/alerts-mobil.css diff --git a/static/alerts.css b/static/alerts.css new file mode 100644 index 0000000..09cd043 --- /dev/null +++ b/static/alerts.css @@ -0,0 +1,223 @@ +/* + * url http://www.w3schools.com/css/default.asp + * + * Light Mauve (Hex: #DCD0FF) (RGB: 220, 208, 255) + * Mauve (Mallow) (Hex: #E0B0FF) (RGB: 224, 176, 255) + * Opera Mauve (Hex: #CA82AF) (RGB: 202, 130, 175) + * Mauve Taupe (Hex: #915F6D) (RGB: 145, 95, 109) + * Old Mauve (Hex: #673147) (RGB: 103, 49, 71) + */ +body { + background-color: #777777; + font-family: Arial; +} + +h1 { + font-family: "Neo Tech Std", Arial; + font-weight: normal; + margin: 0; +} + +#header { + font-size: small; + background-color: #ffffff; + text-align: center; + background-image: url('/images/logo.png'); + background-repeat: no-repeat; + background-position: top right; + text-align: left; +} + +#errors { + font-size: larger; + background-color: #ffffff; + background-image: url(/images/broken_window.jpg); + background-repeat: no-repeat; +} + +#errors h1 { padding-left: 100px; color: #ff0000; } + + +/* lifted from http://matthewjamestaylor.com/blog/perfect-3-column.htm */ + +/* column container */ +.colmask { + position:relative; /* This fixes the IE7 overflow hidden bug */ + clear:both; + float:left; + width:100%; /* width of whole page */ + overflow:hidden; /* This chops off any overhanging divs */ + border-top: solid; + border-bottom: solid; +} +/* common column settings */ +.colright, +.colmid, +.colleft { + float:left; + width:100%; /* width of page */ + position:relative; +} +.col1, +.col2, +.col3 { + float:left; + position:relative; + padding:0 0 0 0; /* no left and right padding on columns, we just make them narrower instead + only padding top and bottom is included here, make it whatever value you need */ + overflow:hidden; +} +/* 3 Column settings */ +.threecol { +} +.threecol .colmid { + right:33%; /* width of the right column */ +} +.threecol .colleft { + right:34%; /* width of the middle column */ +} +.threecol .col1 { + font-family: "Neo Tech Std", Arial; + font-size: large; + width:34%; /* width of center column content (column width minus padding on either side) */ + left:100%; /* 100% plus left padding of center column */ +} +.threecol .col2 { + font-family: "Neo Tech Std", Arial; + font-size: large; + width:33%; /* Width of left column content (column width minus padding on either side) */ + left:33%; /* width of (right column) plus (center column left and right padding) plus (left column left padding) */ +} +.threecol .col3 { + font-family: "Neo Tech Std", Arial; + font-size: large; + width:33%; /* Width of right column content (column width minus padding on either side) */ + left:67%; /* Please make note of the brackets here: + (100% - left column width) plus (center column left and right padding) plus (left column left and right padding) plus (right column left padding) */ +} + +/* Navigation bar directly under alert counts */ +#navigation { + background-color: #ffffff; + text-align: right; + padding-bottom: 4px; + padding-right: 4px; +} +#navigation * { font-size: smaller; } +#navigation a { padding-left: 1em; padding-right: 1em; } +#navigation a:hover { background-color: #ff7f7f; } + +/* Styling only for alert counts - layout thankfully outsourced (see above) */ +#alert_counts { font-size: 8pt; } +#alert_counts * { text-align: center; } +#alert_counts .urgent { background-color: #ff2020; } +#alert_counts .normal { background-color: #ffff40; } +#alert_counts .low { background-color: #8080ff; } +#alert_counts * .unacknowledged { + font-family: "Neo Tech Std", Arial; + font-size: 24pt; + width: 100%; +} +#alert_counts * .acknowledged { + font-family: "Neo Tech Std", Arial; + font-size: 12pt; + width: 80%; + text-align: right; + padding-right: 1em; +} + +/* Style for each alert strip */ +#alert_strip { + width: 100%; + background-color: #808080; + padding: 2px; +} +#alert_summary .urgent .alert_strip { background-color: #ff2020; } +#alert_summary .urgent .alert_strip:hover { background-color: #ffa0a0; } +#alert_summary .normal .alert_strip { background-color: #ffff40; } +#alert_summary .normal .alert_strip:hover { background-color: #ffffa0; } +#alert_summary .low .alert_strip { background-color: #8080ff; } +#alert_summary .low .alert_strip:hover { background-color: #a0a0ff; } + +/* widgets along with each alert */ +.alert_strip { padding: 2px;} +.alert_strip * { display: inline; vertical-align: middle; } +.alert_strip .source { font-size: smaller; font-style: italic; } +.alert_strip .subject { font-size: smaller; font-weight: bold; padding-left:1em;} +.alert_strip .summary { font-size: larger; padding-left:1em; padding-right:1em;} +.alert_strip * img { border: none; } +.alert_whole { + padding-top:0em; + padding-bottom:0.25em; + padding-right:0em; + padding-left:0em; +} + + +/* the detail strip can contain arbitrary HTML */ +.detail_strip { + background-color: #eeeeee; + font-size: smaller; + left: 10em; + padding-left: 8.0em; +} + + +/* Not needed anymore. Was used for funky effect which is no longer needed. +img.unacknowledged { background: url(/images/acknowledge_unacknowledged.png); } +img.unacknowledged:hover { background: url(/images/acknowledge_hover.png); } +*/ + +.alert_full_list { display: table; width: 100%; background-color: #e0e0e0; padding: 0.5em; } +.alert_full_list .row { display: table-row; } +.alert_full_list .row.alt { background-color: #e0b0b0; } +.alert_full_list .row h2 { text-align: center; } +.alert_full_list .row * { display: table-cell; padding: 0.2em; } + +.raised.upraised { font-weight: bold; } +.cleared.upcleared { font-weight: bold; } +.acknowledged.upacknowledged { font-weight: bold; } + +#about_alert { + background-color: #ffffff; + padding: 1em; +} + +#about_alert .detail { background-color: #d0ffd0; padding: 0.3em; } + +#about_alert h1 { background-color: #ffd0d0; text-align: center; padding-top: 0.2em; } + +.dangerous { color: #ff0000; } + +.grouped_folder { + outline-color: #673147; + outline-style: solid; + outline-width: 0.15em; + padding-top:0em; + padding-bottom:0.0em; + padding-right:0em; + padding-left:0em; + margin-top:1.0em; +} + +img { + border: none; + vertical-align:middle; +} + +.control { + font-family: "Neo Tech Std", Arial; + color: #673147; + background-color: #aaaaaa; + padding-left: 6.5em; + padding-right: 0.5em; + font-size: smaller; + font-style: italic; + text-align: right; +} + +.error { + color: #ff0000; + font-family: "Neo Tech Std", Arial; + text-align: center; +} diff --git a/static/alerts2.css b/static/alerts2.css new file mode 100644 index 0000000..70cc640 --- /dev/null +++ b/static/alerts2.css @@ -0,0 +1,523 @@ +body { + background: #aaaaaa; +} + +a { + color: #222; + text-decoration: underline; +} + +.head h1 { + font-family: "Neo Tech Std", Arial; + font-weight: normal; + font-size: 1.6em; + padding: 0.7em 0 0.0 0.7em; + margin: 0 0 0 0; + background-color: #000; + color: #fff; +} + +h2 { + font-family: "Neo Tech Std", Arial; + font-weight: normal; + font-size: 1.4em; + padding: 0.7em 0 0.0em 0.7em; + margin: 0 0 0 0; +} + +h3 { + font-family: "Neo Tech Std", Arial; + font-weight: normal; + font-size: 1.3em; + padding: 0.7em 0 0.0em 0.7em; + margin: 0 0 0 0; + background-color: #666; +} + +h4 { + font-family: "Neo Tech Std", Arial; + font-weight: normal; + font-size: 1.2em; + padding: 0 0 0.1em 0.7em; + margin: 0 0 0 0; +} + +h5 { + font-family: "Neo Tech Std", Arial; + font-weight: normal; + font-size: 1.0em; + padding: 0 0 0.1em 0.1em; + margin: 0 0 0 0; +} + +.head { + margin: 0 0 1em 0; + width: 100%; +} + +.unacknowledged { + border: 2px solid black; + width: 100%; +} + +.acknowledge { + opacity: 0.65; + filter:alpha(opacity=65); /* IE */ + border: 2px solid black; + margin: 1em 0 1em 0; + width: 100%; +} + +/***************************************************************************** + * HEADER CODE + * For more details, see + * url http://matthewjamestaylor.com/blog/perfect-3-column.htm + *****************************************************************************/ +.nbrUnacknowledged { + font-family: "Neo Tech Std", Arial; + color: #333; + font-size: 1.0em; +} + +.nbrAcknowledged { + font-family: "Neo Tech Std", Arial; + color: #666; + font-size: 0.7em; +} + +/***************************************************************************** + * LAYOUT TECHNIQUES: 3 columns, all fluid + *****************************************************************************/ +#main { + border-top: 2px solid black; + border-bottom: 2px solid black; + border-right: 2px solid black; + height: 29px; + overflow:hidden; + background: #fff; +} +#mainleft { + width:16%; + float:left; + padding: 0.2px 0.2px 0.2px 0.2px; + background-color: #000; + color: white; + font-size: 1.75em; + font-family: "Neo Tech Std", Arial; +} +#maincenter { + width:60.5%; + float:left; + background:#fff; + padding: 0.2px 0.2px 0.2px 0.2px; +} +#mainright { + width:23.5%; + float:right; + background:#fff; + padding: 0.2px 0.2px 0.2px 0.2px; +} +.urgent { + font-family: "Neo Tech Std", Arial; + font-size: 1.3em; + color: #ff0000; + padding-right: 1em; +} +.normal { + font-family: "Neo Tech Std", Arial; + font-size: 1.3em; + color: #ff7000; + padding-right: 1em; +} +.low { + font-family: "Neo Tech Std", Arial; + color: #ccc000; + font-size: 1.3em; + padding-right: 1em; +} +.nbrUnacknowledged { + font-family: "Neo Tech Std", Arial; + color: #333; + font-size: 1.0em; +} +.nbrAcknowledged { + font-family: "Neo Tech Std", Arial; + color: #666; + font-size: 0.7em; +} + + + +/***************************************************************************** + * ALERT LIST CODE. + *****************************************************************************/ +.alert { + background-color: #999; + width: 100%; + /*white-space: normal; + */ +} + +.alert .subject { + font-size: 1.25em; + font-weight: bold; + display: inline; + padding-left: 0.5em; + /* + float: left; + width: 20%; + */ +} + +.alert .source { + display: inline; + padding-left: 0.5em; + /* + color: #444; + font-size: 0.75em; + float: left; + width: 20%; + */ +} + +.alert .summary { + font-size: 1.25em; + display: inline; + /* + float: left; + width: 20%; + */ +} + +.alert .time { + /* + color: #444; + font-size: 0.75em; + */ + display: inline; +} + +.alert .ackDetails { + font-size: 0.75em; + padding-left: 1em; + display: inline; +} + +.alert .detail { + display: inline; +} + +.levelUrgent, +.levelNormal, +.levelLow { + /* + float: left; + width: 3.75%; + */ + padding-left: .5em; + padding-right: .5em; + display: inline; +} + +.levelUrgent { + background-color: #ff0000; +} + +.levelNormal { + background-color: #ff7000; +} + +.levelLow { + background-color: #cccc00; +} + +.conglomerate { + background-color: #aaa; + display: inline; +} +.conglomerate .subject { + font-size: 1.25em; + font-weight: bold; + background-color: #aaa; + display: inline; +} + +.conglomerate .time { + font-size: 0.95em; + display: inline; +} + +.TitleUnacknowledged { + color: #ff0000; + display: inline; +} + +.inlineDetails { + padding: 0.0em 0.0em 0.0em 1.5em; + margin: 0 0 0 0; +} + +/***************************************************************************** + * TREEVIEW LIST CODE. + *****************************************************************************/ +.treeview, .treeview ul { + padding: 0; + margin: 0; + list-style: none; +} + +.treeview ul { + background-color: #aaa; + margin-top: 4px; +} + +.treeview .hitarea { + background: url(/images/treeview-default.png) -64px -25px no-repeat; + height: 16px; + width: 16px; + margin-left: -16px; + float: left; + cursor: pointer; +} +/* fix for IE6 */ +* html .hitarea { + display: inline; + float:none; +} + +.treeview li { + margin: 0; + padding: 3px 0pt 3px 16px; +} + +.treeview a.selected { + background-color: #eee; +} + +#treecontrol { + font-size: 0.75em; +} + +.treeview .hover { color: black; cursor: pointer; } + +.treeview li { background: url(/images/treeview-default-line.png) 0 0 no-repeat; } +.treeview li.collapsable, .treeview li.expandable { background-position: 0 -176px; } + +.treeview .expandable-hitarea { background-position: -80px -3px; } + +.treeview li.last { background-position: 0 -1766px } +.treeview li.lastCollapsable, .treeview li.lastExpandable { background-image: url(images/treeview-default.png); } +.treeview li.lastCollapsable { background-position: 0 -111px } +.treeview li.lastExpandable { background-position: -32px -67px } + +.treeview div.lastCollapsable-hitarea, .treeview div.lastExpandable-hitarea { background-position: 0; } + +.treeview-black li { background-image: url(images/treeview-black-line.png); } +.treeview-black .hitarea, .treeview-black li.lastCollapsable, .treeview-black li.lastExpandable { background-image: url(images/treeview-black.png); } + + +/***************************************************************************** + * POP! CODE. + *****************************************************************************/ +.pop { + position: absolute; + display: inline; +} +.pop .pop_menu { + display: none; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border: 2px solid #000; +} +.pop .pop_toggle { + /* + background-image: url(/images/arrow_down.png); + background-repeat: no-repeat; + background-position: center center; + */ + width: 7em; + height: 1em; + overflow: hidden; + cursor: pointer; +} +.active .pop_menu .title { + /* + border: 1px solid #000; + background-color: #999; + */ + font-family: "Neo Tech Std", Arial; + font-size: 1.2em; + text-align: center; +} +.active .pop_menu .content { + padding: 0.5em 0.5em 0.5em 0.5em; + border: 1px solid #000; + background-color: #ccc; + overflow: auto; +} +.active .pop_menu { + display: block; + background-color: #999; + color: #000; + position: absolute; + padding: .6em; + width: 33em; +} +.active .pop_toggle { + z-index: 2000; + position: absolute; + top: 0; + left: 0; + background-image: url(/images/arrow_up.png); + width: 20px; + height: 20px; +} + + +/***************************************************************************** + * jQUERY COUNTDOWN STYLES 1.5.8. + *****************************************************************************/ +.countdown { + display: inline; + float: right; +} + +.hasCountdown { + background-color: #fff; + color: #aaa; +} +.countdown_rtl { + direction: rtl; +} +.countdown_holding span { + background-color: #fff; +} +.countdown_row { + clear: both; + width: 10px; + padding: 0px 0px; + text-align: center; +} +.countdown_show1 .countdown_section { + width: 3em; +} +.countdown_show2 .countdown_section { + width: 3.5em; +} +.countdown_show3 .countdown_section { + width: 0%; +} +.countdown_show4 .countdown_section { + width: 0%; +} +.countdown_show5 .countdown_section { + width: 0%; +} +.countdown_show6 .countdown_section { + width: 0%; +} +.countdown_show7 .countdown_section { + width: 0%; +} +.countdown_section { + display: block; + float: left; + font-size: 0.6em; + text-align: center; +} +.countdown_amount { + font-size: 0.6em; +} +.countdown_descr { + display: block; + width: 10px; +} + +/***************************************************************************** + * CHANGE STATUS OF ALERTS -- aka ACKNOWLEDGE + *****************************************************************************/ +.darkMask { + position: absolute; + top: 0px; + left: 0px; + overflow: hidden; + display: none; + width: 100%; + height: 100%; + z-index: 10; + background-color: black; + opacity: 0.3; + filter:alpha(opacity=30); /* IE */ +} + +.updateAlertStatus { + display: none; + border: 2px solid black; + + position: absolute; + max-width: 12em; + width: 100%; + padding: 0.25em 0.25em 0.25em 0.25em; + + text-align: justify; + + background-color: white; + z-index: 100; +} + +.updateAlertStatus em { + font-size: 0.9em; + text-align: center; +} + +.raiseAlert:hover, +.raiseAlert, +.timeList:hover, +.timeList, +.trashAlert:hover, +.trashAlert, +.buttonAcknowledgeAlert:hover, +.buttonAcknowledgeAlert, +.timeList:hover, +.timeList { + width: 100%; + text-align: center; + padding: 0.5em 0.5em 0.5em 0.5em; +} +.trashAlert:hover, +.trashAlert { + font-weight: bold; +} +.trashAlert:hover { + background-color: black; + color: red; + border: 1px solid #f00; +} +.trashAlert { + background-color: red; + color: black; + border: 1px solid #000; +} + +/***************************************************************************** + * LOGIN PAGE + *****************************************************************************/ +.loginForm { + font-family: "Neo Tech Std", Arial; + border: 2px solid #000; + padding: 0.5em 0.0em 0.5em 0.5em; +} + +.loginNotes { + font-family: Arial; + font-weight: italic; + font-size: 0.8em; +} + +.submitLoginButton { + border: 1px solid #000; + font-family: "Neo Tech Std", Arial; + font-weight: normal; + font-size: 1.2em; + padding-top: 0.3em; +} diff --git a/static/datadumper.js b/static/datadumper.js new file mode 100644 index 0000000..9f90918 --- /dev/null +++ b/static/datadumper.js @@ -0,0 +1,204 @@ +/** + * Copyright (c)2005-2009 Matt Kruse (javascripttoolbox.com) + * + * Dual licensed under the MIT and GPL licenses. + * This basically means you can use this code however you want for + * free, but don't claim to have written it yourself! + * Donations always accepted: http://www.JavascriptToolbox.com/donate/ + * + * Please do not link to the .js files on javascripttoolbox.com from + * your site. Copy the files locally to your server instead. + * + */ +var Dumper = (function(){ + // "Private" + var maxIterations = 1000; + var maxDepth = -1; // Max depth that Dumper will traverse in object + var iterations = 0; + var indent = 1; + var indentText = " "; + var newline = "\n"; + var object = null; // Keeps track of the root object passed in + var properties = null; // Holds properties of top-level object to traverse - others are ignored + + function args(a,index) { + var myargs = new Array(); + for (var i=index; i<a.length; i++) { + myargs[myargs.length] = a[i]; + } + return myargs; + }; + + function pad(len) { + var ret = ""; + for (var i=0; i<len; i++) { + ret += indentText; + } + return ret; + }; + + function string(o) { + var level = 1; + var indentLevel = indent; + var ret = ""; + if (arguments.length>1 && typeof(arguments[1])=="number") { + level = arguments[1]; + indentLevel = arguments[2]; + if (o == object) { + return "[original object]"; + } + } + else { + iterations = 0; + object = o; + // If a list of properties are passed in + if (arguments.length>1) { + var list = arguments; + var listIndex = 1; + if (typeof(arguments[1])=="object") { + list = arguments[1]; + listIndex = 0; + } + for (var i=listIndex; i<list.length; i++) { + if (properties == null) { properties = new Object(); } + properties[list[i]]=1; + } + } + } + if (iterations++>maxIterations) { return "[Max Iterations Reached]"; } // Just in case, so the script doesn't hang + if (maxDepth != -1 && level > (maxDepth+1)) { + return "..."; + } + // undefined + if (typeof(o)=="undefined") { + return "[undefined]"; + } + // NULL + if (o==null) { + return "[null]"; + } + // DOM Object + if (o==window) { + return "[window]"; + } + if (o==window.document) { + return "[document]"; + } + // FUNCTION + if (typeof(o)=="function") { + return "[function]"; + } + // BOOLEAN + if (typeof(o)=="boolean") { + return (o)?"true":"false"; + } + // STRING + if (typeof(o)=="string") { + return "'" + o + "'"; + } + // NUMBER + if (typeof(o)=="number") { + return o; + } + if (typeof(o)=="object") { + if (typeof(o.length)=="number" ) { + // ARRAY + if (maxDepth != -1 && level > maxDepth) { + return "[ ... ]"; + } + ret = "["; + for (var i=0; i<o.length;i++) { + if (i>0) { + ret += "," + newline + pad(indentLevel); + } + else { + ret += newline + pad(indentLevel); + } + ret += string(o[i],level+1,indentLevel-0+indent); + } + if (i > 0) { + ret += newline + pad(indentLevel-indent); + } + ret += "]"; + return ret; + } + else { + // OBJECT + if (maxDepth != -1 && level > maxDepth) { + return "{ ... }"; + } + ret = "{"; + var count = 0; + for (i in o) { + if (o==object && properties!=null && properties[i]!=1) { + // do nothing with this node + } + else { + if (typeof(o[i]) != "unknown") { + var processAttribute = true; + // Check if this is a DOM object, and if so, if we have to limit properties to look at + if (o.ownerDocument|| o.tagName || (o.nodeType && o.nodeName)) { + processAttribute = false; + if (i=="tagName" || i=="nodeName" || i=="nodeType" || i=="id" || i=="className") { + processAttribute = true; + } + } + if (processAttribute) { + if (count++>0) { + ret += "," + newline + pad(indentLevel); + } + else { + ret += newline + pad(indentLevel); + } + ret += "'" + i + "' => " + string(o[i],level+1,indentLevel-0+i.length+6+indent); + } + } + } + } + if (count > 0) { + ret += newline + pad(indentLevel-indent); + } + ret += "}"; + return ret; + } + } + }; + + string.popup = function(o) { + var w = window.open("about:blank"); + w.document.open(); + w.document.writeln("<HTML><BODY><PRE>"); + w.document.writeln(string(o,args(arguments,1))); + w.document.writeln("</PRE></BODY></HTML>"); + w.document.close(); + }; + + string.alert = function(o) { + alert(string(o,args(arguments,1))); + }; + + string.write = function(o) { + var argumentsIndex = 1; + var d = document; + if (arguments.length>1 && arguments[1]==window.document) { + d = arguments[1]; + argumentsIndex = 2; + } + var temp = indentText; + indentText = " "; + d.write(string(o,args(arguments,argumentsIndex))); + indentText = temp; + }; + + string.setMaxIterations = function(i) { + maxIterations = i; + }; + + string.setMaxDepth = function(i) { + maxDepth = i; + }; + + string.$VERSION = 1.0; + + return string; +})(); diff --git a/static/images/acknowledge_acknowledged.png b/static/images/acknowledge_acknowledged.png Binary files differnew file mode 100644 index 0000000..c61e853 --- /dev/null +++ b/static/images/acknowledge_acknowledged.png diff --git a/static/images/acknowledge_acknowledged_hover.png b/static/images/acknowledge_acknowledged_hover.png Binary files differnew file mode 100644 index 0000000..c61e853 --- /dev/null +++ b/static/images/acknowledge_acknowledged_hover.png diff --git a/static/images/acknowledge_unacknowledged.png b/static/images/acknowledge_unacknowledged.png Binary files differnew file mode 100644 index 0000000..cd2c41c --- /dev/null +++ b/static/images/acknowledge_unacknowledged.png diff --git a/static/images/acknowledge_unacknowledged_hover.png b/static/images/acknowledge_unacknowledged_hover.png Binary files differnew file mode 100644 index 0000000..cd2c41c --- /dev/null +++ b/static/images/acknowledge_unacknowledged_hover.png diff --git a/static/images/alarm_alert_seen.xcf b/static/images/alarm_alert_seen.xcf Binary files differnew file mode 100644 index 0000000..d7f9c71 --- /dev/null +++ b/static/images/alarm_alert_seen.xcf diff --git a/static/images/arrow-down.gif b/static/images/arrow-down.gif Binary files differnew file mode 100644 index 0000000..6262672 --- /dev/null +++ b/static/images/arrow-down.gif diff --git a/static/images/arrow-up.gif b/static/images/arrow-up.gif Binary files differnew file mode 100644 index 0000000..4e897ca --- /dev/null +++ b/static/images/arrow-up.gif diff --git a/static/images/arrow_down.png b/static/images/arrow_down.png Binary files differnew file mode 100644 index 0000000..2c4e279 --- /dev/null +++ b/static/images/arrow_down.png diff --git a/static/images/arrow_up.png b/static/images/arrow_up.png Binary files differnew file mode 100644 index 0000000..1ebb193 --- /dev/null +++ b/static/images/arrow_up.png diff --git a/static/images/broken_window.jpg b/static/images/broken_window.jpg Binary files differnew file mode 100644 index 0000000..7436e40 --- /dev/null +++ b/static/images/broken_window.jpg diff --git a/static/images/detail_show.png b/static/images/detail_show.png Binary files differnew file mode 100644 index 0000000..7bc9233 --- /dev/null +++ b/static/images/detail_show.png diff --git a/static/images/error.png b/static/images/error.png Binary files differnew file mode 100644 index 0000000..628cf2d --- /dev/null +++ b/static/images/error.png diff --git a/static/images/hourglass.png b/static/images/hourglass.png Binary files differnew file mode 100644 index 0000000..57b03ce --- /dev/null +++ b/static/images/hourglass.png diff --git a/static/images/logo.png b/static/images/logo.png Binary files differnew file mode 100644 index 0000000..1ecbb0d --- /dev/null +++ b/static/images/logo.png diff --git a/static/images/minus.png b/static/images/minus.png Binary files differnew file mode 100644 index 0000000..a527596 --- /dev/null +++ b/static/images/minus.png diff --git a/static/images/plus.png b/static/images/plus.png Binary files differnew file mode 100644 index 0000000..0486e30 --- /dev/null +++ b/static/images/plus.png diff --git a/static/images/resize.png b/static/images/resize.png Binary files differnew file mode 100644 index 0000000..b5f302a --- /dev/null +++ b/static/images/resize.png diff --git a/static/images/toggle.png b/static/images/toggle.png Binary files differnew file mode 100644 index 0000000..9210455 --- /dev/null +++ b/static/images/toggle.png diff --git a/static/images/toggle_alerts.png b/static/images/toggle_alerts.png Binary files differnew file mode 100644 index 0000000..c04dbcd --- /dev/null +++ b/static/images/toggle_alerts.png diff --git a/static/images/treeview-black-line.png b/static/images/treeview-black-line.png Binary files differnew file mode 100644 index 0000000..874da97 --- /dev/null +++ b/static/images/treeview-black-line.png diff --git a/static/images/treeview-black.png b/static/images/treeview-black.png Binary files differnew file mode 100644 index 0000000..c84b194 --- /dev/null +++ b/static/images/treeview-black.png diff --git a/static/images/treeview-default-line.png b/static/images/treeview-default-line.png Binary files differnew file mode 100644 index 0000000..f86b784 --- /dev/null +++ b/static/images/treeview-default-line.png diff --git a/static/images/treeview-default.gif b/static/images/treeview-default.gif Binary files differnew file mode 100644 index 0000000..894cfa0 --- /dev/null +++ b/static/images/treeview-default.gif diff --git a/static/images/treeview-default.png b/static/images/treeview-default.png Binary files differnew file mode 100644 index 0000000..a27541e --- /dev/null +++ b/static/images/treeview-default.png diff --git a/static/images/zoom.png b/static/images/zoom.png Binary files differnew file mode 100644 index 0000000..908612e --- /dev/null +++ b/static/images/zoom.png diff --git a/static/jquery-1.4.2.min.js b/static/jquery-1.4.2.min.js new file mode 100644 index 0000000..7c24308 --- /dev/null +++ b/static/jquery-1.4.2.min.js @@ -0,0 +1,154 @@ +/*! + * jQuery JavaScript Library v1.4.2 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Sat Feb 13 22:33:48 2010 -0500 + */ +(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o<i;o++)e(a[o],b,f?d.call(a[o],o,e(a[o],b)):d,j);return a}return i? +e(a[0],b):w}function J(){return(new Date).getTime()}function Y(){return false}function Z(){return true}function na(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function oa(a){var b,d=[],f=[],e=arguments,j,i,o,k,n,r;i=c.data(this,"events");if(!(a.liveFired===this||!i||!i.live||a.button&&a.type==="click")){a.liveFired=this;var u=i.live.slice(0);for(k=0;k<u.length;k++){i=u[k];i.origType.replace(O,"")===a.type?f.push(i.selector):u.splice(k--,1)}j=c(a.target).closest(f,a.currentTarget);n=0;for(r= +j.length;n<r;n++)for(k=0;k<u.length;k++){i=u[k];if(j[n].selector===i.selector){o=j[n].elem;f=null;if(i.preType==="mouseenter"||i.preType==="mouseleave")f=c(a.relatedTarget).closest(i.selector)[0];if(!f||f!==o)d.push({elem:o,handleObj:i})}}n=0;for(r=d.length;n<r;n++){j=d[n];a.currentTarget=j.elem;a.data=j.handleObj.data;a.handleObj=j.handleObj;if(j.handleObj.origHandler.apply(j.elem,e)===false){b=false;break}}return b}}function pa(a,b){return"live."+(a&&a!=="*"?a+".":"")+b.replace(/\./g,"`").replace(/ /g, +"&")}function qa(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function ra(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var f=c.data(a[d++]),e=c.data(this,f);if(f=f&&f.events){delete e.handle;e.events={};for(var j in f)for(var i in f[j])c.event.add(this,j,f[j][i],f[j][i].data)}}})}function sa(a,b,d){var f,e,j;b=b&&b[0]?b[0].ownerDocument||b[0]:s;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===s&&!ta.test(a[0])&&(c.support.checkClone||!ua.test(a[0]))){e= +true;if(j=c.fragments[a[0]])if(j!==1)f=j}if(!f){f=b.createDocumentFragment();c.clean(a,b,f,d)}if(e)c.fragments[a[0]]=j?f:1;return{fragment:f,cacheable:e}}function K(a,b){var d={};c.each(va.concat.apply([],va.slice(0,b)),function(){d[this]=a});return d}function wa(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var c=function(a,b){return new c.fn.init(a,b)},Ra=A.jQuery,Sa=A.$,s=A.document,T,Ta=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/, +Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&& +(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this, +a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b=== +"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this, +function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(j in e){i=a[j];o=e[j];if(a!==o)if(f&&o&&(c.isPlainObject(o)||c.isArray(o))){i=i&&(c.isPlainObject(i)|| +c.isArray(i))?i:c.isArray(o)?[]:{};a[j]=c.extend(f,i,o)}else if(o!==w)a[j]=o}return a};c.extend({noConflict:function(a){A.$=Sa;if(a)A.jQuery=Ra;return c},isReady:false,ready:function(){if(!c.isReady){if(!s.body)return setTimeout(c.ready,13);c.isReady=true;if(Q){for(var a,b=0;a=Q[b++];)a.call(s,c);Q=null}c.fn.triggerHandler&&c(s).triggerHandler("ready")}},bindReady:function(){if(!xa){xa=true;if(s.readyState==="complete")return c.ready();if(s.addEventListener){s.addEventListener("DOMContentLoaded", +L,false);A.addEventListener("load",c.ready,false)}else if(s.attachEvent){s.attachEvent("onreadystatechange",L);A.attachEvent("onload",c.ready);var a=false;try{a=A.frameElement==null}catch(b){}s.documentElement.doScroll&&a&&ma()}}},isFunction:function(a){return $.call(a)==="[object Function]"},isArray:function(a){return $.call(a)==="[object Array]"},isPlainObject:function(a){if(!a||$.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!aa.call(a,"constructor")&&!aa.call(a.constructor.prototype, +"isPrototypeOf"))return false;var b;for(b in a);return b===w||aa.call(a,b)},isEmptyObject:function(a){for(var b in a)return false;return true},error:function(a){throw a;},parseJSON:function(a){if(typeof a!=="string"||!a)return null;a=c.trim(a);if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return A.JSON&&A.JSON.parse?A.JSON.parse(a):(new Function("return "+ +a))();else c.error("Invalid JSON: "+a)},noop:function(){},globalEval:function(a){if(a&&Va.test(a)){var b=s.getElementsByTagName("head")[0]||s.documentElement,d=s.createElement("script");d.type="text/javascript";if(c.support.scriptEval)d.appendChild(s.createTextNode(a));else d.text=a;b.insertBefore(d,b.firstChild);b.removeChild(d)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,b,d){var f,e=0,j=a.length,i=j===w||c.isFunction(a);if(d)if(i)for(f in a){if(b.apply(a[f], +d)===false)break}else for(;e<j;){if(b.apply(a[e++],d)===false)break}else if(i)for(f in a){if(b.call(a[f],f,a[f])===false)break}else for(d=a[0];e<j&&b.call(d,e,d)!==false;d=a[++e]);return a},trim:function(a){return(a||"").replace(Wa,"")},makeArray:function(a,b){b=b||[];if(a!=null)a.length==null||typeof a==="string"||c.isFunction(a)||typeof a!=="function"&&a.setInterval?ba.call(b,a):c.merge(b,a);return b},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var d=0,f=b.length;d<f;d++)if(b[d]=== +a)return d;return-1},merge:function(a,b){var d=a.length,f=0;if(typeof b.length==="number")for(var e=b.length;f<e;f++)a[d++]=b[f];else for(;b[f]!==w;)a[d++]=b[f++];a.length=d;return a},grep:function(a,b,d){for(var f=[],e=0,j=a.length;e<j;e++)!d!==!b(a[e],e)&&f.push(a[e]);return f},map:function(a,b,d){for(var f=[],e,j=0,i=a.length;j<i;j++){e=b(a[j],j,d);if(e!=null)f[f.length]=e}return f.concat.apply([],f)},guid:1,proxy:function(a,b,d){if(arguments.length===2)if(typeof b==="string"){d=a;a=d[b];b=w}else if(b&& +!c.isFunction(b)){d=b;b=w}if(!b&&a)b=function(){return a.apply(d||this,arguments)};if(a)b.guid=a.guid=a.guid||b.guid||c.guid++;return b},uaMatch:function(a){a=a.toLowerCase();a=/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||!/compatible/.test(a)&&/(mozilla)(?:.*? rv:([\w.]+))?/.exec(a)||[];return{browser:a[1]||"",version:a[2]||"0"}},browser:{}});P=c.uaMatch(P);if(P.browser){c.browser[P.browser]=true;c.browser.version=P.version}if(c.browser.webkit)c.browser.safari= +true;if(ya)c.inArray=function(a,b){return ya.call(b,a)};T=c(s);if(s.addEventListener)L=function(){s.removeEventListener("DOMContentLoaded",L,false);c.ready()};else if(s.attachEvent)L=function(){if(s.readyState==="complete"){s.detachEvent("onreadystatechange",L);c.ready()}};(function(){c.support={};var a=s.documentElement,b=s.createElement("script"),d=s.createElement("div"),f="script"+J();d.style.display="none";d.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>"; +var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected, +parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent= +false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n= +s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true, +applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando]; +else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this, +a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b=== +w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i, +cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1)if(e.className){for(var j=" "+e.className+" ", +i=e.className,o=0,k=b.length;o<k;o++)if(j.indexOf(" "+b[o]+" ")<0)i+=" "+b[o];e.className=c.trim(i)}else e.className=a}return this},removeClass:function(a){if(c.isFunction(a))return this.each(function(k){var n=c(this);n.removeClass(a.call(this,k,n.attr("class")))});if(a&&typeof a==="string"||a===w)for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1&&e.className)if(a){for(var j=(" "+e.className+" ").replace(Aa," "),i=0,o=b.length;i<o;i++)j=j.replace(" "+b[i]+" ", +" ");e.className=c.trim(j)}else e.className=""}return this},toggleClass:function(a,b){var d=typeof a,f=typeof b==="boolean";if(c.isFunction(a))return this.each(function(e){var j=c(this);j.toggleClass(a.call(this,e,j.attr("class"),b),b)});return this.each(function(){if(d==="string")for(var e,j=0,i=c(this),o=b,k=a.split(ca);e=k[j++];){o=f?o:!i.hasClass(e);i[o?"addClass":"removeClass"](e)}else if(d==="undefined"||d==="boolean"){this.className&&c.data(this,"__className__",this.className);this.className= +this.className||a===false?"":c.data(this,"__className__")||""}})},hasClass:function(a){a=" "+a+" ";for(var b=0,d=this.length;b<d;b++)if((" "+this[b].className+" ").replace(Aa," ").indexOf(a)>-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j<d;j++){var i= +e[j];if(i.selected){a=c(i).val();if(b)return a;f.push(a)}}return f}if(Ba.test(b.type)&&!c.support.checkOn)return b.getAttribute("value")===null?"on":b.value;return(b.value||"").replace(Za,"")}return w}var o=c.isFunction(a);return this.each(function(k){var n=c(this),r=a;if(this.nodeType===1){if(o)r=a.call(this,k,n.val());if(typeof r==="number")r+="";if(c.isArray(r)&&Ba.test(this.type))this.checked=c.inArray(n.val(),r)>=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected= +c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); +a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g, +function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split("."); +k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a), +C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B<r.length;B++){u=r[B];if(d.guid===u.guid){if(i||k.test(u.namespace)){f==null&&r.splice(B--,1);n.remove&&n.remove.call(a,u)}if(f!= +null)break}}if(r.length===0||f!=null&&r.length===1){if(!n.teardown||n.teardown.call(a,o)===false)Ca(a,e,z.handle);delete C[e]}}else for(var B=0;B<r.length;B++){u=r[B];if(i||k.test(u.namespace)){c.event.remove(a,n,u.handler,B);r.splice(B--,1)}}}if(c.isEmptyObject(C)){if(b=z.handle)b.elem=null;delete z.events;delete z.handle;c.isEmptyObject(z)&&c.removeData(a)}}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[G]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type= +e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&& +f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive; +if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e<j;e++){var i=d[e];if(b||f.test(i.namespace)){a.handler=i.handler;a.data=i.data;a.handleObj=i;i=i.handler.apply(this,arguments);if(i!==w){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}}return a.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), +fix:function(a){if(a[G])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement||s;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=s.documentElement;d=s.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop|| +d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&&a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==w)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a){c.event.add(this,a.origType,c.extend({},a,{handler:oa}))},remove:function(a){var b=true,d=a.origType.replace(O,"");c.each(c.data(this, +"events").live||[],function(){if(d===this.origType.replace(O,""))return b=false});b&&c.event.remove(this,a.origType,oa)}},beforeunload:{setup:function(a,b,d){if(this.setInterval)this.onbeforeunload=d;return false},teardown:function(a,b){if(this.onbeforeunload===b)this.onbeforeunload=null}}}};var Ca=s.removeEventListener?function(a,b,d){a.removeEventListener(b,d,false)}:function(a,b,d){a.detachEvent("on"+b,d)};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent= +a;this.type=a.type}else this.type=a;this.timeStamp=J();this[G]=true};c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=Z;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped=Z;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=Z;this.stopPropagation()},isDefaultPrevented:Y,isPropagationStopped:Y, +isImmediatePropagationStopped:Y};var Da=function(a){var b=a.relatedTarget;try{for(;b&&b!==this;)b=b.parentNode;if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}}catch(d){}},Ea=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ea:Da,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ea:Da)}}});if(!c.support.submitBubbles)c.event.special.submit= +{setup:function(){if(this.nodeName.toLowerCase()!=="form"){c.event.add(this,"click.specialSubmit",function(a){var b=a.target,d=b.type;if((d==="submit"||d==="image")&&c(b).closest("form").length)return na("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,d=b.type;if((d==="text"||d==="password")&&c(b).closest("form").length&&a.keyCode===13)return na("submit",this,arguments)})}else return false},teardown:function(){c.event.remove(this,".specialSubmit")}}; +if(!c.support.changeBubbles){var da=/textarea|input|select/i,ea,Fa=function(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex>-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data", +e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a, +"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a, +d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j<o;j++)c.event.add(this[j],d,i,f)}return this}});c.fn.extend({unbind:function(a,b){if(typeof a==="object"&& +!a.preventDefault)for(var d in a)this.unbind(d,a[d]);else{d=0;for(var f=this.length;d<f;d++)c.event.remove(this[d],a,b)}return this},delegate:function(a,b,d,f){return this.live(b,d,f,a)},undelegate:function(a,b,d){return arguments.length===0?this.unbind("live"):this.die(b,null,d,a)},trigger:function(a,b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}}, +toggle:function(a){for(var b=arguments,d=1;d<b.length;)c.proxy(a,b[d++]);return this.click(c.proxy(a,function(f){var e=(c.data(this,"lastToggle"+a.guid)||0)%d;c.data(this,"lastToggle"+a.guid,e+1);f.preventDefault();return b[e].apply(this,arguments)||false}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var Ga={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};c.each(["live","die"],function(a,b){c.fn[b]=function(d,f,e,j){var i,o=0,k,n,r=j||this.selector, +u=j?this:c(this.context);if(c.isFunction(f)){e=f;f=w}for(d=(d||"").split(" ");(i=d[o++])!=null;){j=O.exec(i);k="";if(j){k=j[0];i=i.replace(O,"")}if(i==="hover")d.push("mouseenter"+k,"mouseleave"+k);else{n=i;if(i==="focus"||i==="blur"){d.push(Ga[i]+k);i+=k}else i=(Ga[i]||i)+k;b==="live"?u.each(function(){c.event.add(this,pa(i,r),{data:f,selector:r,handler:e,origType:i,origHandler:e,preType:n})}):u.unbind(pa(i,r),e)}}return this}});c.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "), +function(a,b){c.fn[b]=function(d){return d?this.bind(b,d):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});A.attachEvent&&!A.addEventListener&&A.attachEvent("onunload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}});(function(){function a(g){for(var h="",l,m=0;g[m];m++){l=g[m];if(l.nodeType===3||l.nodeType===4)h+=l.nodeValue;else if(l.nodeType!==8)h+=a(l.childNodes)}return h}function b(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q]; +if(t){t=t[g];for(var y=false;t;){if(t.sizcache===l){y=m[t.sizset];break}if(t.nodeType===1&&!p){t.sizcache=l;t.sizset=q}if(t.nodeName.toLowerCase()===h){y=t;break}t=t[g]}m[q]=y}}}function d(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===l){y=m[t.sizset];break}if(t.nodeType===1){if(!p){t.sizcache=l;t.sizset=q}if(typeof h!=="string"){if(t===h){y=true;break}}else if(k.filter(h,[t]).length>0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, +e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift(); +t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D|| +g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h<g.length;h++)g[h]===g[h-1]&&g.splice(h--,1)}return g};k.matches=function(g,h){return k(g,null,null,h)};k.find=function(g,h,l){var m,q;if(!g)return[]; +for(var p=0,v=n.order.length;p<v;p++){var t=n.order[p];if(q=n.leftMatch[t].exec(g)){var y=q[1];q.splice(1,1);if(y.substr(y.length-1)!=="\\"){q[1]=(q[1]||"").replace(/\\/g,"");m=n.find[t](q,h,l);if(m!=null){g=g.replace(n.match[t],"");break}}}}m||(m=h.getElementsByTagName("*"));return{set:m,expr:g}};k.filter=function(g,h,l,m){for(var q=g,p=[],v=h,t,y,S=h&&h[0]&&x(h[0]);g&&h.length;){for(var H in n.filter)if((t=n.leftMatch[H].exec(g))!=null&&t[2]){var M=n.filter[H],I,D;D=t[1];y=false;t.splice(1,1);if(D.substr(D.length- +1)!=="\\"){if(v===p)p=[];if(n.preFilter[H])if(t=n.preFilter[H](t,v,l,p,m,S)){if(t===true)continue}else y=I=true;if(t)for(var U=0;(D=v[U])!=null;U++)if(D){I=M(D,t,U,v);var Ha=m^!!I;if(l&&I!=null)if(Ha)y=true;else v[U]=false;else if(Ha){p.push(D);y=true}}if(I!==w){l||(v=p);g=g.replace(n.match[H],"");if(!y)return[];break}}}if(g===q)if(y==null)k.error(g);else break;q=g}return v};k.error=function(g){throw"Syntax error, unrecognized expression: "+g;};var n=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF-]|\\.)+)/, +CLASS:/\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(g){return g.getAttribute("href")}}, +relative:{"+":function(g,h){var l=typeof h==="string",m=l&&!/\W/.test(h);l=l&&!m;if(m)h=h.toLowerCase();m=0;for(var q=g.length,p;m<q;m++)if(p=g[m]){for(;(p=p.previousSibling)&&p.nodeType!==1;);g[m]=l||p&&p.nodeName.toLowerCase()===h?p||false:p===h}l&&k.filter(h,g,true)},">":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m<q;m++){var p=g[m];if(p){l=p.parentNode;g[m]=l.nodeName.toLowerCase()===h?l:false}}}else{m=0;for(q=g.length;m<q;m++)if(p=g[m])g[m]= +l?p.parentNode:p.parentNode===h;l&&k.filter(h,g,true)}},"":function(g,h,l){var m=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("parentNode",h,m,g,p,l)},"~":function(g,h,l){var m=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("previousSibling",h,m,g,p,l)}},find:{ID:function(g,h,l){if(typeof h.getElementById!=="undefined"&&!l)return(g=h.getElementById(g[1]))?[g]:[]},NAME:function(g,h){if(typeof h.getElementsByName!=="undefined"){var l=[]; +h=h.getElementsByName(g[1]);for(var m=0,q=h.length;m<q;m++)h[m].getAttribute("name")===g[1]&&l.push(h[m]);return l.length===0?null:l}},TAG:function(g,h){return h.getElementsByTagName(g[1])}},preFilter:{CLASS:function(g,h,l,m,q,p){g=" "+g[1].replace(/\\/g,"")+" ";if(p)return g;p=0;for(var v;(v=h[p])!=null;p++)if(v)if(q^(v.className&&(" "+v.className+" ").replace(/[\t\n]/g," ").indexOf(g)>=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()}, +CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m, +g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)}, +text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}}, +setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return h<l[3]-0},gt:function(g,h,l){return h>l[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h= +h[3];l=0;for(m=h.length;l<m;l++)if(h[l]===g)return false;return true}else k.error("Syntax error, unrecognized expression: "+q)},CHILD:function(g,h){var l=h[1],m=g;switch(l){case "only":case "first":for(;m=m.previousSibling;)if(m.nodeType===1)return false;if(l==="first")return true;m=g;case "last":for(;m=m.nextSibling;)if(m.nodeType===1)return false;return true;case "nth":l=h[2];var q=h[3];if(l===1&&q===0)return true;h=h[0];var p=g.parentNode;if(p&&(p.sizcache!==h||!g.nodeIndex)){var v=0;for(m=p.firstChild;m;m= +m.nextSibling)if(m.nodeType===1)m.nodeIndex=++v;p.sizcache=h}g=g.nodeIndex-q;return l===0?g===0:g%l===0&&g/l>=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m=== +"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g, +h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l<m;l++)h.push(g[l]);else for(l=0;g[l];l++)h.push(g[l]);return h}}var B;if(s.documentElement.compareDocumentPosition)B=function(g,h){if(!g.compareDocumentPosition|| +!h.compareDocumentPosition){if(g==h)i=true;return g.compareDocumentPosition?-1:1}g=g.compareDocumentPosition(h)&4?-1:g===h?0:1;if(g===0)i=true;return g};else if("sourceIndex"in s.documentElement)B=function(g,h){if(!g.sourceIndex||!h.sourceIndex){if(g==h)i=true;return g.sourceIndex?-1:1}g=g.sourceIndex-h.sourceIndex;if(g===0)i=true;return g};else if(s.createRange)B=function(g,h){if(!g.ownerDocument||!h.ownerDocument){if(g==h)i=true;return g.ownerDocument?-1:1}var l=g.ownerDocument.createRange(),m= +h.ownerDocument.createRange();l.setStart(g,0);l.setEnd(g,0);m.setStart(h,0);m.setEnd(h,0);g=l.compareBoundaryPoints(Range.START_TO_END,m);if(g===0)i=true;return g};(function(){var g=s.createElement("div"),h="script"+(new Date).getTime();g.innerHTML="<a name='"+h+"'/>";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&& +q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML="<a href='#'></a>"; +if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="<p class='TEST'></p>";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}(); +(function(){var g=s.createElement("div");g.innerHTML="<div class='test e'></div><div class='test'></div>";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}: +function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q<p;q++)k(g,h[q],l);return k.filter(m,l)};c.find=k;c.expr=k.selectors;c.expr[":"]=c.expr.filters;c.unique=k.uniqueSort;c.text=a;c.isXMLDoc=x;c.contains=E})();var eb=/Until$/,fb=/^(?:parents|prevUntil|prevAll)/, +gb=/,/;R=Array.prototype.slice;var Ia=function(a,b,d){if(c.isFunction(b))return c.grep(a,function(e,j){return!!b.call(e,j,e)===d});else if(b.nodeType)return c.grep(a,function(e){return e===b===d});else if(typeof b==="string"){var f=c.grep(a,function(e){return e.nodeType===1});if(Ua.test(b))return c.filter(b,f,!d);else b=c.filter(b,f)}return c.grep(a,function(e){return c.inArray(e,b)>=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f<e;f++){d=b.length; +c.find(a,this[f],b);if(f>0)for(var j=d;j<b.length;j++)for(var i=0;i<d;i++)if(b[i]===b[j]){b.splice(j--,1);break}}return b},has:function(a){var b=c(a);return this.filter(function(){for(var d=0,f=b.length;d<f;d++)if(c.contains(this,b[d]))return true})},not:function(a){return this.pushStack(Ia(this,a,false),"not",a)},filter:function(a){return this.pushStack(Ia(this,a,true),"filter",a)},is:function(a){return!!a&&c.filter(a,this).length>0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j= +{},i;if(f&&a.length){e=0;for(var o=a.length;e<o;e++){i=a[e];j[i]||(j[i]=c.expr.match.POS.test(i)?c(i,b||this.context):i)}for(;f&&f.ownerDocument&&f!==b;){for(i in j){e=j[i];if(e.jquery?e.index(f)>-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a=== +"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode", +d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")? +a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType=== +1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/<tbody/i,jb=/<|&#?\w+;/,ta=/<script|<object|<embed|<option|<style/i,ua=/checked\s*(?:[^=]|=\s*.checked.)/i,Ma=function(a,b,d){return hb.test(d)? +a:b+"></"+d+">"},F={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div<div>","</div>"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= +c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, +wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, +prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, +this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); +return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja, +""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b<d;b++)if(this[b].nodeType===1){c.cleanData(this[b].getElementsByTagName("*"));this[b].innerHTML=a}}catch(f){this.empty().append(a)}}else c.isFunction(a)?this.each(function(e){var j=c(this),i=j.html();j.empty().append(function(){return a.call(this,e,i)})}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&& +this[0].parentNode){if(c.isFunction(a))return this.each(function(b){var d=c(this),f=d.html();d.replaceWith(a.call(this,b,f))});if(typeof a!=="string")a=c(a).detach();return this.each(function(){var b=this.nextSibling,d=this.parentNode;c(this).remove();b?c(b).before(a):c(d).append(a)})}else return this.pushStack(c(c.isFunction(a)?a():a),"replaceWith",a)},detach:function(a){return this.remove(a,true)},domManip:function(a,b,d){function f(u){return c.nodeName(u,"table")?u.getElementsByTagName("tbody")[0]|| +u.appendChild(u.ownerDocument.createElement("tbody")):u}var e,j,i=a[0],o=[],k;if(!c.support.checkClone&&arguments.length===3&&typeof i==="string"&&ua.test(i))return this.each(function(){c(this).domManip(a,b,d,true)});if(c.isFunction(i))return this.each(function(u){var z=c(this);a[0]=i.call(this,u,b?z.html():w);z.domManip(a,b,d)});if(this[0]){e=i&&i.parentNode;e=c.support.parentNode&&e&&e.nodeType===11&&e.childNodes.length===this.length?{fragment:e}:sa(a,this,o);k=e.fragment;if(j=k.childNodes.length=== +1?(k=k.firstChild):k.firstChild){b=b&&c.nodeName(j,"tr");for(var n=0,r=this.length;n<r;n++)d.call(b?f(this[n],j):this[n],n>0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]); +return this}else{e=0;for(var j=d.length;e<j;e++){var i=(e>0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["", +""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]==="<table>"&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e= +c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]? +c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja= +function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter= +Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a, +"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f= +a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b= +a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=/<script(.|\s)*?\/script>/gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!== +"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("<div />").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this}, +serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), +function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href, +global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&& +e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)? +"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache=== +false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B= +false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since", +c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E|| +d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x); +g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status=== +1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b=== +"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional; +if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay"); +this[a].style.display=d||"";if(c.css(this[a],"display")==="none"){d=this[a].nodeName;var f;if(la[d])f=la[d];else{var e=c("<"+d+" />").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a<b;a++)this[a].style.display=c.data(this[a],"olddisplay")||"";return this}},hide:function(a,b){if(a||a===0)return this.animate(K("hide",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");!d&&d!=="none"&&c.data(this[a], +"olddisplay",c.css(this[a],"display"))}a=0;for(b=this.length;a<b;a++)this[a].style.display="none";return this}},_toggle:c.fn.toggle,toggle:function(a,b){var d=typeof a==="boolean";if(c.isFunction(a)&&c.isFunction(b))this._toggle.apply(this,arguments);else a==null||d?this.each(function(){var f=d?a:c(this).is(":hidden");c(this)[f?"show":"hide"]()}):this.animate(K("toggle",3),a,b);return this},fadeTo:function(a,b,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,d)}, +animate:function(a,b,d,f){var e=c.speed(b,d,f);if(c.isEmptyObject(a))return this.each(e.complete);return this[e.queue===false?"each":"queue"](function(){var j=c.extend({},e),i,o=this.nodeType===1&&c(this).is(":hidden"),k=this;for(i in a){var n=i.replace(ia,ja);if(i!==n){a[n]=a[i];delete a[i];i=n}if(a[i]==="hide"&&o||a[i]==="show"&&!o)return j.complete.call(this);if((i==="height"||i==="width")&&this.style){j.display=c.css(this,"display");j.overflow=this.style.overflow}if(c.isArray(a[i])){(j.specialEasing= +j.specialEasing||{})[i]=a[i][1];a[i]=a[i][0]}}if(j.overflow!=null)this.style.overflow="hidden";j.curAnim=c.extend({},a);c.each(a,function(r,u){var z=new c.fx(k,j,r);if(Ab.test(u))z[u==="toggle"?o?"show":"hide":u](a);else{var C=Bb.exec(u),B=z.cur(true)||0;if(C){u=parseFloat(C[2]);var E=C[3]||"px";if(E!=="px"){k.style[r]=(u||1)+E;B=(u||1)/z.cur(true)*B;k.style[r]=B+E}if(C[1])u=(C[1]==="-="?-1:1)*u+B;z.custom(B,u,E)}else z.custom(B,u,"")}});return true})},stop:function(a,b){var d=c.timers;a&&this.queue([]); +this.each(function(){for(var f=d.length-1;f>=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration=== +"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]|| +c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start; +this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now= +this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem, +e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length|| +c.fx.stop()},stop:function(){clearInterval(W);W=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){c.style(a.elem,"opacity",a.now)},_default:function(a){if(a.elem.style&&a.elem.style[a.prop]!=null)a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit;else a.elem[a.prop]=a.now}}});if(c.expr&&c.expr.filters)c.expr.filters.animated=function(a){return c.grep(c.timers,function(b){return a===b.elem}).length};c.fn.offset="getBoundingClientRect"in s.documentElement? +function(a){var b=this[0];if(a)return this.each(function(e){c.offset.setOffset(this,a,e)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);var d=b.getBoundingClientRect(),f=b.ownerDocument;b=f.body;f=f.documentElement;return{top:d.top+(self.pageYOffset||c.support.boxModel&&f.scrollTop||b.scrollTop)-(f.clientTop||b.clientTop||0),left:d.left+(self.pageXOffset||c.support.boxModel&&f.scrollLeft||b.scrollLeft)-(f.clientLeft||b.clientLeft||0)}}:function(a){var b= +this[0];if(a)return this.each(function(r){c.offset.setOffset(this,a,r)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);c.offset.initialize();var d=b.offsetParent,f=b,e=b.ownerDocument,j,i=e.documentElement,o=e.body;f=(e=e.defaultView)?e.getComputedStyle(b,null):b.currentStyle;for(var k=b.offsetTop,n=b.offsetLeft;(b=b.parentNode)&&b!==o&&b!==i;){if(c.offset.supportsFixedPosition&&f.position==="fixed")break;j=e?e.getComputedStyle(b,null):b.currentStyle; +k-=b.scrollTop;n-=b.scrollLeft;if(b===d){k+=b.offsetTop;n+=b.offsetLeft;if(c.offset.doesNotAddBorder&&!(c.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(b.nodeName))){k+=parseFloat(j.borderTopWidth)||0;n+=parseFloat(j.borderLeftWidth)||0}f=d;d=b.offsetParent}if(c.offset.subtractsBorderForOverflowNotVisible&&j.overflow!=="visible"){k+=parseFloat(j.borderTopWidth)||0;n+=parseFloat(j.borderLeftWidth)||0}f=j}if(f.position==="relative"||f.position==="static"){k+=o.offsetTop;n+=o.offsetLeft}if(c.offset.supportsFixedPosition&& +f.position==="fixed"){k+=Math.max(i.scrollTop,o.scrollTop);n+=Math.max(i.scrollLeft,o.scrollLeft)}return{top:k,left:n}};c.offset={initialize:function(){var a=s.body,b=s.createElement("div"),d,f,e,j=parseFloat(c.curCSS(a,"marginTop",true))||0;c.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"});b.innerHTML="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>"; +a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b); +c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a, +d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top- +f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset": +"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in +e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window); diff --git a/static/jquery.cookie.js b/static/jquery.cookie.js new file mode 100644 index 0000000..8e8e1d9 --- /dev/null +++ b/static/jquery.cookie.js @@ -0,0 +1,92 @@ +/** + * Cookie plugin + * + * Copyright (c) 2006 Klaus Hartl (stilbuero.de) + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ + +/** + * Create a cookie with the given name and value and other optional parameters. + * + * @example $.cookie('the_cookie', 'the_value'); + * @desc Set the value of a cookie. + * @example $.cookie('the_cookie', 'the_value', {expires: 7, path: '/', domain: 'jquery.com', secure: true}); + * @desc Create a cookie with all available options. + * @example $.cookie('the_cookie', 'the_value'); + * @desc Create a session cookie. + * @example $.cookie('the_cookie', null); + * @desc Delete a cookie by passing null as value. + * + * @param String name The name of the cookie. + * @param String value The value of the cookie. + * @param Object options An object literal containing key/value pairs to provide optional cookie attributes. + * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object. + * If a negative value is specified (e.g. a date in the past), the cookie will be deleted. + * If set to null or omitted, the cookie will be a session cookie and will not be retained + * when the the browser exits. + * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie). + * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie). + * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will + * require a secure protocol (like HTTPS). + * @type undefined + * + * @name $.cookie + * @cat Plugins/Cookie + * @author Klaus Hartl/klaus.hartl@stilbuero.de + */ + +/** + * Get the value of a cookie with the given name. + * + * @example $.cookie('the_cookie'); + * @desc Get the value of a cookie. + * + * @param String name The name of the cookie. + * @return The value of the cookie. + * @type String + * + * @name $.cookie + * @cat Plugins/Cookie + * @author Klaus Hartl/klaus.hartl@stilbuero.de + */ +jQuery.cookie = function(name, value, options) { + if (typeof value != 'undefined') { // name and value given, set cookie + options = options || {}; + if (value === null) { + value = ''; + options.expires = -1; + } + var expires = ''; + if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { + var date; + if (typeof options.expires == 'number') { + date = new Date(); + date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); + } else { + date = options.expires; + } + expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE + } + var path = options.path ? '; path=' + options.path : ''; + var domain = options.domain ? '; domain=' + options.domain : ''; + var secure = options.secure ? '; secure' : ''; + document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); + } else { // only name given, get cookie + var cookieValue = null; + if (document.cookie && document.cookie != '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) == (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } +};
\ No newline at end of file diff --git a/static/jquery.countdown.js b/static/jquery.countdown.js new file mode 100644 index 0000000..27e2f4a --- /dev/null +++ b/static/jquery.countdown.js @@ -0,0 +1,759 @@ +/* http://keith-wood.name/countdown.html + Countdown for jQuery v1.5.8. + Written by Keith Wood (kbwood{at}iinet.com.au) January 2008. + Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and + MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. + Please attribute the author if you use it. */ + +/* Display a countdown timer. + Attach it with options like: + $('div selector').countdown( + {until: new Date(2009, 1 - 1, 1, 0, 0, 0), onExpiry: happyNewYear}); */ + +(function($) { // Hide scope, no $ conflict + +/* Countdown manager. */ +function Countdown() { + this.regional = []; // Available regional settings, indexed by language code + this.regional[''] = { // Default regional settings + // The display texts for the counters + labels: ['Years', 'Months', 'Weeks', 'Days', 'Hours', 'Minutes', 'Seconds'], + // The display texts for the counters if only one + labels1: ['Year', 'Month', 'Week', 'Day', 'Hour', 'Minute', 'Second'], + compactLabels: ['y', 'm', 'w', 'd'], // The compact texts for the counters + whichLabels: null, // Function to determine which labels to use + timeSeparator: ':', // Separator for time periods + isRTL: false // True for right-to-left languages, false for left-to-right + }; + this._defaults = { + until: null, // new Date(year, mth - 1, day, hr, min, sec) - date/time to count down to + // or numeric for seconds offset, or string for unit offset(s): + // 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds + since: null, // new Date(year, mth - 1, day, hr, min, sec) - date/time to count up from + // or numeric for seconds offset, or string for unit offset(s): + // 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds + timezone: null, // The timezone (hours or minutes from GMT) for the target times, + // or null for client local + serverSync: null, // A function to retrieve the current server time for synchronisation + format: 'dHMS', // Format for display - upper case for always, lower case only if non-zero, + // 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds + layout: '', // Build your own layout for the countdown + compact: false, // True to display in a compact format, false for an expanded one + significant: 0, // The number of periods with values to show, zero for all + description: '', // The description displayed for the countdown + expiryUrl: '', // A URL to load upon expiry, replacing the current page + expiryText: '', // Text to display upon expiry, replacing the countdown + alwaysExpire: false, // True to trigger onExpiry even if never counted down + onExpiry: null, // Callback when the countdown expires - + // receives no parameters and 'this' is the containing division + onTick: null, // Callback when the countdown is updated - + // receives int[7] being the breakdown by period (based on format) + // and 'this' is the containing division + tickInterval: 1 // Interval (seconds) between onTick callbacks + }; + $.extend(this._defaults, this.regional['']); + this._serverSyncs = []; +} + +var PROP_NAME = 'countdown'; + +var Y = 0; // Years +var O = 1; // Months +var W = 2; // Weeks +var D = 3; // Days +var H = 4; // Hours +var M = 5; // Minutes +var S = 6; // Seconds + +$.extend(Countdown.prototype, { + /* Class name added to elements to indicate already configured with countdown. */ + markerClassName: 'hasCountdown', + + /* Shared timer for all countdowns. */ + _timer: setInterval(function() { $.countdown._updateTargets(); }, 980), + /* List of currently active countdown targets. */ + _timerTargets: [], + + /* Override the default settings for all instances of the countdown widget. + @param options (object) the new settings to use as defaults */ + setDefaults: function(options) { + this._resetExtraLabels(this._defaults, options); + extendRemove(this._defaults, options || {}); + }, + + /* Convert a date/time to UTC. + @param tz (number) the hour or minute offset from GMT, e.g. +9, -360 + @param year (Date) the date/time in that timezone or + (number) the year in that timezone + @param month (number, optional) the month (0 - 11) (omit if year is a Date) + @param day (number, optional) the day (omit if year is a Date) + @param hours (number, optional) the hour (omit if year is a Date) + @param mins (number, optional) the minute (omit if year is a Date) + @param secs (number, optional) the second (omit if year is a Date) + @param ms (number, optional) the millisecond (omit if year is a Date) + @return (Date) the equivalent UTC date/time */ + UTCDate: function(tz, year, month, day, hours, mins, secs, ms) { + if (typeof year == 'object' && year.constructor == Date) { + ms = year.getMilliseconds(); + secs = year.getSeconds(); + mins = year.getMinutes(); + hours = year.getHours(); + day = year.getDate(); + month = year.getMonth(); + year = year.getFullYear(); + } + var d = new Date(); + d.setUTCFullYear(year); + d.setUTCDate(1); + d.setUTCMonth(month || 0); + d.setUTCDate(day || 1); + d.setUTCHours(hours || 0); + d.setUTCMinutes((mins || 0) - (Math.abs(tz) < 30 ? tz * 60 : tz)); + d.setUTCSeconds(secs || 0); + d.setUTCMilliseconds(ms || 0); + return d; + }, + + /* Convert a set of periods into seconds. + Averaged for months and years. + @param periods (number[7]) the periods per year/month/week/day/hour/minute/second + @return (number) the corresponding number of seconds */ + periodsToSeconds: function(periods) { + return periods[0] * 31557600 + periods[1] * 2629800 + periods[2] * 604800 + + periods[3] * 86400 + periods[4] * 3600 + periods[5] * 60 + periods[6]; + }, + + /* Retrieve one or more settings values. + @param name (string, optional) the name of the setting to retrieve + or 'all' for all instance settings or omit for all default settings + @return (any) the requested setting(s) */ + _settingsCountdown: function(target, name) { + if (!name) { + return $.countdown._defaults; + } + var inst = $.data(target, PROP_NAME); + return (name == 'all' ? inst.options : inst.options[name]); + }, + + /* Attach the countdown widget to a div. + @param target (element) the containing division + @param options (object) the initial settings for the countdown */ + _attachCountdown: function(target, options) { + var $target = $(target); + if ($target.hasClass(this.markerClassName)) { + return; + } + $target.addClass(this.markerClassName); + var inst = {options: $.extend({}, options), + _periods: [0, 0, 0, 0, 0, 0, 0]}; + $.data(target, PROP_NAME, inst); + this._changeCountdown(target); + }, + + /* Add a target to the list of active ones. + @param target (element) the countdown target */ + _addTarget: function(target) { + if (!this._hasTarget(target)) { + this._timerTargets.push(target); + } + }, + + /* See if a target is in the list of active ones. + @param target (element) the countdown target + @return (boolean) true if present, false if not */ + _hasTarget: function(target) { + return ($.inArray(target, this._timerTargets) > -1); + }, + + /* Remove a target from the list of active ones. + @param target (element) the countdown target */ + _removeTarget: function(target) { + this._timerTargets = $.map(this._timerTargets, + function(value) { return (value == target ? null : value); }); // delete entry + }, + + /* Update each active timer target. */ + _updateTargets: function() { + for (var i = this._timerTargets.length - 1; i >= 0; i--) { + this._updateCountdown(this._timerTargets[i]); + } + }, + + /* Redisplay the countdown with an updated display. + @param target (jQuery) the containing division + @param inst (object) the current settings for this instance */ + _updateCountdown: function(target, inst) { + var $target = $(target); + inst = inst || $.data(target, PROP_NAME); + if (!inst) { + return; + } + $target.html(this._generateHTML(inst)); + $target[(this._get(inst, 'isRTL') ? 'add' : 'remove') + 'Class']('countdown_rtl'); + var onTick = this._get(inst, 'onTick'); + if (onTick) { + var periods = inst._hold != 'lap' ? inst._periods : + this._calculatePeriods(inst, inst._show, this._get(inst, 'significant'), new Date()); + var tickInterval = this._get(inst, 'tickInterval'); + if (tickInterval == 1 || this.periodsToSeconds(periods) % tickInterval == 0) { + onTick.apply(target, [periods]); + } + } + var expired = inst._hold != 'pause' && + (inst._since ? inst._now.getTime() < inst._since.getTime() : + inst._now.getTime() >= inst._until.getTime()); + if (expired && !inst._expiring) { + inst._expiring = true; + if (this._hasTarget(target) || this._get(inst, 'alwaysExpire')) { + this._removeTarget(target); + var onExpiry = this._get(inst, 'onExpiry'); + if (onExpiry) { + onExpiry.apply(target, []); + } + var expiryText = this._get(inst, 'expiryText'); + if (expiryText) { + var layout = this._get(inst, 'layout'); + inst.options.layout = expiryText; + this._updateCountdown(target, inst); + inst.options.layout = layout; + } + var expiryUrl = this._get(inst, 'expiryUrl'); + if (expiryUrl) { + window.location = expiryUrl; + } + } + inst._expiring = false; + } + else if (inst._hold == 'pause') { + this._removeTarget(target); + } + $.data(target, PROP_NAME, inst); + }, + + /* Reconfigure the settings for a countdown div. + @param target (element) the containing division + @param options (object) the new settings for the countdown or + (string) an individual property name + @param value (any) the individual property value + (omit if options is an object) */ + _changeCountdown: function(target, options, value) { + options = options || {}; + if (typeof options == 'string') { + var name = options; + options = {}; + options[name] = value; + } + var inst = $.data(target, PROP_NAME); + if (inst) { + this._resetExtraLabels(inst.options, options); + extendRemove(inst.options, options); + this._adjustSettings(target, inst); + $.data(target, PROP_NAME, inst); + var now = new Date(); + if ((inst._since && inst._since < now) || + (inst._until && inst._until > now)) { + this._addTarget(target); + } + this._updateCountdown(target, inst); + } + }, + + /* Reset any extra labelsn and compactLabelsn entries if changing labels. + @param base (object) the options to be updated + @param options (object) the new option values */ + _resetExtraLabels: function(base, options) { + var changingLabels = false; + for (var n in options) { + if (n != 'whichLabels' && n.match(/[Ll]abels/)) { + changingLabels = true; + break; + } + } + if (changingLabels) { + for (var n in base) { // Remove custom numbered labels + if (n.match(/[Ll]abels[0-9]/)) { + base[n] = null; + } + } + } + }, + + /* Calculate interal settings for an instance. + @param target (element) the containing division + @param inst (object) the current settings for this instance */ + _adjustSettings: function(target, inst) { + var now; + var serverSync = this._get(inst, 'serverSync'); + var serverOffset = 0; + var serverEntry = null; + for (var i = 0; i < this._serverSyncs.length; i++) { + if (this._serverSyncs[i][0] == serverSync) { + serverEntry = this._serverSyncs[i][1]; + break; + } + } + if (serverEntry != null) { + serverOffset = (serverSync ? serverEntry : 0); + now = new Date(); + } + else { + var serverResult = (serverSync ? serverSync.apply(target, []) : null); + now = new Date(); + serverOffset = (serverResult ? now.getTime() - serverResult.getTime() : 0); + this._serverSyncs.push([serverSync, serverOffset]); + } + var timezone = this._get(inst, 'timezone'); + timezone = (timezone == null ? -now.getTimezoneOffset() : timezone); + inst._since = this._get(inst, 'since'); + if (inst._since != null) { + inst._since = this.UTCDate(timezone, this._determineTime(inst._since, null)); + if (inst._since && serverOffset) { + inst._since.setMilliseconds(inst._since.getMilliseconds() + serverOffset); + } + } + inst._until = this.UTCDate(timezone, this._determineTime(this._get(inst, 'until'), now)); + if (serverOffset) { + inst._until.setMilliseconds(inst._until.getMilliseconds() + serverOffset); + } + inst._show = this._determineShow(inst); + }, + + /* Remove the countdown widget from a div. + @param target (element) the containing division */ + _destroyCountdown: function(target) { + var $target = $(target); + if (!$target.hasClass(this.markerClassName)) { + return; + } + this._removeTarget(target); + $target.removeClass(this.markerClassName).empty(); + $.removeData(target, PROP_NAME); + }, + + /* Pause a countdown widget at the current time. + Stop it running but remember and display the current time. + @param target (element) the containing division */ + _pauseCountdown: function(target) { + this._hold(target, 'pause'); + }, + + /* Pause a countdown widget at the current time. + Stop the display but keep the countdown running. + @param target (element) the containing division */ + _lapCountdown: function(target) { + this._hold(target, 'lap'); + }, + + /* Resume a paused countdown widget. + @param target (element) the containing division */ + _resumeCountdown: function(target) { + this._hold(target, null); + }, + + /* Pause or resume a countdown widget. + @param target (element) the containing division + @param hold (string) the new hold setting */ + _hold: function(target, hold) { + var inst = $.data(target, PROP_NAME); + if (inst) { + if (inst._hold == 'pause' && !hold) { + inst._periods = inst._savePeriods; + var sign = (inst._since ? '-' : '+'); + inst[inst._since ? '_since' : '_until'] = + this._determineTime(sign + inst._periods[0] + 'y' + + sign + inst._periods[1] + 'o' + sign + inst._periods[2] + 'w' + + sign + inst._periods[3] + 'd' + sign + inst._periods[4] + 'h' + + sign + inst._periods[5] + 'm' + sign + inst._periods[6] + 's'); + this._addTarget(target); + } + inst._hold = hold; + inst._savePeriods = (hold == 'pause' ? inst._periods : null); + $.data(target, PROP_NAME, inst); + this._updateCountdown(target, inst); + } + }, + + /* Return the current time periods. + @param target (element) the containing division + @return (number[7]) the current periods for the countdown */ + _getTimesCountdown: function(target) { + var inst = $.data(target, PROP_NAME); + return (!inst ? null : (!inst._hold ? inst._periods : + this._calculatePeriods(inst, inst._show, this._get(inst, 'significant'), new Date()))); + }, + + /* Get a setting value, defaulting if necessary. + @param inst (object) the current settings for this instance + @param name (string) the name of the required setting + @return (any) the setting's value or a default if not overridden */ + _get: function(inst, name) { + return (inst.options[name] != null ? + inst.options[name] : $.countdown._defaults[name]); + }, + + /* A time may be specified as an exact value or a relative one. + @param setting (string or number or Date) - the date/time value + as a relative or absolute value + @param defaultTime (Date) the date/time to use if no other is supplied + @return (Date) the corresponding date/time */ + _determineTime: function(setting, defaultTime) { + var offsetNumeric = function(offset) { // e.g. +300, -2 + var time = new Date(); + time.setTime(time.getTime() + offset * 1000); + return time; + }; + var offsetString = function(offset) { // e.g. '+2d', '-4w', '+3h +30m' + offset = offset.toLowerCase(); + var time = new Date(); + var year = time.getFullYear(); + var month = time.getMonth(); + var day = time.getDate(); + var hour = time.getHours(); + var minute = time.getMinutes(); + var second = time.getSeconds(); + var pattern = /([+-]?[0-9]+)\s*(s|m|h|d|w|o|y)?/g; + var matches = pattern.exec(offset); + while (matches) { + switch (matches[2] || 's') { + case 's': second += parseInt(matches[1], 10); break; + case 'm': minute += parseInt(matches[1], 10); break; + case 'h': hour += parseInt(matches[1], 10); break; + case 'd': day += parseInt(matches[1], 10); break; + case 'w': day += parseInt(matches[1], 10) * 7; break; + case 'o': + month += parseInt(matches[1], 10); + day = Math.min(day, $.countdown._getDaysInMonth(year, month)); + break; + case 'y': + year += parseInt(matches[1], 10); + day = Math.min(day, $.countdown._getDaysInMonth(year, month)); + break; + } + matches = pattern.exec(offset); + } + return new Date(year, month, day, hour, minute, second, 0); + }; + var time = (setting == null ? defaultTime : + (typeof setting == 'string' ? offsetString(setting) : + (typeof setting == 'number' ? offsetNumeric(setting) : setting))); + if (time) time.setMilliseconds(0); + return time; + }, + + /* Determine the number of days in a month. + @param year (number) the year + @param month (number) the month + @return (number) the days in that month */ + _getDaysInMonth: function(year, month) { + return 32 - new Date(year, month, 32).getDate(); + }, + + /* Determine which set of labels should be used for an amount. + @param num (number) the amount to be displayed + @return (number) the set of labels to be used for this amount */ + _normalLabels: function(num) { + return num; + }, + + /* Generate the HTML to display the countdown widget. + @param inst (object) the current settings for this instance + @return (string) the new HTML for the countdown display */ + _generateHTML: function(inst) { + // Determine what to show + var significant = this._get(inst, 'significant'); + inst._periods = (inst._hold ? inst._periods : + this._calculatePeriods(inst, inst._show, significant, new Date())); + // Show all 'asNeeded' after first non-zero value + var shownNonZero = false; + var showCount = 0; + var sigCount = significant; + var show = $.extend({}, inst._show); + for (var period = Y; period <= S; period++) { + shownNonZero |= (inst._show[period] == '?' && inst._periods[period] > 0); + show[period] = (inst._show[period] == '?' && !shownNonZero ? null : inst._show[period]); + showCount += (show[period] ? 1 : 0); + sigCount -= (inst._periods[period] > 0 ? 1 : 0); + } + var showSignificant = [false, false, false, false, false, false, false]; + for (var period = S; period >= Y; period--) { // Determine significant periods + if (inst._show[period]) { + if (inst._periods[period]) { + showSignificant[period] = true; + } + else { + showSignificant[period] = sigCount > 0; + sigCount--; + } + } + } + var compact = this._get(inst, 'compact'); + var layout = this._get(inst, 'layout'); + var labels = (compact ? this._get(inst, 'compactLabels') : this._get(inst, 'labels')); + var whichLabels = this._get(inst, 'whichLabels') || this._normalLabels; + var timeSeparator = this._get(inst, 'timeSeparator'); + var description = this._get(inst, 'description') || ''; + var showCompact = function(period) { + var labelsNum = $.countdown._get(inst, + 'compactLabels' + whichLabels(inst._periods[period])); + return (show[period] ? inst._periods[period] + + (labelsNum ? labelsNum[period] : labels[period]) + ' ' : ''); + }; + var showFull = function(period) { + var labelsNum = $.countdown._get(inst, 'labels' + whichLabels(inst._periods[period])); + return ((!significant && show[period]) || (significant && showSignificant[period]) ? + '<span class="countdown_section"><span class="countdown_amount">' + + inst._periods[period] + '</span><br/>' + + (labelsNum ? labelsNum[period] : labels[period]) + '</span>' : ''); + }; + return (layout ? this._buildLayout(inst, show, layout, compact, significant, showSignificant) : + ((compact ? // Compact version + '<span class="countdown_row countdown_amount' + + (inst._hold ? ' countdown_holding' : '') + '">' + + showCompact(Y) + showCompact(O) + showCompact(W) + showCompact(D) + + (show[H] ? this._minDigits(inst._periods[H], 2) : '') + + (show[M] ? (show[H] ? timeSeparator : '') + + this._minDigits(inst._periods[M], 2) : '') + + (show[S] ? (show[H] || show[M] ? timeSeparator : '') + + this._minDigits(inst._periods[S], 2) : '') : + // Full version + '<span class="countdown_row countdown_show' + (significant || showCount) + + (inst._hold ? ' countdown_holding' : '') + '">' + + showFull(Y) + showFull(O) + showFull(W) + showFull(D) + + showFull(H) + showFull(M) + showFull(S)) + '</span>' + + (description ? '<span class="countdown_row countdown_descr">' + description + '</span>' : ''))); + }, + + /* Construct a custom layout. + @param inst (object) the current settings for this instance + @param show (string[7]) flags indicating which periods are requested + @param layout (string) the customised layout + @param compact (boolean) true if using compact labels + @param significant (number) the number of periods with values to show, zero for all + @param showSignificant (boolean[7]) other periods to show for significance + @return (string) the custom HTML */ + _buildLayout: function(inst, show, layout, compact, significant, showSignificant) { + var labels = this._get(inst, (compact ? 'compactLabels' : 'labels')); + var whichLabels = this._get(inst, 'whichLabels') || this._normalLabels; + var labelFor = function(index) { + return ($.countdown._get(inst, + (compact ? 'compactLabels' : 'labels') + whichLabels(inst._periods[index])) || + labels)[index]; + }; + var digit = function(value, position) { + return Math.floor(value / position) % 10; + }; + var subs = {desc: this._get(inst, 'description'), sep: this._get(inst, 'timeSeparator'), + yl: labelFor(Y), yn: inst._periods[Y], ynn: this._minDigits(inst._periods[Y], 2), + ynnn: this._minDigits(inst._periods[Y], 3), y1: digit(inst._periods[Y], 1), + y10: digit(inst._periods[Y], 10), y100: digit(inst._periods[Y], 100), + y1000: digit(inst._periods[Y], 1000), + ol: labelFor(O), on: inst._periods[O], onn: this._minDigits(inst._periods[O], 2), + onnn: this._minDigits(inst._periods[O], 3), o1: digit(inst._periods[O], 1), + o10: digit(inst._periods[O], 10), o100: digit(inst._periods[O], 100), + o1000: digit(inst._periods[O], 1000), + wl: labelFor(W), wn: inst._periods[W], wnn: this._minDigits(inst._periods[W], 2), + wnnn: this._minDigits(inst._periods[W], 3), w1: digit(inst._periods[W], 1), + w10: digit(inst._periods[W], 10), w100: digit(inst._periods[W], 100), + w1000: digit(inst._periods[W], 1000), + dl: labelFor(D), dn: inst._periods[D], dnn: this._minDigits(inst._periods[D], 2), + dnnn: this._minDigits(inst._periods[D], 3), d1: digit(inst._periods[D], 1), + d10: digit(inst._periods[D], 10), d100: digit(inst._periods[D], 100), + d1000: digit(inst._periods[D], 1000), + hl: labelFor(H), hn: inst._periods[H], hnn: this._minDigits(inst._periods[H], 2), + hnnn: this._minDigits(inst._periods[H], 3), h1: digit(inst._periods[H], 1), + h10: digit(inst._periods[H], 10), h100: digit(inst._periods[H], 100), + h1000: digit(inst._periods[H], 1000), + ml: labelFor(M), mn: inst._periods[M], mnn: this._minDigits(inst._periods[M], 2), + mnnn: this._minDigits(inst._periods[M], 3), m1: digit(inst._periods[M], 1), + m10: digit(inst._periods[M], 10), m100: digit(inst._periods[M], 100), + m1000: digit(inst._periods[M], 1000), + sl: labelFor(S), sn: inst._periods[S], snn: this._minDigits(inst._periods[S], 2), + snnn: this._minDigits(inst._periods[S], 3), s1: digit(inst._periods[S], 1), + s10: digit(inst._periods[S], 10), s100: digit(inst._periods[S], 100), + s1000: digit(inst._periods[S], 1000)}; + var html = layout; + // Replace period containers: {p<}...{p>} + for (var i = Y; i <= S; i++) { + var period = 'yowdhms'.charAt(i); + var re = new RegExp('\\{' + period + '<\\}(.*)\\{' + period + '>\\}', 'g'); + html = html.replace(re, ((!significant && show[i]) || + (significant && showSignificant[i]) ? '$1' : '')); + } + // Replace period values: {pn} + $.each(subs, function(n, v) { + var re = new RegExp('\\{' + n + '\\}', 'g'); + html = html.replace(re, v); + }); + return html; + }, + + /* Ensure a numeric value has at least n digits for display. + @param value (number) the value to display + @param len (number) the minimum length + @return (string) the display text */ + _minDigits: function(value, len) { + value = '' + value; + if (value.length >= len) { + return value; + } + value = '0000000000' + value; + return value.substr(value.length - len); + }, + + /* Translate the format into flags for each period. + @param inst (object) the current settings for this instance + @return (string[7]) flags indicating which periods are requested (?) or + required (!) by year, month, week, day, hour, minute, second */ + _determineShow: function(inst) { + var format = this._get(inst, 'format'); + var show = []; + show[Y] = (format.match('y') ? '?' : (format.match('Y') ? '!' : null)); + show[O] = (format.match('o') ? '?' : (format.match('O') ? '!' : null)); + show[W] = (format.match('w') ? '?' : (format.match('W') ? '!' : null)); + show[D] = (format.match('d') ? '?' : (format.match('D') ? '!' : null)); + show[H] = (format.match('h') ? '?' : (format.match('H') ? '!' : null)); + show[M] = (format.match('m') ? '?' : (format.match('M') ? '!' : null)); + show[S] = (format.match('s') ? '?' : (format.match('S') ? '!' : null)); + return show; + }, + + /* Calculate the requested periods between now and the target time. + @param inst (object) the current settings for this instance + @param show (string[7]) flags indicating which periods are requested/required + @param significant (number) the number of periods with values to show, zero for all + @param now (Date) the current date and time + @return (number[7]) the current time periods (always positive) + by year, month, week, day, hour, minute, second */ + _calculatePeriods: function(inst, show, significant, now) { + // Find endpoints + inst._now = now; + inst._now.setMilliseconds(0); + var until = new Date(inst._now.getTime()); + if (inst._since) { + if (now.getTime() < inst._since.getTime()) { + inst._now = now = until; + } + else { + now = inst._since; + } + } + else { + until.setTime(inst._until.getTime()); + if (now.getTime() > inst._until.getTime()) { + inst._now = now = until; + } + } + // Calculate differences by period + var periods = [0, 0, 0, 0, 0, 0, 0]; + if (show[Y] || show[O]) { + // Treat end of months as the same + var lastNow = $.countdown._getDaysInMonth(now.getFullYear(), now.getMonth()); + var lastUntil = $.countdown._getDaysInMonth(until.getFullYear(), until.getMonth()); + var sameDay = (until.getDate() == now.getDate() || + (until.getDate() >= Math.min(lastNow, lastUntil) && + now.getDate() >= Math.min(lastNow, lastUntil))); + var getSecs = function(date) { + return (date.getHours() * 60 + date.getMinutes()) * 60 + date.getSeconds(); + }; + var months = Math.max(0, + (until.getFullYear() - now.getFullYear()) * 12 + until.getMonth() - now.getMonth() + + ((until.getDate() < now.getDate() && !sameDay) || + (sameDay && getSecs(until) < getSecs(now)) ? -1 : 0)); + periods[Y] = (show[Y] ? Math.floor(months / 12) : 0); + periods[O] = (show[O] ? months - periods[Y] * 12 : 0); + // Adjust for months difference and end of month if necessary + now = new Date(now.getTime()); + var wasLastDay = (now.getDate() == lastNow); + var lastDay = $.countdown._getDaysInMonth(now.getFullYear() + periods[Y], + now.getMonth() + periods[O]); + if (now.getDate() > lastDay) { + now.setDate(lastDay); + } + now.setFullYear(now.getFullYear() + periods[Y]); + now.setMonth(now.getMonth() + periods[O]); + if (wasLastDay) { + now.setDate(lastDay); + } + } + var diff = Math.floor((until.getTime() - now.getTime()) / 1000); + var extractPeriod = function(period, numSecs) { + periods[period] = (show[period] ? Math.floor(diff / numSecs) : 0); + diff -= periods[period] * numSecs; + }; + extractPeriod(W, 604800); + extractPeriod(D, 86400); + extractPeriod(H, 3600); + extractPeriod(M, 60); + extractPeriod(S, 1); + if (diff > 0 && !inst._since) { // Round up if left overs + var multiplier = [1, 12, 4.3482, 7, 24, 60, 60]; + var lastShown = S; + var max = 1; + for (var period = S; period >= Y; period--) { + if (show[period]) { + if (periods[lastShown] >= max) { + periods[lastShown] = 0; + diff = 1; + } + if (diff > 0) { + periods[period]++; + diff = 0; + lastShown = period; + max = 1; + } + } + max *= multiplier[period]; + } + } + if (significant) { // Zero out insignificant periods + for (var period = Y; period <= S; period++) { + if (significant && periods[period]) { + significant--; + } + else if (!significant) { + periods[period] = 0; + } + } + } + return periods; + } +}); + +/* jQuery extend now ignores nulls! + @param target (object) the object to update + @param props (object) the new settings + @return (object) the updated object */ +function extendRemove(target, props) { + $.extend(target, props); + for (var name in props) { + if (props[name] == null) { + target[name] = null; + } + } + return target; +} + +/* Process the countdown functionality for a jQuery selection. + @param command (string) the command to run (optional, default 'attach') + @param options (object) the new settings to use for these countdown instances + @return (jQuery) for chaining further calls */ +$.fn.countdown = function(options) { + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (options == 'getTimes' || options == 'settings') { + return $.countdown['_' + options + 'Countdown']. + apply($.countdown, [this[0]].concat(otherArgs)); + } + return this.each(function() { + if (typeof options == 'string') { + $.countdown['_' + options + 'Countdown'].apply($.countdown, [this].concat(otherArgs)); + } + else { + $.countdown._attachCountdown(this, options); + } + }); +}; + +/* Initialise the countdown functionality. */ +$.countdown = new Countdown(); // singleton instance + +})(jQuery); diff --git a/static/jquery.pop.js b/static/jquery.pop.js new file mode 100644 index 0000000..d679166 --- /dev/null +++ b/static/jquery.pop.js @@ -0,0 +1,63 @@ +// +// pop! for jQuery +// v0.2 requires jQuery v1.2 or later +// +// Licensed under the MIT: +// http://www.opensource.org/licenses/mit-license.php +// +// Copyright 2007,2008 SEAOFCLOUDS [http://seaofclouds.com] +// + +(function($) { + + $.pop = function(options){ + + // settings + var settings = { + pop_class : '.pop', + pop_toggle_text : '' + } + + // inject html wrapper + function initpops (){ + $(settings.pop_class).each(function() { + var pop_classes = $(this).attr("class"); + $(this).addClass("pop_menu"); + $(this).wrap("<div class='"+pop_classes+"'></div>"); + $(".pop_menu").attr("class", "pop_menu"); + $(this).before(" \ + <div class='pop_toggle'>"+settings.pop_toggle_text+"</div> \ + "); + }); + } + initpops(); + + // assign reverse z-indexes to each pop + var totalpops = $(settings.pop_class).size() + 1000; + $(settings.pop_class).each(function(i) { + var popzindex = totalpops - i; + $(this).css({ zIndex: popzindex }); + }); + // close pops if user clicks outside of pop + activePop = null; + function closeInactivePop() { + $(settings.pop_class).each(function (i) { + if ($(this).hasClass('active') && i!=activePop) { + $(this).removeClass('active'); + } + }); + return false; + } + $(settings.pop_class).mouseover(function() { activePop = $(settings.pop_class).index(this); }); + $(settings.pop_class).mouseout(function() { activePop = null; }); + + $(document.body).click(function(){ + closeInactivePop(); + }); + // toggle that pop + $(".pop_toggle").click(function(){ + $(this).parent(settings.pop_class).toggleClass("active"); + }); + } + +})(jQuery); diff --git a/static/jquery.treeview.pack.js b/static/jquery.treeview.pack.js new file mode 100644 index 0000000..eddac49 --- /dev/null +++ b/static/jquery.treeview.pack.js @@ -0,0 +1,16 @@ +/* + * Treeview 1.4 - jQuery plugin to hide and show branches of a tree + * + * http://bassistance.de/jquery-plugins/jquery-plugin-treeview/ + * http://docs.jquery.com/Plugins/Treeview + * + * Copyright (c) 2007 Jörn Zaefferer + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Revision: $Id: jquery.treeview.js 4684 2008-02-07 19:08:06Z joern.zaefferer $ + * + */ +eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}(';(4($){$.1l($.F,{E:4(b,c){l a=3.n(\'.\'+b);3.n(\'.\'+c).o(c).m(b);a.o(b).m(c);8 3},s:4(a,b){8 3.n(\'.\'+a).o(a).m(b).P()},1n:4(a){a=a||"1j";8 3.1j(4(){$(3).m(a)},4(){$(3).o(a)})},1h:4(b,a){b?3.1g({1e:"p"},b,a):3.x(4(){T(3)[T(3).1a(":U")?"H":"D"]();7(a)a.A(3,O)})},12:4(b,a){7(b){3.1g({1e:"D"},b,a)}1L{3.D();7(a)3.x(a)}},11:4(a){7(!a.1k){3.n(":r-1H:G(9)").m(k.r);3.n((a.1F?"":"."+k.X)+":G(."+k.W+")").6(">9").D()}8 3.n(":y(>9)")},S:4(b,c){3.n(":y(>9):G(:y(>a))").6(">1z").C(4(a){c.A($(3).19())}).w($("a",3)).1n();7(!b.1k){3.n(":y(>9:U)").m(k.q).s(k.r,k.t);3.G(":y(>9:U)").m(k.u).s(k.r,k.v);3.1r("<J 14=\\""+k.5+"\\"/>").6("J."+k.5).x(4(){l a="";$.x($(3).B().1o("14").13(" "),4(){a+=3+"-5 "});$(3).m(a)})}3.6("J."+k.5).C(c)},z:4(g){g=$.1l({N:"z"},g);7(g.w){8 3.1K("w",[g.w])}7(g.p){l d=g.p;g.p=4(){8 d.A($(3).B()[0],O)}}4 1m(b,c){4 L(a){8 4(){K.A($("J."+k.5,b).n(4(){8 a?$(3).B("."+a).1i:1I}));8 1G}}$("a:10(0)",c).C(L(k.u));$("a:10(1)",c).C(L(k.q));$("a:10(2)",c).C(L())}4 K(){$(3).B().6(">.5").E(k.Z,k.Y).E(k.I,k.M).P().E(k.u,k.q).E(k.v,k.t).6(">9").1h(g.1f,g.p);7(g.1E){$(3).B().1D().6(">.5").s(k.Z,k.Y).s(k.I,k.M).P().s(k.u,k.q).s(k.v,k.t).6(">9").12(g.1f,g.p)}}4 1d(){4 1C(a){8 a?1:0}l b=[];j.x(4(i,e){b[i]=$(e).1a(":y(>9:1B)")?1:0});$.V(g.N,b.1A(""))}4 1c(){l b=$.V(g.N);7(b){l a=b.13("");j.x(4(i,e){$(e).6(">9")[1y(a[i])?"H":"D"]()})}}3.m("z");l j=3.6("Q").11(g);1x(g.1w){18"V":l h=g.p;g.p=4(){1d();7(h){h.A(3,O)}};1c();17;18"1b":l f=3.6("a").n(4(){8 3.16.15()==1b.16.15()});7(f.1i){f.m("1v").1u("9, Q").w(f.19()).H()}17}j.S(g,K);7(g.R){1m(3,g.R);$(g.R).H()}8 3.1t("w",4(a,b){$(b).1s().o(k.r).o(k.v).o(k.t).6(">.5").o(k.I).o(k.M);$(b).6("Q").1q().11(g).S(g,K)})}});l k=$.F.z.1J={W:"W",X:"X",q:"q",Y:"q-5",M:"t-5",u:"u",Z:"u-5",I:"v-5",v:"v",t:"t",r:"r",5:"5"};$.F.1p=$.F.z})(T);',62,110,'|||this|function|hitarea|find|if|return|ul||||||||||||var|addClass|filter|removeClass|toggle|expandable|last|replaceClass|lastExpandable|collapsable|lastCollapsable|add|each|has|treeview|apply|parent|click|hide|swapClass|fn|not|show|lastCollapsableHitarea|div|toggler|handler|lastExpandableHitarea|cookieId|arguments|end|li|control|applyClasses|jQuery|hidden|cookie|open|closed|expandableHitarea|collapsableHitarea|eq|prepareBranches|heightHide|split|class|toLowerCase|href|break|case|next|is|location|deserialize|serialize|height|animated|animate|heightToggle|length|hover|prerendered|extend|treeController|hoverClass|attr|Treeview|andSelf|prepend|prev|bind|parents|selected|persist|switch|parseInt|span|join|visible|binary|siblings|unique|collapsed|false|child|true|classes|trigger|else'.split('|'),0,{}))
\ No newline at end of file diff --git a/static/mauve.js b/static/mauve.js new file mode 100644 index 0000000..0cc32ec --- /dev/null +++ b/static/mauve.js @@ -0,0 +1,211 @@ +/* + * This contains all the 'clever' javascript used on the page. + */ +var mouse_is_inside = false; + +/* +try { + $("#myselector").click (function () {}); //my jQuery code here +} catch (e) { + //this executes if jQuery isn't loaded + alert(e.message + + "\nCould be a network error leading to jquery not being loaded!\n" + + "Reloading the page."); + window.location.reload(true); +} +*/ + +//////////////////////////////////////////////////////////////////////////////// +// Treeview data. +$(document).ready(function(){ + $("#blackAck").treeview({ + control: "#treecontrolAck", + persist: "cookie", + cookieId: "treeview-black" + }); + +}); +$(document).ready(function(){ + $("#blackNew").treeview({ + control: "#treecontrolNew", + persist: "cookie", + cookieId: "treeview-black" + }); +}); + +$(document).ready(function(){ + + //////////////////////////////////////////////////////////////////////////////// + // This allows pop! to do its thing, used for details. + $.pop(); + + //////////////////////////////////////////////////////////////////////////////// + // Countdown code. + + /* + // This binds to the timer that reloads the page every 300 seconds via callback. + $('#reloadPage').countdown({until: +300, onExpiry: liftOff, format: 'MS'}); + + // This is the callback that reloads the page. + function liftOff() { + window.location.reload(true); + } + */ + + + //////////////////////////////////////////////////////////////////////////////// + // Mouse outside of changeStatus form. + // See url http://stackoverflow.com/questions/1403615/use-jquery-to-hide-div-when-click-outside-it + $('.updateAlertStatus').hover(function(){ + mouse_is_inside=true; + }, function(){ + mouse_is_inside=false; + }); + $('body').mouseup(function(){ + if(! mouse_is_inside) + { + //$(".updateAlertStatus").fadeOut(1000); + //$('.darkMask').fadeOut(1000); + $(".updateAlertStatus").hide(); + $('.darkMask').hide(); + } + }); +}); + +//////////////////////////////////////////////////////////////////////////////// +// Acknowledge status functions. + + +//////////////////////////////////////////////////////////////////////////////// +// Standards are there to be violated... +function mouseX(evt) { + if (evt.pageX) return evt.pageX; + else if (evt.clientX) + return evt.clientX + (document.documentElement.scrollLeft ? + document.documentElement.scrollLeft : + document.body.scrollLeft); + else return null; +} + +//////////////////////////////////////////////////////////////////////////////// +// Standards are there to be violated... +function mouseY(evt) { + if (evt.pageY) return evt.pageY; + else if (evt.clientY) + return evt.clientY + (document.documentElement.scrollTop ? + document.documentElement.scrollTop : + document.body.scrollTop); + else return null; +} + +//////////////////////////////////////////////////////////////////////////////// +// Shows the updateAlertStatus div where the mouse clicked and mask the rest of +// page. +function showAcknowledgeStatus (e, id, ackTime) { + + // Build the form. + document.changeAlertStatusForm.AlertID.value = id; + document.changeAlertStatusForm.AlertDefaultAcknowledgeTime.value = ackTime; + var myselect=document.getElementById("sample"); + myselect.remove(0); + str = returnTimeString(ackTime); + myselect.add(new Option(str, ackTime, true, true), myselect.options[0]) + + // Show the form. + //leftVal = mouseX(e); + leftVal = 2 + topVal = mouseY(e); + $('.updateAlertStatus').css({left:leftVal,top:topVal}).fadeIn(500); + $('.darkMask').css({height:$(document).height()}).show(); +} + +// Returns the default time. +function returnTimeString (time) { + hrs = time / 3600 + if (1 == hrs) + { + str = "1 hour" + } + else if (24 > hrs && 1 > hrs) + { + str = hrs + " hours" + } + else if (24 == hrs) + { + str = "1 day" + } + else if (24 < hrs && 168 > hrs) + { + str = hrs / 24 + " days" + } + else if (168 == hrs) + { + str = "1 week" + } + else + { + str = hrs / 168 + " weeks" + } + return str + ", default." +} + +//////////////////////////////////////////////////////////////////////////////// +// Shows the updateAlertSatus div for group of alerts. +function showBulkAcknowledgeStatus(e, ids, ackTime) +{ + for (i in ids) + { + changeAcknowledgeStatusCall(ids[i], ackTime); + } + //window.location.reload(true); + tmp = $('#firstAlert'+ids[0]); + tmp.remove() +} + +function changeAcknowledgeStatusCall (id, acknowledgedUntil) { + $.post('/alert/acknowledge/'+id+'/'+acknowledgedUntil); + tmp = $('#alert'+id) + tmp.remove(); + tmp.appendTo('#blackAck'); +} + +//////////////////////////////////////////////////////////////////////////////// +// Actually gets the alert updated and moves it to the right list. +// Note that id is a numberical ID of the alert. +// Note that acknowledgedUntil is a number of seconds. +function changeAcknowledgeStatus (id, acknowledgedUntil) { + if (-1 != id) + { + changeAcknowledgeStatusCall(id, acknowledgedUntil); + } + $(".updateAlertStatus").hide(); + $('.darkMask').hide(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Clears (aka trash aka delete) an alert. +// THIS IS NOT WHAT YOU WANT +// url http://stackoverflow.com/questions/95600/jquery-error-option-in-ajax-utility +// url http://stackoverflow.com/questions/377644/jquery-ajax-error-handling-show-custom-exception-messages +function clearAlert (id) { + $.post('/alert/'+id+'/clear'); + $(".updateAlertStatus").hide(); + $('.darkMask').hide(); + tmp = $('#alert'+id) + tmp.remove(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Raises (aka unacknowledge) an alert. +function raiseAlert (id) { + $.post('/alert/'+id+'/raise'); + $(".updateAlertStatus").hide(); + $('.darkMask').hide(); + tmp = $('#alert'+id) + tmp.remove(); + tmp.appendTo('#blackNew'); +} + + +//////////////////////////////////////////////////////////////////////////////// +// EOF diff --git a/static/mauve_utils.js b/static/mauve_utils.js new file mode 100644 index 0000000..2a14dc2 --- /dev/null +++ b/static/mauve_utils.js @@ -0,0 +1,150 @@ +// rather simple first stab at automating image rollovers - any image with +// a class of auto_hover will set its source to be the original name + _hover.png +// when rolled over, and back again when the mouse moves away. +// +// need to initialise by calling addAutoHover() after document has loaded. +// +function addAutoHover() { + $$('img.auto_hover').each(function(image) { + image.observe('mouseover', function(event) { + image.src = image.src.gsub(".png", "_hover.png"); + }); + image.observe('mouseout', function(event) { + image.src = image.src.gsub("_hover.png", ".png"); + }); + preload = new Image(); + preload.src = image.src.gsub(".png", "_hover.png"); + }); +}; + +function addRefresh() { + updater1 = new Ajax.PeriodicalUpdater("alert_summary", "/_alert_summary", + { method: 'get', frequency: 120 }); + updater2 = new Ajax.PeriodicalUpdater("alert_counts", "/_alert_counts", + { method: 'get', frequency: 120 }); +} + +// Pop up the big white box at the top when something goes wrong, scroll so +// user can see it. +// +function reportError(message) { + $('errors_list').insert('<li>'+message+'</li>'); + $('errors').show(); + $('errors').scrollTo(); +} +// Hide the big white box again +// +function clearErrors() { $('errors').hide(); } + +// Wrapper around reportError to report an error in updating a particular +// alert. +// +function acknowledgeFailed(id, message) { + if (message) + reportError("<strong>Couldn't update alert "+id+":</strong> "+message); + else + reportError("<strong>Couldn't update alert "+id+"</strong>"); +} + +// Updates the page from a JSON representation of a particular alert. +// +function updateAlert(alert) { + var strip = $('alert_'+alert.id); + if (!strip) { + reportError("Alert "+id+" not rendered - bug?"); + return; + } + + image = strip.down(".acknowledge img"); + image.src = alert.acknowledged_at ? + "/images/acknowledge_acknowledged.png" : + "/images/acknowledge_unacknowledged.png" + + if (strip.down(".source")) + strip.down(".source").update(alert.source); + if (strip.down(".subject")) + strip.down(".subject").update(alert.subject); + if (strip.down(".summary") && strip.down(".summary").down()) + strip.down(".summary").down().update(alert.summary); + strip.next().update(alert.detail); + + if (alert.acknowledged_at) + strip.next().hide(); +} + +// called when user hits the acknowledge button for an alert - makes a callback +// to the server to communicate the change, and updates the button +// appropriately. +// +function toggleAcknowledge(id) { + updater = new Ajax.Request('/alert/'+id+'/acknowledge', { + + method: 'post', + + // ignored by server, see http://www.ruby-forum.com/topic/162976 for why + postBody: 'x', + + onFailure: function(xhr) { acknowledgeFailed(id, "Failure - "+xhr.statusText); }, + + onException: function(xhr, ex) { acknowledgeFailed(id, Dumper(ex)); }, + + onSuccess: function(xhr) { + if (xhr.status == 200) { + content_type = xhr.getResponseHeader("Content-Type"); + if (content_type != "application/json") { + acknowledgeFailed(id, "Got "+content_type+" not application/json from server"); + } else { + updateAlert(xhr.responseText.evalJSON()); + } + } else { + acknowledgeFailed(id, "Connection problem"); + } + } + }); +}; + + +// Controls the showing of details on alerts. +function toggleDetailView(id) { + updater = new Ajax.Request('/alert/'+id+'/toggleDetailView', { + method: 'post', + postBody: 'x', + onFailure: function(xhr) { acknowledgeFailed(id, "Failure - "+xhr.statusText); }, + onException: function(xhr, ex) { acknowledgeFailed(id, Dumper(ex)); }, + onSuccess: function(xhr) { + if (xhr.status == 200) { + content_type = xhr.getResponseHeader("Content-Type"); + if (content_type != "application/json") { + acknowledgeFailed(id, "Got "+content_type+" not application/json from server"); + } else { + //updateAlert(xhr.responseText.evalJSON()); + } + } else { + acknowledgeFailed(id, "Connection problem"); + } + } + }); +} + + +// Controls the showing of folding on alerts. +function toggleFoldingView(subject) { + updater = new Ajax.Request('/alert/fold/'+subject, { + method: 'post', + postBody: 'x', + onFailure: function(xhr) { acknowledgeFailed(subject, "Failure - "+xhr.statusText); }, + onException: function(xhr, ex) { acknowledgeFailed(subject, Dumper(ex)); }, + onSuccess: function(xhr) { + if (xhr.status == 200) { + content_type = xhr.getResponseHeader("Content-Type"); + if (content_type != "application/json") { + acknowledgeFailed(subject, "Got "+content_type+" not application/json from server"); + } else { + //updateAlert(xhr.responseText.evalJSON()); + } + } else { + acknowledgeFailed(subject, "Connection problem"); + } + } + }); +} diff --git a/static/prototype.js b/static/prototype.js new file mode 100644 index 0000000..9fe6e12 --- /dev/null +++ b/static/prototype.js @@ -0,0 +1,4874 @@ +/* Prototype JavaScript framework, version 1.6.1 + * (c) 2005-2009 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ + * + *--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.6.1', + + Browser: (function(){ + var ua = navigator.userAgent; + var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'; + return { + IE: !!window.attachEvent && !isOpera, + Opera: isOpera, + WebKit: ua.indexOf('AppleWebKit/') > -1, + Gecko: ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1, + MobileSafari: /Apple.*Mobile.*Safari/.test(ua) + } + })(), + + BrowserFeatures: { + XPath: !!document.evaluate, + SelectorsAPI: !!document.querySelector, + ElementExtensions: (function() { + var constructor = window.Element || window.HTMLElement; + return !!(constructor && constructor.prototype); + })(), + SpecificElementExtensions: (function() { + if (typeof window.HTMLDivElement !== 'undefined') + return true; + + var div = document.createElement('div'); + var form = document.createElement('form'); + var isSupported = false; + + if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) { + isSupported = true; + } + + div = form = null; + + return isSupported; + })() + }, + + ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, + K: function(x) { return x } +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + + +var Abstract = { }; + + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + +/* Based on Alex Arnell's inheritance implementation. */ + +var Class = (function() { + function subclass() {}; + function create() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + return klass; + } + + function addMethods(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) { + if (source.toString != Object.prototype.toString) + properties.push("toString"); + if (source.valueOf != Object.prototype.valueOf) + properties.push("valueOf"); + } + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value; + value = (function(m) { + return function() { return ancestor[m].apply(this, arguments); }; + })(property).wrap(method); + + value.valueOf = method.valueOf.bind(method); + value.toString = method.toString.bind(method); + } + this.prototype[property] = value; + } + + return this; + } + + return { + create: create, + Methods: { + addMethods: addMethods + } + }; +})(); +(function() { + + var _toString = Object.prototype.toString; + + function extend(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; + } + + function inspect(object) { + try { + if (isUndefined(object)) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : String(object); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + } + + function toJSON(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (isElement(object)) return; + + var results = []; + for (var property in object) { + var value = toJSON(object[property]); + if (!isUndefined(value)) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + } + + function toQueryString(object) { + return $H(object).toQueryString(); + } + + function toHTML(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + } + + function keys(object) { + var results = []; + for (var property in object) + results.push(property); + return results; + } + + function values(object) { + var results = []; + for (var property in object) + results.push(object[property]); + return results; + } + + function clone(object) { + return extend({ }, object); + } + + function isElement(object) { + return !!(object && object.nodeType == 1); + } + + function isArray(object) { + return _toString.call(object) == "[object Array]"; + } + + + function isHash(object) { + return object instanceof Hash; + } + + function isFunction(object) { + return typeof object === "function"; + } + + function isString(object) { + return _toString.call(object) == "[object String]"; + } + + function isNumber(object) { + return _toString.call(object) == "[object Number]"; + } + + function isUndefined(object) { + return typeof object === "undefined"; + } + + extend(Object, { + extend: extend, + inspect: inspect, + toJSON: toJSON, + toQueryString: toQueryString, + toHTML: toHTML, + keys: keys, + values: values, + clone: clone, + isElement: isElement, + isArray: isArray, + isHash: isHash, + isFunction: isFunction, + isString: isString, + isNumber: isNumber, + isUndefined: isUndefined + }); +})(); +Object.extend(Function.prototype, (function() { + var slice = Array.prototype.slice; + + function update(array, args) { + var arrayLength = array.length, length = args.length; + while (length--) array[arrayLength + length] = args[length]; + return array; + } + + function merge(array, args) { + array = slice.call(array, 0); + return update(array, args); + } + + function argumentNames() { + var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] + .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') + .replace(/\s+/g, '').split(','); + return names.length == 1 && !names[0] ? [] : names; + } + + function bind(context) { + if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; + var __method = this, args = slice.call(arguments, 1); + return function() { + var a = merge(args, arguments); + return __method.apply(context, a); + } + } + + function bindAsEventListener(context) { + var __method = this, args = slice.call(arguments, 1); + return function(event) { + var a = update([event || window.event], args); + return __method.apply(context, a); + } + } + + function curry() { + if (!arguments.length) return this; + var __method = this, args = slice.call(arguments, 0); + return function() { + var a = merge(args, arguments); + return __method.apply(this, a); + } + } + + function delay(timeout) { + var __method = this, args = slice.call(arguments, 1); + timeout = timeout * 1000 + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + } + + function defer() { + var args = update([0.01], arguments); + return this.delay.apply(this, args); + } + + function wrap(wrapper) { + var __method = this; + return function() { + var a = update([__method.bind(this)], arguments); + return wrapper.apply(this, a); + } + } + + function methodize() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + var a = update([this], arguments); + return __method.apply(null, a); + }; + } + + return { + argumentNames: argumentNames, + bind: bind, + bindAsEventListener: bindAsEventListener, + curry: curry, + delay: delay, + defer: defer, + wrap: wrap, + methodize: methodize + } +})()); + + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; +var PeriodicalExecuter = Class.create({ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + this.currentlyExecuting = false; + } catch(e) { + this.currentlyExecuting = false; + throw e; + } + } + } +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, (function() { + + function prepareReplacement(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; + } + + function gsub(pattern, replacement) { + var result = '', source = this, match; + replacement = prepareReplacement(replacement); + + if (Object.isString(pattern)) + pattern = RegExp.escape(pattern); + + if (!(pattern.length || pattern.source)) { + replacement = replacement(''); + return replacement + source.split('').join(replacement) + replacement; + } + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + } + + function sub(pattern, replacement, count) { + replacement = prepareReplacement(replacement); + count = Object.isUndefined(count) ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + } + + function scan(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + } + + function truncate(length, truncation) { + length = length || 30; + truncation = Object.isUndefined(truncation) ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + } + + function strip() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + } + + function stripTags() { + return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, ''); + } + + function stripScripts() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + } + + function extractScripts() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + } + + function evalScripts() { + return this.extractScripts().map(function(script) { return eval(script) }); + } + + function escapeHTML() { + return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + } + + function unescapeHTML() { + return this.stripTags().replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&'); + } + + + function toQueryParams(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return { }; + + return match[1].split(separator || '&').inject({ }, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; + }); + } + + function toArray() { + return this.split(''); + } + + function succ() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + } + + function times(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + } + + function camelize() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + } + + function capitalize() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + } + + function underscore() { + return this.replace(/::/g, '/') + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') + .replace(/([a-z\d])([A-Z])/g, '$1_$2') + .replace(/-/g, '_') + .toLowerCase(); + } + + function dasherize() { + return this.replace(/_/g, '-'); + } + + function inspect(useDoubleQuotes) { + var escapedString = this.replace(/[\x00-\x1f\\]/g, function(character) { + if (character in String.specialChar) { + return String.specialChar[character]; + } + return '\\u00' + character.charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + } + + function toJSON() { + return this.inspect(true); + } + + function unfilterJSON(filter) { + return this.replace(filter || Prototype.JSONFilter, '$1'); + } + + function isJSON() { + var str = this; + if (str.blank()) return false; + str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + } + + function evalJSON(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + } + + function include(pattern) { + return this.indexOf(pattern) > -1; + } + + function startsWith(pattern) { + return this.indexOf(pattern) === 0; + } + + function endsWith(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + } + + function empty() { + return this == ''; + } + + function blank() { + return /^\s*$/.test(this); + } + + function interpolate(object, pattern) { + return new Template(this, pattern).evaluate(object); + } + + return { + gsub: gsub, + sub: sub, + scan: scan, + truncate: truncate, + strip: String.prototype.trim ? String.prototype.trim : strip, + stripTags: stripTags, + stripScripts: stripScripts, + extractScripts: extractScripts, + evalScripts: evalScripts, + escapeHTML: escapeHTML, + unescapeHTML: unescapeHTML, + toQueryParams: toQueryParams, + parseQuery: toQueryParams, + toArray: toArray, + succ: succ, + times: times, + camelize: camelize, + capitalize: capitalize, + underscore: underscore, + dasherize: dasherize, + inspect: inspect, + toJSON: toJSON, + unfilterJSON: unfilterJSON, + isJSON: isJSON, + evalJSON: evalJSON, + include: include, + startsWith: startsWith, + endsWith: endsWith, + empty: empty, + blank: blank, + interpolate: interpolate + }; +})()); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (object && Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return (match[1] + ''); + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; + +var Enumerable = (function() { + function each(iterator, context) { + var index = 0; + try { + this._each(function(value) { + iterator.call(context, value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + } + + function eachSlice(number, iterator, context) { + var index = -number, slices = [], array = this.toArray(); + if (number < 1) return array; + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + } + + function all(iterator, context) { + iterator = iterator || Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator.call(context, value, index); + if (!result) throw $break; + }); + return result; + } + + function any(iterator, context) { + iterator = iterator || Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator.call(context, value, index)) + throw $break; + }); + return result; + } + + function collect(iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator.call(context, value, index)); + }); + return results; + } + + function detect(iterator, context) { + var result; + this.each(function(value, index) { + if (iterator.call(context, value, index)) { + result = value; + throw $break; + } + }); + return result; + } + + function findAll(iterator, context) { + var results = []; + this.each(function(value, index) { + if (iterator.call(context, value, index)) + results.push(value); + }); + return results; + } + + function grep(filter, iterator, context) { + iterator = iterator || Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(RegExp.escape(filter)); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator.call(context, value, index)); + }); + return results; + } + + function include(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + } + + function inGroupsOf(number, fillWith) { + fillWith = Object.isUndefined(fillWith) ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + } + + function inject(memo, iterator, context) { + this.each(function(value, index) { + memo = iterator.call(context, memo, value, index); + }); + return memo; + } + + function invoke(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + } + + function max(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index); + if (result == null || value >= result) + result = value; + }); + return result; + } + + function min(iterator, context) { + iterator = iterator || Prototype.K; + var result; + this.each(function(value, index) { + value = iterator.call(context, value, index); + if (result == null || value < result) + result = value; + }); + return result; + } + + function partition(iterator, context) { + iterator = iterator || Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator.call(context, value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + } + + function pluck(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + } + + function reject(iterator, context) { + var results = []; + this.each(function(value, index) { + if (!iterator.call(context, value, index)) + results.push(value); + }); + return results; + } + + function sortBy(iterator, context) { + return this.map(function(value, index) { + return { + value: value, + criteria: iterator.call(context, value, index) + }; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + } + + function toArray() { + return this.map(); + } + + function zip() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + } + + function size() { + return this.toArray().length; + } + + function inspect() { + return '#<Enumerable:' + this.toArray().inspect() + '>'; + } + + + + + + + + + + return { + each: each, + eachSlice: eachSlice, + all: all, + every: all, + any: any, + some: any, + collect: collect, + map: collect, + detect: detect, + findAll: findAll, + select: findAll, + filter: findAll, + grep: grep, + include: include, + member: include, + inGroupsOf: inGroupsOf, + inject: inject, + invoke: invoke, + max: max, + min: min, + partition: partition, + pluck: pluck, + reject: reject, + sortBy: sortBy, + toArray: toArray, + entries: toArray, + zip: zip, + size: size, + inspect: inspect, + find: detect + }; +})(); +function $A(iterable) { + if (!iterable) return []; + if ('toArray' in Object(iterable)) return iterable.toArray(); + var length = iterable.length || 0, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +function $w(string) { + if (!Object.isString(string)) return []; + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +Array.from = $A; + + +(function() { + var arrayProto = Array.prototype, + slice = arrayProto.slice, + _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available + + function each(iterator) { + for (var i = 0, length = this.length; i < length; i++) + iterator(this[i]); + } + if (!_each) _each = each; + + function clear() { + this.length = 0; + return this; + } + + function first() { + return this[0]; + } + + function last() { + return this[this.length - 1]; + } + + function compact() { + return this.select(function(value) { + return value != null; + }); + } + + function flatten() { + return this.inject([], function(array, value) { + if (Object.isArray(value)) + return array.concat(value.flatten()); + array.push(value); + return array; + }); + } + + function without() { + var values = slice.call(arguments, 0); + return this.select(function(value) { + return !values.include(value); + }); + } + + function reverse(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + } + + function uniq(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + } + + function intersect(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); + }); + } + + + function clone() { + return slice.call(this, 0); + } + + function size() { + return this.length; + } + + function inspect() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } + + function toJSON() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (!Object.isUndefined(value)) results.push(value); + }); + return '[' + results.join(', ') + ']'; + } + + function indexOf(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; + } + + function lastIndexOf(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; + } + + function concat() { + var array = slice.call(this, 0), item; + for (var i = 0, length = arguments.length; i < length; i++) { + item = arguments[i]; + if (Object.isArray(item) && !('callee' in item)) { + for (var j = 0, arrayLength = item.length; j < arrayLength; j++) + array.push(item[j]); + } else { + array.push(item); + } + } + return array; + } + + Object.extend(arrayProto, Enumerable); + + if (!arrayProto._reverse) + arrayProto._reverse = arrayProto.reverse; + + Object.extend(arrayProto, { + _each: _each, + clear: clear, + first: first, + last: last, + compact: compact, + flatten: flatten, + without: without, + reverse: reverse, + uniq: uniq, + intersect: intersect, + clone: clone, + toArray: clone, + size: size, + inspect: inspect, + toJSON: toJSON + }); + + var CONCAT_ARGUMENTS_BUGGY = (function() { + return [].concat(arguments)[0][0] !== 1; + })(1,2) + + if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat; + + if (!arrayProto.indexOf) arrayProto.indexOf = indexOf; + if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf; +})(); +function $H(object) { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + function initialize(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + } + + function _each(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + } + + function set(key, value) { + return this._object[key] = value; + } + + function get(key) { + if (this._object[key] !== Object.prototype[key]) + return this._object[key]; + } + + function unset(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + } + + function toObject() { + return Object.clone(this._object); + } + + function keys() { + return this.pluck('key'); + } + + function values() { + return this.pluck('value'); + } + + function index(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + } + + function merge(object) { + return this.clone().update(object); + } + + function update(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + } + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + function toQueryString() { + return this.inject([], function(results, pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return results.concat(values.map(toQueryPair.curry(key))); + } else results.push(toQueryPair(key, values)); + return results; + }).join('&'); + } + + function inspect() { + return '#<Hash:{' + this.map(function(pair) { + return pair.map(Object.inspect).join(': '); + }).join(', ') + '}>'; + } + + function toJSON() { + return Object.toJSON(this.toObject()); + } + + function clone() { + return new Hash(this); + } + + return { + initialize: initialize, + _each: _each, + set: set, + get: get, + unset: unset, + toObject: toObject, + toTemplateReplacements: toObject, + keys: keys, + values: values, + index: index, + merge: merge, + update: update, + toQueryString: toQueryString, + inspect: inspect, + toJSON: toJSON, + clone: clone + }; +})()); + +Hash.from = $H; +Object.extend(Number.prototype, (function() { + function toColorPart() { + return this.toPaddedString(2, 16); + } + + function succ() { + return this + 1; + } + + function times(iterator, context) { + $R(0, this, true).each(iterator, context); + return this; + } + + function toPaddedString(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + } + + function toJSON() { + return isFinite(this) ? this.toString() : 'null'; + } + + function abs() { + return Math.abs(this); + } + + function round() { + return Math.round(this); + } + + function ceil() { + return Math.ceil(this); + } + + function floor() { + return Math.floor(this); + } + + return { + toColorPart: toColorPart, + succ: succ, + times: times, + toPaddedString: toPaddedString, + toJSON: toJSON, + abs: abs, + round: round, + ceil: ceil, + floor: floor + }; +})()); + +function $R(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var ObjectRange = Class.create(Enumerable, (function() { + function initialize(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + } + + function _each(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + } + + function include(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } + + return { + initialize: initialize, + _each: _each, + include: include + }; +})()); + + + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +}; + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (Object.isFunction(responder[callback])) { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } +}); +Ajax.Base = Class.create({ + initialize: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); + + this.options.method = this.options.method.toLowerCase(); + + if (Object.isString(this.options.parameters)) + this.options.parameters = this.options.parameters.toQueryParams(); + else if (Object.isHash(this.options.parameters)) + this.options.parameters = this.options.parameters.toObject(); + } +}); +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, + + initialize: function($super, url, options) { + $super(options); + this.transport = Ajax.getTransport(); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); + + if (!['get', 'post'].include(this.method)) { + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Object.toQueryString(params)) { + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } + + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); + }, + + getStatus: function() { + try { + return this.transport.status || 0; + } catch (e) { return 0 } + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && this.isSameOrigin() && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + isSameOrigin: function() { + var m = this.url.match(/^\s*https?:\/\/[^\/]*/); + return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({ + protocol: location.protocol, + domain: document.domain, + port: location.port ? ':' + location.port : '' + })); + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name) || null; + } catch (e) { return null; } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + + + + + + + + +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = Object.isUndefined(xml) ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + status: 0, + + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + getHeader: Ajax.Request.prototype.getHeader, + + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json')) || + this.responseText.blank()) + return null; + try { + return this.responseText.evalJSON(options.sanitizeJSON || + !this.request.isSameOrigin()); + } catch (e) { + this.request.dispatchException(e); + } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + }; + + options = Object.clone(options); + var onComplete = options.onComplete; + options.onComplete = (function(response, json) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, json); + }).bind(this); + + $super(url, options); + }, + + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; + + if (!options.evalScripts) responseText = responseText.stripScripts(); + + if (receiver = $(receiver)) { + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = { }; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(response) { + if (this.options.decay) { + this.decay = (response.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = response.responseText; + } + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); + + + +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (Object.isString(element)) + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(Element.extend(query.snapshotItem(i))); + return results; + }; +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); +} + + +(function(global) { + + var SETATTRIBUTE_IGNORES_NAME = (function(){ + var elForm = document.createElement("form"); + var elInput = document.createElement("input"); + var root = document.documentElement; + elInput.setAttribute("name", "test"); + elForm.appendChild(elInput); + root.appendChild(elForm); + var isBuggy = elForm.elements + ? (typeof elForm.elements.test == "undefined") + : null; + root.removeChild(elForm); + elForm = elInput = null; + return isBuggy; + })(); + + var element = global.Element; + global.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (SETATTRIBUTE_IGNORES_NAME && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); + } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(global.Element, element || { }); + if (element) global.Element.prototype = element.prototype; +})(this); + +Element.cache = { }; +Element.idCounter = 1; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + + hide: function(element) { + element = $(element); + element.style.display = 'none'; + return element; + }, + + show: function(element) { + element = $(element); + element.style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: (function(){ + + var SELECT_ELEMENT_INNERHTML_BUGGY = (function(){ + var el = document.createElement("select"), + isBuggy = true; + el.innerHTML = "<option value=\"test\">test</option>"; + if (el.options && el.options[0]) { + isBuggy = el.options[0].nodeName.toUpperCase() !== "OPTION"; + } + el = null; + return isBuggy; + })(); + + var TABLE_ELEMENT_INNERHTML_BUGGY = (function(){ + try { + var el = document.createElement("table"); + if (el && el.tBodies) { + el.innerHTML = "<tbody><tr><td>test</td></tr></tbody>"; + var isBuggy = typeof el.tBodies[0] == "undefined"; + el = null; + return isBuggy; + } + } catch (e) { + return true; + } + })(); + + var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () { + var s = document.createElement("script"), + isBuggy = false; + try { + s.appendChild(document.createTextNode("")); + isBuggy = !s.firstChild || + s.firstChild && s.firstChild.nodeType !== 3; + } catch (e) { + isBuggy = true; + } + s = null; + return isBuggy; + })(); + + function update(element, content) { + element = $(element); + + if (content && content.toElement) + content = content.toElement(); + + if (Object.isElement(content)) + return element.update().insert(content); + + content = Object.toHTML(content); + + var tagName = element.tagName.toUpperCase(); + + if (tagName === 'SCRIPT' && SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING) { + element.text = content; + return element; + } + + if (SELECT_ELEMENT_INNERHTML_BUGGY || TABLE_ELEMENT_INNERHTML_BUGGY) { + if (tagName in Element._insertionTranslations.tags) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { + element.appendChild(node) + }); + } + else { + element.innerHTML = content.stripScripts(); + } + } + else { + element.innerHTML = content.stripScripts(); + } + + content.evalScripts.bind(content).defer(); + return element; + } + + return update; + })(), + + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, insert, tagName, childNodes; + + for (var position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + insert = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + insert(element, content); + continue; + } + + content = Object.toHTML(content); + + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + + if (position == 'top' || position == 'after') childNodes.reverse(); + childNodes.each(insert.curry(element)); + + content.evalScripts.bind(content).defer(); + } + + return element; + }, + + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return Element.recursivelyCollect(element, 'parentNode'); + }, + + descendants: function(element) { + return Element.select(element, "*"); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return Element.recursivelyCollect(element, 'previousSibling'); + }, + + nextSiblings: function(element) { + return Element.recursivelyCollect(element, 'nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return Element.previousSiblings(element).reverse() + .concat(Element.nextSiblings(element)); + }, + + match: function(element, selector) { + if (Object.isString(selector)) + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = Element.ancestors(element); + return Object.isNumber(expression) ? ancestors[expression] : + Selector.findElement(ancestors, expression, index); + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return Element.firstDescendant(element); + return Object.isNumber(expression) ? Element.descendants(element)[expression] : + Element.select(element, expression)[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = Element.previousSiblings(element); + return Object.isNumber(expression) ? previousSiblings[expression] : + Selector.findElement(previousSiblings, expression, index); + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = Element.nextSiblings(element); + return Object.isNumber(expression) ? nextSiblings[expression] : + Selector.findElement(nextSiblings, expression, index); + }, + + + select: function(element) { + var args = Array.prototype.slice.call(arguments, 1); + return Selector.findChildElements(element, args); + }, + + adjacent: function(element) { + var args = Array.prototype.slice.call(arguments, 1); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = Element.readAttribute(element, 'id'); + if (id) return id; + do { id = 'anonymous_element_' + Element.idCounter++ } while ($(id)); + Element.writeAttribute(element, 'id', id); + return id; + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } + } + return element.getAttribute(name); + }, + + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = Object.isUndefined(value) ? true : value; + + for (var attr in attributes) { + name = t.names[attr] || attr; + value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + + getHeight: function(element) { + return Element.getDimensions(element).height; + }, + + getWidth: function(element) { + return Element.getDimensions(element).width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + if (!Element.hasClassName(element, className)) + element.className += (element.className ? ' ' : '') + className; + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + return Element[Element.hasClassName(element, className) ? + 'removeClassName' : 'addClassName'](element, className); + }, + + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (ancestor.contains) + return ancestor.contains(element) && ancestor !== element; + + while (element = element.parentNode) + if (element == ancestor) return true; + + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = Element.cumulativeOffset(element); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value || value == 'auto') { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; + }, + + setOpacity: function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = Element.getStyle(element, 'display'); + if (display != 'none' && display != null) // Safari bug + return {width: element.offsetWidth, height: element.offsetHeight}; + + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + var originalDisplay = els.display; + els.visibility = 'hidden'; + if (originalPosition != 'fixed') // Switching fixed to absolute causes issues in Safari + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + if (Prototype.Browser.Opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName.toUpperCase() == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (Element.getStyle(element, 'position') == 'absolute') return element; + + var offsets = Element.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (Element.getStyle(element, 'position') == 'relative') return element; + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); + }, + + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + source = $(source); + var p = Element.viewportOffset(source); + + element = $(element); + var delta = [0, 0]; + var parent = null; + if (Element.getStyle(element, 'position') == 'absolute') { + parent = Element.getOffsetParent(element); + delta = Element.viewportOffset(parent); + } + + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + + childElements: Element.Methods.immediateDescendants +}); + +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; + +if (Prototype.Browser.Opera) { + Element.Methods.getStyle = Element.Methods.getStyle.wrap( + function(proceed, element, style) { + switch (style) { + case 'left': case 'top': case 'right': case 'bottom': + if (proceed(element, 'position') === 'static') return null; + case 'height': case 'width': + if (!Element.visible(element)) return null; + + var dim = parseInt(proceed(element, style), 10); + + if (dim !== element['offset' + style.capitalize()]) + return dim + 'px'; + + var properties; + if (style === 'height') { + properties = ['border-top-width', 'padding-top', + 'padding-bottom', 'border-bottom-width']; + } + else { + properties = ['border-left-width', 'padding-left', + 'padding-right', 'border-right-width']; + } + return properties.inject(dim, function(memo, property) { + var val = proceed(element, property); + return val === null ? memo : memo - parseInt(val, 10); + }) + 'px'; + default: return proceed(element, style); + } + } + ); + + Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( + function(proceed, element, attribute) { + if (attribute === 'title') return element.title; + return proceed(element, attribute); + } + ); +} + +else if (Prototype.Browser.IE) { + Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( + function(proceed, element) { + element = $(element); + try { element.offsetParent } + catch(e) { return $(document.body) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + + $w('positionedOffset viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); + var offsetParent = element.getOffsetParent(); + if (offsetParent && offsetParent.getStyle('position') === 'fixed') + offsetParent.setStyle({ zoom: 1 }); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + }); + + Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap( + function(proceed, element) { + try { element.offsetParent } + catch(e) { return Element._returnOffset(0,0) } + return proceed(element); + } + ); + + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = (function(){ + + var classProp = 'className'; + var forProp = 'for'; + + var el = document.createElement('div'); + + el.setAttribute(classProp, 'x'); + + if (el.className !== 'x') { + el.setAttribute('class', 'x'); + if (el.className === 'x') { + classProp = 'class'; + } + } + el = null; + + el = document.createElement('label'); + el.setAttribute(forProp, 'x'); + if (el.htmlFor !== 'x') { + el.setAttribute('htmlFor', 'x'); + if (el.htmlFor === 'x') { + forProp = 'htmlFor'; + } + } + el = null; + + return { + read: { + names: { + 'class': classProp, + 'className': classProp, + 'for': forProp, + 'htmlFor': forProp + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute); + }, + _getAttr2: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: (function(){ + + var el = document.createElement('div'); + el.onclick = Prototype.emptyFunction; + var value = el.getAttribute('onclick'); + var f; + + if (String(value).indexOf('{') > -1) { + f = function(element, attribute) { + attribute = element.getAttribute(attribute); + if (!attribute) return null; + attribute = attribute.toString(); + attribute = attribute.split('{')[1]; + attribute = attribute.split('}')[0]; + return attribute.strip(); + }; + } + else if (value === '') { + f = function(element, attribute) { + attribute = element.getAttribute(attribute); + if (!attribute) return null; + return attribute.strip(); + }; + } + el = null; + return f; + })(), + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; + } + } + } + } + })(); + + Element._attributeTranslations.write = { + names: Object.extend({ + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing' + }, Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc frameBorder').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr2, + src: v._getAttr2, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); + + if (Prototype.BrowserFeatures.ElementExtensions) { + (function() { + function _descendants(element) { + var nodes = element.getElementsByTagName('*'), results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName !== "!") // Filter out comment nodes. + results.push(node); + return results; + } + + Element.Methods.down = function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + return Object.isNumber(expression) ? _descendants(element)[expression] : + Element.select(element, expression)[index || 0]; + } + })(); + } + +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName.toUpperCase() == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + Element.Methods.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return Element._returnOffset(valueL, valueT); + }; +} + +if ('outerHTML' in document.documentElement) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + if (t) { + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + } else div.innerHTML = html; + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + top: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + bottom: function(element, node) { + element.appendChild(node); + }, + after: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + tags: { + TABLE: ['<table>', '</table>', 1], + TBODY: ['<table><tbody>', '</tbody></table>', 2], + TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3], + TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4], + SELECT: ['<select>', '</select>', 1] + } +}; + +(function() { + var tags = Element._insertionTranslations.tags; + Object.extend(tags, { + THEAD: tags.TBODY, + TFOOT: tags.TBODY, + TH: tags.TD + }); +})(); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return !!(node && node.specified); + } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +(function(div) { + + if (!Prototype.BrowserFeatures.ElementExtensions && div['__proto__']) { + window.HTMLElement = { }; + window.HTMLElement.prototype = div['__proto__']; + Prototype.BrowserFeatures.ElementExtensions = true; + } + + div = null; + +})(document.createElement('div')) + +Element.extend = (function() { + + function checkDeficiency(tagName) { + if (typeof window.Element != 'undefined') { + var proto = window.Element.prototype; + if (proto) { + var id = '_' + (Math.random()+'').slice(2); + var el = document.createElement(tagName); + proto[id] = 'x'; + var isBuggy = (el[id] !== 'x'); + delete proto[id]; + el = null; + return isBuggy; + } + } + return false; + } + + function extendElementWith(element, methods) { + for (var property in methods) { + var value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + } + + var HTMLOBJECTELEMENT_PROTOTYPE_BUGGY = checkDeficiency('object'); + + if (Prototype.BrowserFeatures.SpecificElementExtensions) { + if (HTMLOBJECTELEMENT_PROTOTYPE_BUGGY) { + return function(element) { + if (element && typeof element._extendedByPrototype == 'undefined') { + var t = element.tagName; + if (t && (/^(?:object|applet|embed)$/i.test(t))) { + extendElementWith(element, Element.Methods); + extendElementWith(element, Element.Methods.Simulated); + extendElementWith(element, Element.Methods.ByTag[t.toUpperCase()]); + } + } + return element; + } + } + return Prototype.K; + } + + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || typeof element._extendedByPrototype != 'undefined' || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName.toUpperCase(); + + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + extendElementWith(element, methods); + + element._extendedByPrototype = Prototype.emptyFunction; + return element; + + }, { + refresh: function() { + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); + } + + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } + } + + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + var element = document.createElement(tagName); + var proto = element['__proto__'] || element.constructor.prototype; + element = null; + return proto; + } + + var elementPrototype = window.HTMLElement ? HTMLElement.prototype : + Element.prototype; + + if (F.ElementExtensions) { + copy(Element.Methods, elementPrototype); + copy(Element.Methods.Simulated, elementPrototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; + + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; + + +document.viewport = { + + getDimensions: function() { + return { width: this.getWidth(), height: this.getHeight() }; + }, + + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); + } +}; + +(function(viewport) { + var B = Prototype.Browser, doc = document, element, property = {}; + + function getRootElement() { + if (B.WebKit && !doc.evaluate) + return document; + + if (B.Opera && window.parseFloat(window.opera.version()) < 9.5) + return document.body; + + return document.documentElement; + } + + function define(D) { + if (!element) element = getRootElement(); + + property[D] = 'client' + D; + + viewport['get' + D] = function() { return element[property[D]] }; + return viewport['get' + D](); + } + + viewport.getWidth = define.curry('Width'); + + viewport.getHeight = define.curry('Height'); +})(document.viewport); + + +Element.Storage = { + UID: 1 +}; + +Element.addMethods({ + getStorage: function(element) { + if (!(element = $(element))) return; + + var uid; + if (element === window) { + uid = 0; + } else { + if (typeof element._prototypeUID === "undefined") + element._prototypeUID = [Element.Storage.UID++]; + uid = element._prototypeUID[0]; + } + + if (!Element.Storage[uid]) + Element.Storage[uid] = $H(); + + return Element.Storage[uid]; + }, + + store: function(element, key, value) { + if (!(element = $(element))) return; + + if (arguments.length === 2) { + Element.getStorage(element).update(key); + } else { + Element.getStorage(element).set(key, value); + } + + return element; + }, + + retrieve: function(element, key, defaultValue) { + if (!(element = $(element))) return; + var hash = Element.getStorage(element), value = hash.get(key); + + if (Object.isUndefined(value)) { + hash.set(key, defaultValue); + value = defaultValue; + } + + return value; + }, + + clone: function(element, deep) { + if (!(element = $(element))) return; + var clone = element.cloneNode(deep); + clone._prototypeUID = void 0; + if (deep) { + var descendants = Element.select(clone, '*'), + i = descendants.length; + while (i--) { + descendants[i]._prototypeUID = void 0; + } + } + return Element.extend(clone); + } +}); +/* Portions of the Selector class are derived from Jack Slocum's DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + + if (this.shouldUseSelectorsAPI()) { + this.mode = 'selectorsAPI'; + } else if (this.shouldUseXPath()) { + this.mode = 'xpath'; + this.compileXPathMatcher(); + } else { + this.mode = "normal"; + this.compileMatcher(); + } + + }, + + shouldUseXPath: (function() { + + var IS_DESCENDANT_SELECTOR_BUGGY = (function(){ + var isBuggy = false; + if (document.evaluate && window.XPathResult) { + var el = document.createElement('div'); + el.innerHTML = '<ul><li></li></ul><div><ul><li></li></ul></div>'; + + var xpath = ".//*[local-name()='ul' or local-name()='UL']" + + "//*[local-name()='li' or local-name()='LI']"; + + var result = document.evaluate(xpath, el, null, + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + + isBuggy = (result.snapshotLength !== 2); + el = null; + } + return isBuggy; + })(); + + return function() { + if (!Prototype.BrowserFeatures.XPath) return false; + + var e = this.expression; + + if (Prototype.Browser.WebKit && + (e.include("-of-type") || e.include(":empty"))) + return false; + + if ((/(\[[\w-]*?:|:checked)/).test(e)) + return false; + + if (IS_DESCENDANT_SELECTOR_BUGGY) return false; + + return true; + } + + })(), + + shouldUseSelectorsAPI: function() { + if (!Prototype.BrowserFeatures.SelectorsAPI) return false; + + if (Selector.CASE_INSENSITIVE_CLASS_NAMES) return false; + + if (!Selector._div) Selector._div = new Element('div'); + + try { + Selector._div.querySelector(this.expression); + } catch(e) { + return false; + } + + return true; + }, + + compileMatcher: function() { + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m, len = ps.length, name; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i = 0; i<len; i++) { + p = ps[i].re; + name = ps[i].name; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[name]) ? c[name](m) : + new Template(c[name]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; + }, + + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m, len = ps.length, name; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i = 0; i<len; i++) { + name = ps[i].name; + if (m = e.match(ps[i].re)) { + this.matcher.push(Object.isFunction(x[name]) ? x[name](m) : + new Template(x[name]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; + }, + + findElements: function(root) { + root = root || document; + var e = this.expression, results; + + switch (this.mode) { + case 'selectorsAPI': + if (root !== document) { + var oldId = root.id, id = $(root).identify(); + id = id.replace(/([\.:])/g, "\\$1"); + e = "#" + id + " " + e; + } + + results = $A(root.querySelectorAll(e)).map(Element.extend); + root.id = oldId; + + return results; + case 'xpath': + return document._getElementsByXPath(this.xpath, root); + default: + return this.matcher(root); + } + }, + + match: function(element) { + this.tokens = []; + + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m, len = ps.length, name; + + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i = 0; i<len; i++) { + p = ps[i].re; + name = ps[i].name; + if (m = e.match(p)) { + if (as[name]) { + this.tokens.push([name, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + return this.findElements(document).include(element); + } + } + } + } + + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } + + return match; + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#<Selector:" + this.expression.inspect() + ">"; + } +}); + +if (Prototype.BrowserFeatures.SelectorsAPI && + document.compatMode === 'BackCompat') { + Selector.CASE_INSENSITIVE_CLASS_NAMES = (function(){ + var div = document.createElement('div'), + span = document.createElement('span'); + + div.id = "prototype_test_id"; + span.className = 'Test'; + div.appendChild(span); + var isIgnored = (div.querySelector('#prototype_test_id .test') !== null); + div = span = null; + return isIgnored; + })(); +} + +Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: function(m) { + m[1] = m[1].toLowerCase(); + return new Template("[@#{1}]").evaluate(m); + }, + attr: function(m) { + m[1] = m[1].toLowerCase(); + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0)]", + 'checked': "[@checked]", + 'disabled': "[(@disabled) and (@type!='hidden')]", + 'enabled': "[not(@disabled) and (@type!='hidden')]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, v, len = p.length, name; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i = 0; i<len; i++) { + name = p[i].name + if (m = e.match(p[i].re)) { + v = Object.isFunction(x[name]) ? x[name](m) : new Template(x[name]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: [ + { name: 'laterSibling', re: /^\s*~\s*/ }, + { name: 'child', re: /^\s*>\s*/ }, + { name: 'adjacent', re: /^\s*\+\s*/ }, + { name: 'descendant', re: /^\s/ }, + + { name: 'tagName', re: /^\s*(\*|[\w\-]+)(\b|$)?/ }, + { name: 'id', re: /^#([\w\-\*]+)(\b|$)/ }, + { name: 'className', re: /^\.([\w\-\*]+)(\b|$)/ }, + { name: 'pseudo', re: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/ }, + { name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ }, + { name: 'attr', re: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ } + ], + + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); + } + }, + + handlers: { + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + mark: function(nodes) { + var _true = Prototype.emptyFunction; + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = _true; + return nodes; + }, + + unmark: (function(){ + + var PROPERTIES_ATTRIBUTES_MAP = (function(){ + var el = document.createElement('div'), + isBuggy = false, + propName = '_countedByPrototype', + value = 'x' + el[propName] = value; + isBuggy = (el.getAttribute(propName) === value); + el = null; + return isBuggy; + })(); + + return PROPERTIES_ATTRIBUTES_MAP ? + function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node.removeAttribute('_countedByPrototype'); + return nodes; + } : + function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._countedByPrototype = void 0; + return nodes; + } + })(), + + index: function(parentNode, reverse, ofType) { + parentNode._countedByPrototype = Prototype.emptyFunction; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; + } + }, + + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (typeof (n = nodes[i])._countedByPrototype == 'undefined') { + n._countedByPrototype = Prototype.emptyFunction; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + tagName: function(nodes, root, tagName, combinator) { + var uTagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() === uTagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + + if (root == document) { + if (!targetNode) return []; + if (!nodes) return [targetNode]; + } else { + if (!root.sourceIndex || root.sourceIndex < 1) { + var nodes = root.getElementsByTagName('*'); + for (var j = 0, node; node = nodes[j]; j++) { + if (node.id === id) return [node]; + } + } + } + + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator, combinator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + if (nodes && combinator) nodes = this[combinator](nodes); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._countedByPrototype) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (node.tagName == '!' || node.firstChild) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._countedByPrototype) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled && (!node.type || node.type !== 'hidden')) + results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, + '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, + '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + + '-').include('-' + (v || "").toUpperCase() + '-'); } + }, + + split: function(expression) { + var expressions = []; + expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + return expressions; + }, + + matchElements: function(elements, expression) { + var matches = $$(expression), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._countedByPrototype) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + expressions = Selector.split(expressions.join(',')); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); + +if (Prototype.Browser.IE) { + Object.extend(Selector.handlers, { + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + } + }); +} + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} + +var Form = { + reset: function(form) { + form = $(form); + form.reset(); + return form; + }, + + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (Object.isUndefined(options.hash)) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { + if (!element.disabled && element.name) { + key = element.name; value = $(element).getValue(); + if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return options.hash ? data : Object.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); + }, + + getElements: function(form) { + var elements = $(form).getElementsByTagName('*'), + element, + arr = [ ], + serializers = Form.Element.Serializers; + for (var i = 0; element = elements[i]; i++) { + arr.push(element); + } + return arr.inject([], function(elements, child) { + if (serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + }) + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + Form.getElements(form).invoke('disable'); + return form; + }, + + enable: function(form) { + form = $(form); + Form.getElements(form).invoke('enable'); + return form; + }, + + findFirstElement: function(form) { + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return /^(?:input|select|textarea)$/i.test(element.tagName); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); + + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); + } +}; + +/*--------------------------------------------------------------------------*/ + + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +}; + +Form.Element.Methods = { + + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = { }; + pair[element.name] = value; + return Object.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !(/^(?:button|reset|submit)$/i.test(element.type)))) + element.select(); + } catch (e) { } + return element; + }, + + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; + +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); + } + }, + + inputSelector: function(element, value) { + if (Object.isUndefined(value)) return element.checked ? element.value : null; + else element.checked = !!value; + }, + + textarea: function(element, value) { + if (Object.isUndefined(value)) return element.value; + else element.value = value; + }, + + select: function(element, value) { + if (Object.isUndefined(value)) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, currentValue, single = !Object.isArray(value); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + currentValue = this.optionValue(opt); + if (single) { + if (currentValue == value) { + opt.selected = true; + return; + } + } + else opt.selected = value.include(currentValue); + } + } + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; + +/*--------------------------------------------------------------------------*/ + + +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = Class.create({ + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); +(function() { + + var Event = { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: {} + }; + + var docEl = document.documentElement; + var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl + && 'onmouseleave' in docEl; + + var _isButton; + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + _isButton = function(event, code) { + return event.button === buttonMap[code]; + }; + } else if (Prototype.Browser.WebKit) { + _isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + } else { + _isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } + + function isLeftClick(event) { return _isButton(event, 0) } + + function isMiddleClick(event) { return _isButton(event, 1) } + + function isRightClick(event) { return _isButton(event, 2) } + + function element(event) { + event = Event.extend(event); + + var node = event.target, type = event.type, + currentTarget = event.currentTarget; + + if (currentTarget && currentTarget.tagName) { + if (type === 'load' || type === 'error' || + (type === 'click' && currentTarget.tagName.toLowerCase() === 'input' + && currentTarget.type === 'radio')) + node = currentTarget; + } + + if (node.nodeType == Node.TEXT_NODE) + node = node.parentNode; + + return Element.extend(node); + } + + function findElement(event, expression) { + var element = Event.element(event); + if (!expression) return element; + var elements = [element].concat(element.ancestors()); + return Selector.findElement(elements, expression, 0); + } + + function pointer(event) { + return { x: pointerX(event), y: pointerY(event) }; + } + + function pointerX(event) { + var docElement = document.documentElement, + body = document.body || { scrollLeft: 0 }; + + return event.pageX || (event.clientX + + (docElement.scrollLeft || body.scrollLeft) - + (docElement.clientLeft || 0)); + } + + function pointerY(event) { + var docElement = document.documentElement, + body = document.body || { scrollTop: 0 }; + + return event.pageY || (event.clientY + + (docElement.scrollTop || body.scrollTop) - + (docElement.clientTop || 0)); + } + + + function stop(event) { + Event.extend(event); + event.preventDefault(); + event.stopPropagation(); + + event.stopped = true; + } + + Event.Methods = { + isLeftClick: isLeftClick, + isMiddleClick: isMiddleClick, + isRightClick: isRightClick, + + element: element, + findElement: findElement, + + pointer: pointer, + pointerX: pointerX, + pointerY: pointerY, + + stop: stop + }; + + + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + function _relatedTarget(event) { + var element; + switch (event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } + + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return '[object Event]' } + }); + + Event.extend = function(event, element) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + + Object.extend(event, { + target: event.srcElement || element, + relatedTarget: _relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + + return Object.extend(event, methods); + }; + } else { + Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__; + Object.extend(Event.prototype, methods); + Event.extend = Prototype.K; + } + + function _createResponder(element, eventName, handler) { + var registry = Element.retrieve(element, 'prototype_event_registry'); + + if (Object.isUndefined(registry)) { + CACHE.push(element); + registry = Element.retrieve(element, 'prototype_event_registry', $H()); + } + + var respondersForEvent = registry.get(eventName); + if (Object.isUndefined(respondersForEvent)) { + respondersForEvent = []; + registry.set(eventName, respondersForEvent); + } + + if (respondersForEvent.pluck('handler').include(handler)) return false; + + var responder; + if (eventName.include(":")) { + responder = function(event) { + if (Object.isUndefined(event.eventName)) + return false; + + if (event.eventName !== eventName) + return false; + + Event.extend(event, element); + handler.call(element, event); + }; + } else { + if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED && + (eventName === "mouseenter" || eventName === "mouseleave")) { + if (eventName === "mouseenter" || eventName === "mouseleave") { + responder = function(event) { + Event.extend(event, element); + + var parent = event.relatedTarget; + while (parent && parent !== element) { + try { parent = parent.parentNode; } + catch(e) { parent = element; } + } + + if (parent === element) return; + + handler.call(element, event); + }; + } + } else { + responder = function(event) { + Event.extend(event, element); + handler.call(element, event); + }; + } + } + + responder.handler = handler; + respondersForEvent.push(responder); + return responder; + } + + function _destroyCache() { + for (var i = 0, length = CACHE.length; i < length; i++) { + Event.stopObserving(CACHE[i]); + CACHE[i] = null; + } + } + + var CACHE = []; + + if (Prototype.Browser.IE) + window.attachEvent('onunload', _destroyCache); + + if (Prototype.Browser.WebKit) + window.addEventListener('unload', Prototype.emptyFunction, false); + + + var _getDOMEventName = Prototype.K; + + if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) { + _getDOMEventName = function(eventName) { + var translations = { mouseenter: "mouseover", mouseleave: "mouseout" }; + return eventName in translations ? translations[eventName] : eventName; + }; + } + + function observe(element, eventName, handler) { + element = $(element); + + var responder = _createResponder(element, eventName, handler); + + if (!responder) return element; + + if (eventName.include(':')) { + if (element.addEventListener) + element.addEventListener("dataavailable", responder, false); + else { + element.attachEvent("ondataavailable", responder); + element.attachEvent("onfilterchange", responder); + } + } else { + var actualEventName = _getDOMEventName(eventName); + + if (element.addEventListener) + element.addEventListener(actualEventName, responder, false); + else + element.attachEvent("on" + actualEventName, responder); + } + + return element; + } + + function stopObserving(element, eventName, handler) { + element = $(element); + + var registry = Element.retrieve(element, 'prototype_event_registry'); + + if (Object.isUndefined(registry)) return element; + + if (eventName && !handler) { + var responders = registry.get(eventName); + + if (Object.isUndefined(responders)) return element; + + responders.each( function(r) { + Element.stopObserving(element, eventName, r.handler); + }); + return element; + } else if (!eventName) { + registry.each( function(pair) { + var eventName = pair.key, responders = pair.value; + + responders.each( function(r) { + Element.stopObserving(element, eventName, r.handler); + }); + }); + return element; + } + + var responders = registry.get(eventName); + + if (!responders) return; + + var responder = responders.find( function(r) { return r.handler === handler; }); + if (!responder) return element; + + var actualEventName = _getDOMEventName(eventName); + + if (eventName.include(':')) { + if (element.removeEventListener) + element.removeEventListener("dataavailable", responder, false); + else { + element.detachEvent("ondataavailable", responder); + element.detachEvent("onfilterchange", responder); + } + } else { + if (element.removeEventListener) + element.removeEventListener(actualEventName, responder, false); + else + element.detachEvent('on' + actualEventName, responder); + } + + registry.set(eventName, responders.without(responder)); + + return element; + } + + function fire(element, eventName, memo, bubble) { + element = $(element); + + if (Object.isUndefined(bubble)) + bubble = true; + + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + var event; + if (document.createEvent) { + event = document.createEvent('HTMLEvents'); + event.initEvent('dataavailable', true, true); + } else { + event = document.createEventObject(); + event.eventType = bubble ? 'ondataavailable' : 'onfilterchange'; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) + element.dispatchEvent(event); + else + element.fireEvent(event.eventType, event); + + return Event.extend(event); + } + + + Object.extend(Event, Event.Methods); + + Object.extend(Event, { + fire: fire, + observe: observe, + stopObserving: stopObserving + }); + + Element.addMethods({ + fire: fire, + + observe: observe, + + stopObserving: stopObserving + }); + + Object.extend(document, { + fire: fire.methodize(), + + observe: observe.methodize(), + + stopObserving: stopObserving.methodize(), + + loaded: false + }); + + if (window.Event) Object.extend(window.Event, Event); + else window.Event = Event; +})(); + +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */ + + var timer; + + function fireContentLoadedEvent() { + if (document.loaded) return; + if (timer) window.clearTimeout(timer); + document.loaded = true; + document.fire('dom:loaded'); + } + + function checkReadyState() { + if (document.readyState === 'complete') { + document.stopObserving('readystatechange', checkReadyState); + fireContentLoadedEvent(); + } + } + + function pollDoScroll() { + try { document.documentElement.doScroll('left'); } + catch(e) { + timer = pollDoScroll.defer(); + return; + } + fireContentLoadedEvent(); + } + + if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false); + } else { + document.observe('readystatechange', checkReadyState); + if (window == top) + timer = pollDoScroll.defer(); + } + + Event.observe(window, 'load', fireContentLoadedEvent); +})(); + +Element.addMethods(); + +/*------------------------------- DEPRECATED -------------------------------*/ + +Hash.toQueryString = Object.toQueryString; + +var Toggle = { display: Element.toggle }; + +Element.Methods.childOf = Element.Methods.descendantOf; + +var Insertion = { + Before: function(element, content) { + return Element.insert(element, {before:content}); + }, + + Top: function(element, content) { + return Element.insert(element, {top:content}); + }, + + Bottom: function(element, content) { + return Element.insert(element, {bottom:content}); + }, + + After: function(element, content) { + return Element.insert(element, {after:content}); + } +}; + +var $continue = new Error('"throw $continue" is deprecated, use "return" instead'); + +var Position = { + includeScrollOffsets: false, + + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = Element.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = Element.cumulativeScrollOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = Element.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + + cumulativeOffset: Element.Methods.cumulativeOffset, + + positionedOffset: Element.Methods.positionedOffset, + + absolutize: function(element) { + Position.prepare(); + return Element.absolutize(element); + }, + + relativize: function(element) { + Position.prepare(); + return Element.relativize(element); + }, + + realOffset: Element.Methods.cumulativeScrollOffset, + + offsetParent: Element.Methods.getOffsetParent, + + page: Element.Methods.viewportOffset, + + clone: function(source, target, options) { + options = options || { }; + return Element.clonePosition(target, source, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){ + function iter(name) { + return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]"; + } + + instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ? + function(element, className) { + className = className.toString().strip(); + var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className); + return cond ? document._getElementsByXPath('.//*' + cond, element) : []; + } : function(element, className) { + className = className.toString().strip(); + var elements = [], classNames = (/\s/.test(className) ? $w(className) : null); + if (!classNames && !className) return elements; + + var nodes = $(element).getElementsByTagName('*'); + className = ' ' + className + ' '; + + for (var i = 0, child, cn; child = nodes[i]; i++) { + if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) || + (classNames && classNames.all(function(name) { + return !name.toString().blank() && cn.include(' ' + name + ' '); + })))) + elements.push(Element.extend(child)); + } + return elements; + }; + + return function(className, parentElement) { + return $(parentElement || document.body).getElementsByClassName(className); + }; +}(Element.Methods); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set($A(this).concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set($A(this).without(classNameToRemove).join(' ')); + }, + + toString: function() { + return $A(this).join(' '); + } +}; + +Object.extend(Element.ClassNames.prototype, Enumerable); + +/*--------------------------------------------------------------------------*/ diff --git a/test/alert_and_notification_logic.rb b/test/alert_and_notification_logic.rb new file mode 100644 index 0000000..19b2478 --- /dev/null +++ b/test/alert_and_notification_logic.rb @@ -0,0 +1,391 @@ +# Mauve server tests - alerts and notification logic. Define the basic workings +# so that we know what should happen when we send sequences of alerts at +# different times. +# +# These aren't really unit tests, just narrative specifications as to what +# should happen under what stimuli. I suspect I will break these down into +# smaller units if things break under otherwise difficult conditions. +# + +$: << __FILE__.split("/")[0..-2].join("/") +require 'test/unit' +require 'mauve_test_helper' +require 'mauve_time' + +class AlertAndNotificationLogic < Test::Unit::TestCase + include MauveTestHelper + + def configuration_template + <<-TEMPLATE + # This is the head of all the configuration files. Filenames are relative + # to the cwd, which is assumed to be a fleeting test directory. + + server { + ip "127.0.0.1" + port #{@port_alerts ||= 44444} + log_file ENV['TEST_LOG'] ? STDOUT : "#{dir}/log" + log_level 0 + database "sqlite3:///#{dir}/mauve_test.db" + transmission_id_expire_time 600 + + # doesn't restart nicely at the moment + #web_interface { + # port #{@port_web ||= 44444} + #} + } + + # + # All notifications are sent to files which we can open up and check during + # our tests. Network delivery is not tested in this script. + # + + notification_method("xmpp") { + deliver_to_queue AlertAndNotificationLogic::Notifications + deliver_to_file "#{dir}/xmpp.txt" + disable_normal_delivery! + + jid "mauveserv@chat.bytemark.co.uk" + password "foo" + } + + notification_method("email") { + deliver_to_queue AlertAndNotificationLogic::Notifications + deliver_to_file "#{dir}/email.txt" + disable_normal_delivery! + + # add in SMTP server, username, password etc. + # default to sending through localhost + from "matthew@bytemark.co.uk" + server "bytemail.bytemark.co.uk" + subject_prefix "[Bytemark alerts] " + + } + + notification_method("sms") { + provider "AQL" + deliver_to_queue AlertAndNotificationLogic::Notifications + deliver_to_file "#{dir}/sms.txt" + disable_normal_delivery! + + username "x" + password "x" + from "01904890890" + max_messages_per_alert 3 + } + + # a person common to all our tests + + person("joe_bloggs") { + urgent { sms("12345") } + normal { email("12345@joe_bloggs.email") } + low { xmpp("12345@joe_bloggs.xmpp") } + } + + person("jimmy_junior") { + urgent { sms("66666") } + normal { email("jimmy@junior.email") } + low { email("jimmy@junior.email") } + } + + alert_group { + includes { source == "rare-and-important" } + acknowledgement_time 60.minutes + level URGENT + + notify("joe_bloggs") { every 10.minutes } + } + + alert_group { + includes { source == "noisy-and-annoying" || alert_id == "whine" } + acknowledgement_time 24.hours + level LOW + + notify("jimmy_junior") { every 2.hours } + notify("joe_bloggs") { + every 30.minutes + during { + unacknowledged 6.hours + } + } + } + + alert_group { + includes { source == "can-wait-until-monday" } + level NORMAL + + notify("jimmy_junior") { + every 30.minutes + during { days_in_week(1..5) && hours_in_day(9..5) } + } + notify("joe_bloggs") { + every 2.hours + during { days_in_week(1..5) && hours_in_day(9..5) } + } + } + + # catch-all + alert_group { + acknowledgement_time 1.minute + level NORMAL + + notify("joe_bloggs") { every 1.hour } + } + TEMPLATE + end + + def setup + start_server(configuration_template) + end + + def teardown + stop_server + # no tests should leave notifications on the stack + assert_no_notification + end + + # Raise one alert, check representation in database, and that alert is + # received as expected. + # + def test_basic_fields_are_recognised + mauvesend("-o my_source -i alert1 -s \"alert1 summary\" -d \"alert1 detail\" -u \"alert1 subject\"") + + assert_not_nil(alert = Alert.first) + assert_equal("my_source", alert.source) + assert_equal("alert1", alert.alert_id) + assert_equal("alert1 summary", alert.summary) + assert_equal("alert1 detail", alert.detail) + assert_equal("alert1 subject", alert.subject) + assert(alert.raised?) + assert(!alert.cleared?) + assert(!alert.acknowledged?) + + with_next_notification do |destination, this_alert, other_alerts| + assert_equal("12345@joe_bloggs.email", destination) + assert_equal(Alert.first, this_alert) + assert_equal([Alert.first], other_alerts) + end + + end + + # Check that a simple automatic raise, acknowledge & auto-clear request + # work properly. + # + def test_auto_raise_and_clear + # Raise the alert, wait for it to be processed + mauvesend("-o my_source -i alert1 -s \"alert1 summary\" -d \"alert1 detail\" -u \"alert1 subject\" -r +5m -c +10m") + + # Check internal state + # + assert(!Alert.first.raised?, "Auto-raising alert raised early") + assert(!Alert.first.cleared?, "Auto-clearing alert cleared early") + assert(!Alert.first.acknowledged?, "Alert acknowledged when I didn't expect it") + + # We asked for it to be raised in 5 minutes, so no alert yet... + # + assert_no_notification + + # Push forward to when the alert should be raised, check it has been + # + Time.advance(5.minutes) + assert(Alert.first.raised?, "#{Alert.first.inspect} should be raised by now") + assert(!Alert.first.cleared?, "#{Alert.first.inspect} should not be cleared") + + # Check that we have a notification + # + with_next_notification do |destination, this_alert, other_alerts| + assert_equal("12345@joe_bloggs.email", destination) + assert_equal(Alert.first, this_alert) + assert_equal('raised', this_alert.update_type) + end + + # Simulate manual acknowledgement + # + Alert.first.acknowledge!(Configuration.current.people["joe_bloggs"]) + Timers.restart_and_then_wait_until_idle + assert(Alert.first.acknowledged?, "Acknowledgement didn't work") + + # Check that the acknowledgement has caused a notification + # + with_next_notification do |destination, this_alert, other_alerts| + assert_equal("12345@joe_bloggs.email", destination) + assert_equal(Alert.first, this_alert) + assert_equal('acknowledged', this_alert.update_type, this_alert.inspect) + end + assert(Alert.first.acknowledged?) + assert(Alert.first.raised?) + assert(!Alert.first.cleared?) + + # Now with the config set to un-acknowledge alerts after only 1 minute, + # try winding time on and check that this happens. + # + Time.advance(2.minutes) + with_next_notification do |destination, this_alert, other_alerts| + assert_equal("12345@joe_bloggs.email", destination) + assert_equal(Alert.first, this_alert) + assert_equal('raised', this_alert.update_type, this_alert.inspect) + end + + # Check that auto-clearing works four minutes later + # + Time.advance(5.minutes) + assert(Alert.first.cleared?) + assert(!Alert.first.raised?) + + # Finally check for a notification that auto-clearing has happened + # + with_next_notification do |destination, this_alert, other_alerts| + assert_equal("12345@joe_bloggs.email", destination) + assert_equal(Alert.first, this_alert) + assert_equal('cleared', this_alert.update_type, this_alert.inspect) + end + + # And see that no further reminders are sent a while later + Time.advance(1.day) + assert_no_notification + end + + def test_one_alert_changes_from_outside + # Raise our test alert, wait for it to be processed + mauvesend("-o my_source -i alert1 -s \"alert1 summary\" -d \"alert1 detail\" -u \"alert1 subject\"") + + # Check internal representation, external notification + # + assert(Alert.first.raised?) + assert(!Alert.first.cleared?) + with_next_notification do |destination, this_alert, other_alerts| + assert_equal('raised', this_alert.update_type, this_alert.inspect) + end + + # Check we get reminders every hour, and no more + # + 12.times do + Time.advance(1.hour) + with_next_notification do |destination, this_alert, other_alerts| + assert_equal('raised', this_alert.update_type, this_alert.inspect) + end + assert_no_notification + end + + # Clear the alert, wait for it to be processed + mauvesend("-o my_source -i alert1 -c now") + assert(!Alert.first.raised?) + assert(Alert.first.cleared?) + with_next_notification do |destination, this_alert, other_alerts| + assert_equal('cleared', this_alert.update_type, this_alert.inspect) + end + + # Check we can raise the same alert again + Time.advance(1.minute) + mauvesend("-o my_source -i alert1 -s \"alert1 summary\" -d \"alert1 detail\" -u \"alert1 subject\" -r now") + assert(Alert.first.raised?, Alert.first.inspect) + assert(!Alert.first.cleared?, Alert.first.inspect) + with_next_notification do |destination, this_alert, other_alerts| + assert_equal('raised', this_alert.update_type, this_alert.inspect) + end + end + + def test_alert_groups + # check that this alert is reminded more often than normal + mauvesend("-o rare-and-important -i alert1 -s \"rare and important alert\"") + assert(Alert.first.raised?) + assert(!Alert.first.cleared?) + + 10.times do + with_next_notification do |destination, this_alert, other_alerts| + assert_equal('raised', this_alert.update_type, this_alert.inspect) + assert_equal('12345', destination) + Time.advance(10.minutes) + end + end + discard_next_notification + end + + def test_future_raising + mauvesend("-i heartbeat -c now -r +10m -s \"raise in the future\"") + assert(!Alert.first.raised?) + assert(Alert.first.cleared?) + assert_no_notification + + # Check the future alert goes off + # + Time.advance(10.minutes) + assert(Alert.first.raised?) + assert(!Alert.first.cleared?) + with_next_notification do |destination, this_alert, other_alerts| + assert_equal('raised', this_alert.update_type, this_alert.inspect) + end + + # Check that a repeat of the "heartbeat" update clears it, and we get + # a notification. + # + mauvesend("-i heartbeat -c now -r +10m -s \"raise in the future\"") + assert(!Alert.first.raised?) + assert(Alert.first.cleared?) + with_next_notification do |destination, this_alert, other_alerts| + assert_equal('cleared', this_alert.update_type, this_alert.inspect) + end + + # Check that a re-send of the same clear alert doesn't send another + # notification + # + Time.advance(1.minute) + mauvesend("-i heartbeat -c now -r +10m -s \"raise in the future\"") + assert(!Alert.first.raised?) + assert(Alert.first.cleared?) + assert_no_notification + + # Check that a skewed resend doesn't confuse it + # + mauvesend("-i heartbeat -c +1m -r +11m -s \"raise in the future\"") + assert(!Alert.first.raised?) + assert(Alert.first.cleared?) + Time.advance(1.minute) + assert(!Alert.first.raised?) + assert(Alert.first.cleared?) + assert_no_notification + end + + # Make sure that using the "replace all flag" works as expected. + # + def test_replace_flag + mauvesend("-p") + #mauvesend("-p") + assert_no_notification + + mauvesend("-i test1 -s\"\test1\"") + assert(Alert.first.raised?) + with_next_notification do |destination, this_alert, other_alerts| + assert_equal('raised', this_alert.update_type, this_alert.inspect) + end + assert_no_notification + + mauvesend("-p") + #mauvesend("-p") + with_next_notification do |destination, this_alert, other_alerts| + assert_equal('cleared', this_alert.update_type, this_alert.inspect) + end + assert_no_notification + end + + def test_earliest_date + alert = Alert.create!( + :alert_id => "test_id", + :source => "test1", + :subject => "test subject", + :summary => "test summary", + :raised_at => nil, + :will_raise_at => Time.now + 60, + :will_clear_at => Time.now + 120, + :update_type => "cleared", + :updated_at => Time.now + ) + assert(alert) + + assert(AlertEarliestDate.first.alert == alert) + end + +end + + + + diff --git a/test/mauve_test_helper.rb b/test/mauve_test_helper.rb new file mode 100644 index 0000000..8653bd5 --- /dev/null +++ b/test/mauve_test_helper.rb @@ -0,0 +1,104 @@ +require 'tmpdir' +require 'thread' +require 'timeout' +require 'mauve/configuration' + +Thread.abort_on_exception = true + +module MauveTestHelper + include Mauve + Notifications = Queue.new + + # Returns the base directory for temporary files for this test instance + # + def dir + if !@test_dir + now = ::Time.now + base=Dir.tmpdir+"/mauve_test" + Dir.mkdir(base) unless File.directory?(base) + base=base+"/#{$$}" + Dir.mkdir(base) unless File.directory?(base) + Dir.mkdir(@test_dir="#{base}/#{name}") + end + @test_dir + end + + # Starts the Mauve server with a configuration supplied as a string. + # + def start_server(config) + @here = File.expand_path(__FILE__).split("/")[0..-2].join("/") + "/.." + File.open("#{dir}/config_file", "w") { |fh| fh.write(config) } + Notifications.clear + + Configuration.current = ConfigurationBuilder.load("#{dir}/config_file") + Time.reset_to_midnight + @thread = Thread.new do + begin + Configuration.current.server.run + rescue Interrupt + Configuration.current.close + end + end + # avoids races if we try to shut down too quickly + Configuration.current.server.sleep_until_ready + Log.info "TEST RUN STARTED: #{name}" + end + + # Stops the Mauve server, should reset it ready to start again within the + # same process. + # + def stop_server + @thread.raise(Interrupt.new) + @thread.join + end + + # Send an alert to the server, return when the server process has definitely + # processed it (or die after 2s). + # + def mauvesend(cmd) + Configuration.current.server.sleep_until_ready + output = `TEST_TIME=#{Time.now.to_i} #{@here}/mauve_starter.rb #{@here}/bin/mauvesend -v 127.0.0.1:44444 #{cmd} 2>&1` + status = $?.exitstatus + raise "Exit #{status} from command: '#{output}'" unless status == 0 + raise "mauvesend did not return an integer" unless output.to_i > 0 + begin + timeout(2) { Configuration.current.server.sleep_until_transmission_id_received(output.to_i) } + rescue Timeout::Error + flunk("Did not receive transmission id '#{output}'") + end + end + + # Assuming the test configuration contains a notification method with + # "deliver_to_queue TestClass::Notifications", this helper will return the next + # alert notification by that method as a triplet: + # + # [destination, alert, other_alerts] + # + # e.g. destination will be an email address, phone number or xmpp ID, just + # as in the configuration file. alert will be the subject of this + # alert, and other_alerts will be the other notifications that + # are relevant for this person at this time. + # + # The test will fail after 2s if no alert is received. + # + def with_next_notification + Timers.restart_and_then_wait_until_idle + flunk("Nothing on Notifications queue when I expected one") if Notifications.empty? + yield(*Notifications.pop) + end + + def discard_next_notification + with_next_notification { } + end + + # The reverse of next_alert, the test fails if an alert is received + # within 2s. + # + def assert_no_notification + Timers.restart_and_then_wait_until_idle + flunk("#{Notifications.pop.inspect} on Notifications queue when I expected nothing") unless + Notifications.empty? + true + end +end + diff --git a/test/mauve_time.rb b/test/mauve_time.rb new file mode 100644 index 0000000..d8c57a7 --- /dev/null +++ b/test/mauve_time.rb @@ -0,0 +1,38 @@ +require 'logger' +require 'time' + +module Mauve + # A fake Time, which we use in testing. Time#now returns the same value every + # time, unless we call Time#advance which alters the value of 'now' by a + # given number of seconds. There is a simple pass-through for other methods. + # + class Time + class << self + def reset_to_midnight + @now = Time.parse("00:00") + Log.debug "Test time reset to #{@now}" + end + + def now + reset_to_midnight unless @now + @now + end + + def advance(seconds) + @now += seconds + Log.debug "Test time advanced by #{seconds} to #{@now}, kicking Mauve::Timers" + Timers.restart_and_then_wait_until_idle + @now + end + + def at(*a) + ::Time.at(*a) + end + + def parse(*a) + ::Time.parse(*a) + end + end + end +end + diff --git a/test/notification.rb b/test/notification.rb new file mode 100644 index 0000000..9cc3306 --- /dev/null +++ b/test/notification.rb @@ -0,0 +1,57 @@ +$: << "../lib/" + +require 'test/unit' +require 'mauve/notification' + +# Test changes to notification things. +class MauveNotificationTest < Test::Unit::TestCase + + def test_x_in_list_of_y + mdr = Mauve::DuringRunner.new(Time.now) + [ + [[0,1,3,4], 2, false], + [[0,2,4,6], 2, true], + [[0..1,3..6],2, false], + [[0..2, 4,5],2, true], + [[0,1..3], 2, true], + ].each do |y,x,result| + assert_equal(result, mdr.send(:x_in_list_of_y, x,y)) + end + end + + def test_hours_in_day + t = Time.gm(2010,1,2,3,4,5) + # => Sat Jan 02 03:04:05 UTC 2010 + mdr = Mauve::DuringRunner.new(t) + [ + [[0,1,3,4], true], + [[0,2,4,6], false], + [[[0,1,3],4], true], + [[[0,2,4],6], false], + [[0..1,3..6], true], + [[0..2, 4,5], false], + [[0,1..3], true], + [[4..12], false] + ].each do |hours, result| + assert_equal(result, mdr.send(:hours_in_day, hours)) + end + end + + def test_days_in_week + t = Time.gm(2010,1,2,3,4,5) + # => Sat Jan 02 03:04:05 UTC 2010 + mdr = Mauve::DuringRunner.new(t) + [ + [[0,1,3,4], false], + [[0,2,4,6], true], + [[[0,1,3],4], false], + [[[0,2,4],6], true], + [[0..1,3..6], true], + [[0..2, 4,5], false], + [[0,1..3], false], + [[4..6], true] + ].each do |days, result| + assert_equal(result, mdr.send(:days_in_week, days), "#{t.wday} in #{days.join(", ")}") + end + end +end diff --git a/utils/buffer_size.sh b/utils/buffer_size.sh new file mode 100755 index 0000000..02984b2 --- /dev/null +++ b/utils/buffer_size.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +NO_ARGS=0 +OPTERROR=65 +if [ $# -eq "$NO_ARGS" ] # Script invoked with no command-line args? +then + echo "Usage: `basename $0` File.log" + exit $OPTERROR # Exit and explain usage, if no argument(s) given. +fi +logFile=$1 + +egrep 'Buffer has [0-9]* packets left' $logFile |\ + awk 'BEGIN {print " date sz"} {print s++ " " $1 "::" $2 " " $10}' > data + +R --vanilla --no-save --slave <<RSCRIPT +lst <- read.table("data") +attach(lst) +summary(sz) +png(filename="buffer.png", width=1024) +dates <- strptime(as.character(date), "%Y-%m-%d::%H:%M:%S") +plot(dates, sz, type='l', + main="Mauve server: Maximum buffer size per second", + xlab="Time", + ylab="Maximum number of packets waiting to be processed") +abline(h=1.05, col="red") +abline(h=mean(sz), col="blue") +RSCRIPT +img=`which qiv` +if [ $? != 0 ] +then echo "Cannot display image here" +else $img buffer.png +fi diff --git a/utils/clear_wrong_reminder.sql b/utils/clear_wrong_reminder.sql new file mode 100644 index 0000000..fde0504 --- /dev/null +++ b/utils/clear_wrong_reminder.sql @@ -0,0 +1,4 @@ +select * from 'mauve_alerts' where update_type='cleared' and will_unacknowledge_at != 0; + +update 'mauve_alerts' set will_unacknowledge_at=NULL where update_type='cleared' and will_unacknowledge_at != 0; + diff --git a/utils/get_last_test_db.sh b/utils/get_last_test_db.sh new file mode 100755 index 0000000..71ce4f1 --- /dev/null +++ b/utils/get_last_test_db.sh @@ -0,0 +1,3 @@ +#!/bin/sh +test=`echo $1 | sed s/://` +sqlitebrowser `/bin/ls -dtr /tmp/mauve_test/* | tail -1`/$test\(ZTestCases\)/mauve_test.db diff --git a/utils/get_last_test_log.sh b/utils/get_last_test_log.sh new file mode 100755 index 0000000..26597d0 --- /dev/null +++ b/utils/get_last_test_log.sh @@ -0,0 +1,3 @@ +#!/bin/sh +test=`echo $1 | sed s/://` +less `/bin/ls -dtr /tmp/mauve_test/* | tail -1`/$test\(ZTestCases\)/log000001 diff --git a/utils/get_live_db.sh b/utils/get_live_db.sh new file mode 100755 index 0000000..35c5aea --- /dev/null +++ b/utils/get_live_db.sh @@ -0,0 +1,3 @@ +#!/bin/sh +scp one-eyed-jack:/var/lib/mauveserver/alerts.db . && \ + sqlitebrowser alerts.db diff --git a/utils/init_jmauve.sh b/utils/init_jmauve.sh new file mode 100755 index 0000000..3011d62 --- /dev/null +++ b/utils/init_jmauve.sh @@ -0,0 +1,80 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: mauveserver +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start the mauve alerting daemon at boot time +# Description: Start the mauve alerting daemon at boot time +### END INIT INFO + +PATH=/bin:/sbin:/usr/bin:/usr/local/bin +DAEMON=/usr/bin/jmauveserver +DAEMON_OPTS=/etc/mauvealert/mauveserver.conf +DESC="mauvealert server" +PIDFILE=/var/run/jmauveserver.pid +LOG=/var/log/mauve + +test -x $DAEMON || exit 0 + +. /lib/lsb/init-functions + +case "$1" in + start) + log_begin_msg "Starting $DESC:" "$NAME" + if [ ! -d $LOG ]; then mkdir $LOG; fi + chown mauveserver $LOG /var/lib/mauveserver + $DAEMON $DAEMON_OPTS & + echo $! > $PIDFILE + for i in `seq 0 1 11`;do sleep 1; echo -n '.'; done + kill -0 $(cat $PIDFILE) + if [ $? != 0 ] ; then echo -n "failed"; else echo -n "success"; fi + + # Email on start. + address="yann@bytemark.co.uk" + lastLog=`/bin/ls -tr $LOG/*.log | tail -1` + logLastLines=`tail -101 $lastLog` + echo $logLastLines | mail -s "Mauve was started at `date`" $address + + log_end_msg $? + ;; + stop) + log_begin_msg "Stopping $DESC:" "$NAME" + if [ -f $PIDFILE ] ; then + kill `cat $PIDFILE` + rm $PIDFILE + + # Email on stop. + address="yann@bytemark.co.uk" + lastLog=`/bin/ls -tr $LOG/*.log | tail -1` + logLastLines=`tail -101 $lastLog` + echo $logLastLines | mail -s "Mauve was stopped at `date`" $address + + else + echo Not running to stop + exit 1 + fi + log_end_msg $? + ;; + reload) + if [ -f $PIDFILE ] ; then + echo Sending restart signal to mauveserver + kill -HUP `cat $PIDFILE` + else + echo Not running to reload + fi + ;; + restart|reload|force-reload) + $0 stop + sleep 1 + $0 start + ;; + *) + N=/etc/init.d/$NAME + echo "Usage: $N {start|stop|restart}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/utils/init_mauve.sh b/utils/init_mauve.sh new file mode 100755 index 0000000..309562c --- /dev/null +++ b/utils/init_mauve.sh @@ -0,0 +1,83 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: mauveserver +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start the mauve alerting daemon at boot time +# Description: Start the mauve alerting daemon at boot time +### END INIT INFO + +PATH=/bin:/sbin:/usr/bin:/usr/local/bin +DAEMON=/usr/bin/mauveserver +DAEMON_OPTS=/etc/mauvealert/mauveserver.conf +DESC="mauvealert server" +PIDFILE=/var/run/mauveserver.pid +LOG=/var/log/mauve + +test -x $DAEMON || exit 0 + +. /lib/lsb/init-functions + +case "$1" in + start) + log_begin_msg "Starting $DESC:" "$NAME" + if [ ! -d $LOG ]; then mkdir $LOG; fi + chown mauveserver $LOG /var/lib/mauveserver + $DAEMON $DAEMON_OPTS & + echo $! > $PIDFILE + #start-stop-daemon --background --make-pidfile --pidfile $PIDFILE \ + # --start --quiet --chuid mauveserver \ + # --exec "$DAEMON" --oknodo -- $DAEMON_OPTS + sleep 3 + #kill -0 $(cat $PIDFILE) + #if [ $? != 0 ] ; then echo -n "failed"; else echo -n "success"; fi + + # Email on start. + address="yann@bytemark.co.uk" + lastLog=`/bin/ls -tr $LOG/*.log | tail -1` + logLastLines=`tail -101 $lastLog` + echo $logLastLines | mail -s "Mauve was started at `date`" $address + + log_end_msg $? + ;; + stop) + log_begin_msg "Stopping $DESC:" "$NAME" + if [ -f $PIDFILE ] ; then + kill `cat $PIDFILE` + rm $PIDFILE + + # Email on stop. + address="yann@bytemark.co.uk" + lastLog=`/bin/ls -tr $LOG/*.log | tail -1` + logLastLines=`tail -101 $lastLog` + echo $logLastLines | mail -s "Mauve was stopped at `date`" $address + + else + echo Not running to stop + exit 1 + fi + log_end_msg $? + ;; + reload) + if [ -f $PIDFILE ] ; then + echo Sending restart signal to mauveserver + kill -HUP `cat $PIDFILE` + else + echo Not running to reload + fi + ;; + restart|reload|force-reload) + $0 stop + sleep 1 + $0 start + ;; + *) + N=/etc/init.d/$NAME + echo "Usage: $N {start|stop|restart}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/utils/packet_processing.sh b/utils/packet_processing.sh new file mode 100755 index 0000000..d78a916 --- /dev/null +++ b/utils/packet_processing.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +NO_ARGS=0 +OPTERROR=65 +if [ $# -eq "$NO_ARGS" ] # Script invoked with no command-line args? +then + echo "Usage: `basename $0` File.log" + exit $OPTERROR # Exit and explain usage, if no argument(s) given. +fi +logFile=$1 + +egrep 'Packet processed in [\.0-9]* seconds' $logFile |\ + awk 'BEGIN {print " date sz"} {print s++ " " $1 "::" $2 " " $11}' > data + +R --vanilla --no-save --slave <<RSCRIPT +lst <- read.table("data") +attach(lst) +summary(sz) +png(filename="packets.png", width=1024) +dates <- strptime(as.character(date), "%Y-%m-%d::%H:%M:%S") +plot(dates, sz, type='l', + main="Mauve server: maximum processing time of a packet per second.", + xlab="Time", + ylab="Maximum processing time of one packet") +abline(h=1.05, col="red") +abline(h=mean(sz), col="blue") +RSCRIPT +img=`which qiv` +if [ $? != 0 ] +then echo "Cannot display image here" +else $img packets.png +fi diff --git a/utils/run_android.sh b/utils/run_android.sh new file mode 100755 index 0000000..e932257 --- /dev/null +++ b/utils/run_android.sh @@ -0,0 +1,2 @@ +#!/bin/sh +/usr/local/android-sdk-linux_x86/tools/android& diff --git a/utils/supportbot-mauve b/utils/supportbot-mauve new file mode 100755 index 0000000..614c5f5 --- /dev/null +++ b/utils/supportbot-mauve @@ -0,0 +1,73 @@ +#!/usr/bin/ruby +require 'fileutils' +require 'yaml' +require 'timeout' +require 'rubygems' +require 'cgi' + +class RT + class Ticket < Struct.new(:id, :subject, :content) + end + + def initialize(cli, ticket_query) + @cli = cli + @ticket_query = ticket_query + end + + def query(args) + IO.popen("#{@cli} #{args}", "r") do |io| + io.read + end + end + + def tickets + query(@ticket_query).split("\n").map do |line| + if /^(\d+): (.*)$/.match(line) + t = Ticket.new($1.to_i, $2, "") + query("show ticket/#{t.id}/attachments").split("\n")[-5..-1].each do |line2| + next unless /\s(\d+):\s+\(text\/(plain|html)/.match(line2) + is_html = $2 == "html" + + attachment = query("show -f Content ticket/#{t.id}/attachments/#{$1}"). + split("Content: ")[1]. + split("\n"). + map { |line3| line3.gsub(/^\s+/,"") }. + join("\n") + next unless attachment.match(/Action=Take/) + if is_html + t.content += attachment + "<hr/>" + else + t.content += "<pre>"+attachment+"</pre><hr/>" + end + end + t + else + nil + end + end.compact + end +end + +class String; def escape; gsub(/[^A-Za-z0-9]/) { |x| "\\#{x}" }; end end + +config = File.open("supportbot.config") { |fh| + YAML::parse(fh.read).transform } + +system("mauvesend -o supportbot -i heartbeat -r +10m -c now -s \"heartbeat not received\" -d \"Headtbeat send from supportbot-mauve code. It maybe down.\"") + +rt = RT.new(config['rt']['bin'], config['rt']['ticket_query2']) + +if 0 < rt.tickets.size + args = %w( mauvesend alert.bytemark.co.uk -o supportbot ) + args += rt.tickets.map do |ticket| + ["-i", ticket.id.to_s, + "-u", ticket.id.to_s, + "-s", ticket.subject, + "-d", ticket.content + ] + end.flatten + exec(*args) +else + args = %w( mauvesend alert.bytemark.co.uk --id supportbot -o supportbot -p -c now ) + exec(*args) +end diff --git a/utils/test-smack.rb b/utils/test-smack.rb new file mode 100755 index 0000000..022f9d0 --- /dev/null +++ b/utils/test-smack.rb @@ -0,0 +1,101 @@ +#!/usr/bin/jruby +# CLASSPATH="$CLASSPATH:/home/yann/projects/mauvealert/jars/smack.jar:/home/yann/projects/mauvealert/jars/smackx.jar +# ./test-smack + +require 'java' +require '../jars/smack.jar' +require '../jars/smackx.jar' +require 'rubygems' +require 'rainbow' +require 'pp' + +include_class "org.jivesoftware.smack.XMPPConnection" +include_class "org.jivesoftware.smackx.muc.MultiUserChat" + +user_jid='mauvealert' +password='WojIsEv8ScaufOm1' +msg = "What fresh hell is this? -- Dorothy Parker." +begin + + print "XMPP object instanciated.\n".color(:green) + xmpp = XMPPConnection.new("chat.bytemark.co.uk") + + + print "Connection done.\n".color(:green) + xmpp.connect + if true != xmpp.isConnected + print "Failed to connect".color(:red) + return -1 + end + + + print "Login.\n".color(:green) + xmpp.login(user_jid, password, "Testing_smack") + if true != xmpp.isAuthenticated() + print "Failed to authenticate\n".color(:red) + return -1 + end + if true == xmpp.isSecureConnection() + print "Connection is secure\n".color(:green) + else + print "Connection is NOT secure.\n".color(:yellow) + end + + + print "Get chat manager.\n".color(:green) + chat = xmpp.getChatManager.createChat( + "yann@chat.bytemark.co.uk", nil) + + print "Sending message to #{chat.getParticipant}.\n".color(:green) + chat.sendMessage(msg) + + + print "Joining, sending a message and leaving a room.\n".color(:green) + #muc = MultiUserChat.new(xmpp, "office@conference.chat.bytemark.co.uk") + muc = MultiUserChat.new(xmpp, "test@conference.chat.bytemark.co.uk") + muc.join("Mauve alert bot") + muc.sendMessage(msg) + sleep 1 + #muc.leave() + + + print "Adieu monde cruel!\n".color(:green) + xmpp.disconnect + + + print "all done.\n".color(:green) +rescue => ex + print "EPIC FAIL: Raised #{ex.class} because #{ex.message}\n\n".color(:red) + raise ex +end + +=begin +require 'java' +require './jars/smack.jar' +require './jars/smackx.jar' +include_class "org.jivesoftware.smack.XMPPConnection" +include_class "org.jivesoftware.smackx.muc.MultiUserChat" +user_jid='mauvealert' +password='WojIsEv8ScaufOm1' +msg = "What fresh hell is this? -- Dorothy Parker." +xmpp = XMPPConnection.new("chat.bytemark.co.uk") +xmpp.connect +xmpp.login(user_jid, password, "mauve_test") + +jid="yann@chat.bytemark.co.uk" +chat = xmpp.getChatManager.createChat(jid, nil) +chat.sendMessage(msg) + +xmpp.getRoster().reload() +xmpp.getRoster().getPresence(jid).isAvailable() +xmpp.getRoster().getPresence(jid).getStatus() + +muc = MultiUserChat.new(xmpp, 'office@conference.chat.bytemark.co.uk/mauvealert') +muc.join("Mauve alert bot") +muc.sendMessage(msg) + +muc2 = MultiUserChat.new(xmpp, "test@conference.chat.bytemark.co.uk") +muc2.join("Mauve alert bot") +muc2.sendMessage(msg) + +=end diff --git a/views/_acknowledge.haml b/views/_acknowledge.haml new file mode 100644 index 0000000..cc0ea71 --- /dev/null +++ b/views/_acknowledge.haml @@ -0,0 +1,6 @@ +%h4 Acknowledged alerts +#treecontrolAck= partial("treecontrol") +%ul{:id => "blackAck", :class => "treeview-black-ack"} + = partial("get_list", :locals => {:level => "urgent", :alertsList => @grouped_ack_urgent}) + = partial("get_list", :locals => {:level => "normal", :alertsList => @grouped_ack_normal}) + = partial("get_list", :locals => {:level => "low", :alertsList => @grouped_ack_low}) diff --git a/views/_alert_counts.haml b/views/_alert_counts.haml new file mode 100644 index 0000000..6dcecc0 --- /dev/null +++ b/views/_alert_counts.haml @@ -0,0 +1,16 @@ +.colmask.threecol + .colmid + .colleft + .urgent.col1 + Urgent + .unacknowledged{:title=>"Urgent alert unacknowledged"} #{@urgent.length - @count_ack[:urgent]} + .acknowledged{:title=>"Urgent alert acknowledged"} #{@count_ack[:urgent]} + .normal.col2 + Normal + .unacknowledged{:title=>"Normal alert unacknowledged"} #{@normal.length - @count_ack[:normal]} + .acknowledged{:title=>"Normal alert acknowledged"} #{@count_ack[:normal]} + .low.col3 + Low + .unacknowledged{:title=>"Low alert unacknowledged"} #{@low.length - @count_ack[:low]} + .acknowledged{:title=>"Low alert acknowledged"} #{@count_ack[:low]} + diff --git a/views/_alert_strips_group.haml b/views/_alert_strips_group.haml new file mode 100644 index 0000000..0a6f3bf --- /dev/null +++ b/views/_alert_strips_group.haml @@ -0,0 +1,37 @@ +//- alerts.each_pair do |subject, alerts| +// .grouped_folder{:id=>subject} +// - lst = Array.new() +// - count = 0 +// - alerts.sort.each do |alert| +// - if count > 0 +// - displayFolding = (false == session[:display_folding][subject])? 'display:none' : '' +// - else +// - count += 1 +// - lst.push("whole_alert_#{alert.id}") +// .alert_whole{:id=>"whole_alert_#{alert.id}", :style => "#{displayFolding}"} +// .alert_strip{:id => "alert_#{alert.id}"} +// %a.acknowledge{:href => "javascript:toggleAcknowledge(#{alert.id});", :onClick => "toggleDetailView(#{alert.id});"} +// - if alert.acknowledged? +// //%img.acknowledged.auto_hover{:src => "/images/acknowledge_acknowledged.png" :title=>"Unacknowledge alert"} +// %img.acknowledged{:src => "/images/acknowledge_acknowledged.png" :title=>"Unacknowledge alert"} +// - else +// //%img.unacknowledged.auto_hover{:src => "/images/acknowledge_unacknowledged.png" :title=>"Acknowledge alert"} +// %img.unacknowledged{:src => "/images/acknowledge_unacknowledged.png" :title=>"Acknowledge alert"} +// - if alert.source != alert.subject +// .source= alert.source +// .subject= alert.subject + ": " +// .summary= alert.summary +// //- if alert.detail +// %a.expand{:href => "javascript:", :onClick => "toggleDetailView(#{alert.id}); $('alert_#{alert.id}').next().toggle();"} +// %img.zoom{:src => "/images/zoom.png" :title => "Show details"} +// %a.full{:href => "/alert/#{alert.id}"} +// %img.hourglass{:src => "/images/hourglass.png" :title => "Show history"} +// - displayDetails = (false == session[:display_alerts][alert.id])? 'display:none' : '' +// .detail_strip{:id => "detail_#{alert.id}", :style => "#{displayDetails}"} +// = alert.detail +// - lst.shift() +// - displaySummary = (1 > lst.size())? 'display:none' : '' +// .control{:id=>subject, :style=>"#{displaySummary}"} +// There are #{lst.size() + 1} alarms in this block... +// %a.expand{:href => "javascript:", :onClick => "toggleFoldingView('#{subject}'); [$('#{lst.join("'), $('")}')].invoke('toggle');"} +// %img.toggle{:src => "/images/toggle_alerts.png" :title => "toggle display of alerts"} diff --git a/views/_alert_summary.haml b/views/_alert_summary.haml new file mode 100644 index 0000000..1798ec1 --- /dev/null +++ b/views/_alert_summary.haml @@ -0,0 +1,7 @@ +// See feature #1051: UI sorting order. +.urgent= partial("alert_strips_group", :locals => {:alerts => @grouped_new_urgent}) +.normal= partial("alert_strips_group", :locals => {:alerts => @grouped_new_normal}) +.low= partial("alert_strips_group", :locals => {:alerts => @grouped_new_low}) +.urgent= partial("alert_strips_group", :locals => {:alerts => @grouped_ack_urgent}) +.normal= partial("alert_strips_group", :locals => {:alerts => @grouped_ack_normal}) +.low= partial("alert_strips_group", :locals => {:alerts => @grouped_ack_low}) diff --git a/views/_event_list.haml b/views/_event_list.haml new file mode 100644 index 0000000..73b9193 --- /dev/null +++ b/views/_event_list.haml @@ -0,0 +1,22 @@ +.alert_full_list + .row + %h2 + %h2 Subject + %h2 Summary + %h2 Raised + %h2 Acknowledged + %h2 Cleared + - alerts.each do |alert| + .row{:id => alert.id, :class => cycle('alt','')} + .more + %a{:href => "/alert/#{alert.id}"} [details] + .subject= alert.subject + .summary= alert.summary + .raised{:class => "up"+alert.update_type}= alert.raised_at ? alert.time_relative(Time.now - alert.raised_at.to_time) : "" + .acknowledged{:class => "up"+alert.update_type} + - if alert.acknowledged_at + = alert.time_relative(Time.now - alert.acknowledged_at.to_time) + by + = alert.acknowledged_by + .cleared{:class => "up"+alert.update_type}= alert.cleared? ? alert.time_relative(Time.now - alert.cleared_at.to_time) : "" + diff --git a/views/_get_alerts.haml b/views/_get_alerts.haml new file mode 100644 index 0000000..8abff2e --- /dev/null +++ b/views/_get_alerts.haml @@ -0,0 +1,43 @@ +- alerts.each do |alert| + %li{:id=>"alert#{alert.id}"} + .alert + - if "urgent" == level + .levelUrgent + %a{:href=>"javascript:", :onClick=>"showAcknowledgeStatus(event, #{alert.id}, #{alert.get_default_acknowledge_time()});"} + Urgent + - elsif "normal" == level + .levelNormal + %a{:href=>"javascript:", :onClick=>"showAcknowledgeStatus(event, #{alert.id}, #{alert.get_default_acknowledge_time()});"} + Normal + -else + .levelLow + %a{:href=>"javascript:", :onClick=>"showAcknowledgeStatus(event, #{alert.id}, #{alert.get_default_acknowledge_time()});"} + Low + .subject= alert.get_safe_html_subject + .summary= alert.get_safe_html_summary + %a{:href=>"/alert/#{alert.id}"} + History + //%a{:href=>"javascript:", :onClick=>"$(furtherInformation#{alert.id}).toggle();"} + %a{:href=>"javascript:", :onClick=>"$(\"#furtherInformation#{alert.id}.furtherInformation\").toggle();"} + Extra + //%a{:href=>"javascript:", :onClick=>"$(inlineDetails#{alert.id}).toggle();"} + %a{:href=>"javascript:", :onClick=>"$(\"#inlineDetails#{alert.id}.inlineDetails\").toggle();"} + Details + .inlineDetails{:id=>"inlineDetails#{alert.id}", :style=>"display: none"} + %h5 Alert details + #{alert.get_details} + //.furtherInformation{:id=>"furtherInformation#{alert.id}"} + .furtherInformation{:id=>"furtherInformation#{alert.id}", :style=>"display: none"} + Alert group is '#{AlertGroup.matches(alert)[0].name}'. + - if alert.source != alert.subject + .source= "Source is '#{alert.get_safe_html_source}'." + - if alert.acknowledged? + .ackDetails + Acknowledged by user + %strong= "#{alert.get_acknowledged_by()} " + and will #{alert.get_unacknowledge_at_string()}. + - else + .time + Last updated on + %em= "#{alert.get_updated_at()}." + diff --git a/views/_get_list.haml b/views/_get_list.haml new file mode 100644 index 0000000..cc8fb9b --- /dev/null +++ b/views/_get_list.haml @@ -0,0 +1,40 @@ +//- alerts.each_pair do |subject, alerts| +- alertsList.keys.sort.each do |subject| + - alerts = alertsList[subject] + - if 1 < alerts.length() + %li{:id=>"firstAlert#{alerts[0].id}"} + - if "urgent" == level + %span.levelUrgent Urgent + - elsif "normal" == level + %span.levelNormal Normal + -else + %span.levelLow Low + .conglomerate + %span.subject= alerts[0].subject + - if alerts[0].acknowledged? + There are <strong>#{alerts.length}</strong> alerts in this conglomerate. + - else + - shortestAckTime = 9223372036854775807 + - lst = Array.new + - alerts.each do |alert| + - shortestAckTime = alert.get_default_acknowledge_time() if alert.get_default_acknowledge_time() < shortestAckTime + - lst << alert.id + %a{:href=>"javascript:", :onClick=>"showBulkAcknowledgeStatus(event, #{"['#{lst.join('\',\'')}']"}, #{shortestAckTime});"} + [Bulk acknowledge <strong>#{alerts.length}</strong> alerts for + - hrs = shortestAckTime / 3600 + - if 1 == hrs + one hour. + - elsif 24 > hrs and 1 > hrs + #{hrs} hours + - elsif 24 == hrs + one day. + - elsif 24 < hrs and 168 > hrs + #{hrs/24} days. + - elsif 168 == hrs + one week + - else + #{hrs/168} weeks + ] + %ul= partial("get_alerts", :locals => {:level => level, :alerts => alerts}) + - else + = partial("get_alerts", :locals => {:level => level, :alerts => alerts}) diff --git a/views/_head.haml b/views/_head.haml new file mode 100644 index 0000000..db2125e --- /dev/null +++ b/views/_head.haml @@ -0,0 +1,22 @@ +.colmask.threecol + .colmid + .colleft + .col1 + .countdown{:id=>"reloadPage"} + %span.count + %span.urgent + Urgent + %span.nbrUnacknowledged{:title=>"Urgent alert unacknowledged"} #{@urgent.length - @count_ack[:urgent]} + %span.nbrAcknowledged{:title=>"Urgent alert acknowledged"} (Seen #{@count_ack[:urgent]}) + %span.normal + Normal + %span.nbrUnacknowledged{:title=>"Normal alert unacknowledged"} #{@normal.length - @count_ack[:normal]} + %span.nbrAcknowledged{:title=>"Normal alert acknowledged"} (Seen #{@count_ack[:normal]}) + %span.low + Low + %span.nbrUnacknowledged{:title=>"Low alert unacknowledged"} #{@low.length - @count_ack[:low]} + %span.nbrAcknowledged{:title=>"Low alert acknowledged"} (Seen #{@count_ack[:low]}) + .col2 + .title #{@title} + .col3 + %img{:src => "/images/logo.png"} diff --git a/views/_head2.haml b/views/_head2.haml new file mode 100644 index 0000000..a37d6c4 --- /dev/null +++ b/views/_head2.haml @@ -0,0 +1,20 @@ +#main + #mainleft + .title #{@title} + #maincenter + .countdown{:id=>"reloadPage"} + %span.count + %span.urgent + Urgent + %span.nbrUnacknowledged{:title=>"Urgent alert unacknowledged"} #{@urgent.length - @count_ack[:urgent]} + %span.nbrAcknowledged{:title=>"Urgent alert acknowledged"} (Seen #{@count_ack[:urgent]}) + %span.normal + Normal + %span.nbrUnacknowledged{:title=>"Normal alert unacknowledged"} #{@normal.length - @count_ack[:normal]} + %span.nbrAcknowledged{:title=>"Normal alert acknowledged"} (Seen #{@count_ack[:normal]}) + %span.low + Low + %span.nbrUnacknowledged{:title=>"Low alert unacknowledged"} #{@low.length - @count_ack[:low]} + %span.nbrAcknowledged{:title=>"Low alert acknowledged"} (Seen #{@count_ack[:low]}) + #mainright + %img{:src => "/images/logo.png"} diff --git a/views/_head3.haml b/views/_head3.haml new file mode 100644 index 0000000..1a2b00f --- /dev/null +++ b/views/_head3.haml @@ -0,0 +1,17 @@ +%h1 + Mauve Alert Panel +.alertsSummary + %span.urgent + Urgent + %span.nbrUnacknowledged{:title=>"Urgent alert unacknowledged"} #{@urgent.length - @count_ack[:urgent]} + %span.nbrAcknowledged{:title=>"Urgent alert acknowledged"} (Seen #{@count_ack[:urgent]}) + %span.normal + Normal + %span.nbrUnacknowledged{:title=>"Normal alert unacknowledged"} #{@normal.length - @count_ack[:normal]} + %span.nbrAcknowledged{:title=>"Normal alert acknowledged"} (Seen #{@count_ack[:normal]}) + %span.low + Low + %span.nbrUnacknowledged{:title=>"Low alert unacknowledged"} #{@low.length - @count_ack[:low]} + %span.nbrAcknowledged{:title=>"Low alert acknowledged"} (Seen #{@count_ack[:low]}) + %a{:href=>"/logout"} + [Logout] diff --git a/views/_jqChangeStatus.haml b/views/_jqChangeStatus.haml new file mode 100644 index 0000000..1b00c17 --- /dev/null +++ b/views/_jqChangeStatus.haml @@ -0,0 +1,21 @@ +%form{:name=>"changeAlertStatusForm", :action=>"html_form_action", :method=>"post"} + %input{:type=>"hidden", :name=>"AlertID", :value=>"Null"} + %input{:type=>"hidden", :name=>"AlertDefaultAcknowledgeTime", :value=>"Null"} + %input{:type=>"button", :name=>"statusAcknowledgedFor", :value=>"Acknowledge this alert for", + :onclick=>"changeAcknowledgeStatus(this.form.AlertID.value, this.form.timeStamp.value)", + :class=>"buttonAcknowledgeAlert"} + %select{:name=>"timeStamp", :id=>"sample", :class=>"timeList"} + %option{:value=>"0"}default... + %option{:value=>"3600"}1 hour + %option{:value=>"7200"}2 hours + %option{:value=>"7560"}3 hours + %option{:value=>"18000"}5 hours + %option{:value=>"25200"}7 hours + %option{:value=>"86400"}1 day + %option{:value=>"172800"}2 days + %option{:value=>"604800"}1 week + %option{:value=>"0"}Forever + %input{:type=>"button", :name=>"raise", :value=>"Unacknowledged", :onClick=>"raiseAlert(this.form.AlertID.value);", :class=>"raiseAlert"} + %input{:type=>"button", :name=>"clear", :value=>"Trash alert", :onClick=>"clearAlert(this.form.AlertID.value);", :class=>"trashAlert"} +%em + Please be careful what you chose and err on the side of shorter times scales. diff --git a/views/_jqModalScript.haml b/views/_jqModalScript.haml new file mode 100644 index 0000000..00cc298 --- /dev/null +++ b/views/_jqModalScript.haml @@ -0,0 +1,22 @@ +.jdHandle.jqDrag +%strong + Acknowledge alert! +%em + Please be careful what you chose and err on the side of shorter times scales. +%form{:name=>"input", :action=>"html_form_action", :method=>"post"} + //%a{:href=>"javascript:toogleAcknowledge(#{alert.id})"} + %a{:href=>"javascript:toogleAcknowledge"} + Acknowledge this alert for + %select + %option{:value=>"1"}1 hour + %option{:value=>"2"}2 hour + %option{:value=>"3"}3 hours + %option{:value=>"5"}5 hours + %option{:value=>"7"}7 hours + %option{:value=>"24"}1 day + %option{:value=>"48"}2 days + %option{:value=>"168"}1 week + %option{:value=>"0"}Forever +%a{:href=>"#", :class=>"jqmClose"} + Close +.jqHandle.jqResize diff --git a/views/_navigation.haml b/views/_navigation.haml new file mode 100644 index 0000000..6c47b28 --- /dev/null +++ b/views/_navigation.haml @@ -0,0 +1,14 @@ +- if @person + %a{:href => "/alerts"} Current alerts + %a{:href => "/events"} Last day + %a{:href => "/preferences"} My preferences + %a{:href => "/logout"} Logout <strong>(#{@person.username})</strong> +- else + %form{:action => '/login', :method => :POST} + Username + %input{:name => 'username', :type => 'text', :size => 10} + Password + %input{:name => 'password', :type => 'password', :size => 10} + %input{:type => 'submit', :value => 'Log in'} +- if flash['error'] + .error= flash['error'] diff --git a/views/_treecontrol.haml b/views/_treecontrol.haml new file mode 100644 index 0000000..caf3a89 --- /dev/null +++ b/views/_treecontrol.haml @@ -0,0 +1,9 @@ +%a{:title=>"Collapse the entire tree below", :href=>"#"} + %img{:src=>"/images/minus.png"} + Collapse All +%a{:title=>"Expand the entire tree below", :href=>"#"} + %img{:src=>"/images/plus.png"} + Expand All +%a{:title=>"Toggle collapse/expand of tree below", :href=>"#"} + %img{:src=>"/images/toggle.png"} + Toggle All diff --git a/views/_unacknowledged.haml b/views/_unacknowledged.haml new file mode 100644 index 0000000..77ed657 --- /dev/null +++ b/views/_unacknowledged.haml @@ -0,0 +1,6 @@ +%h4 Unacknowledged alerts +#treecontrolNew= partial("treecontrol") +%ul{:id => "blackNew", :class => "treeview-black"} + = partial("get_list", :locals => {:level => "urgent", :alertsList => @grouped_new_urgent}) + = partial("get_list", :locals => {:level => "normal", :alertsList => @grouped_new_normal}) + = partial("get_list", :locals => {:level => "low", :alertsList => @grouped_new_low}) diff --git a/views/alert.haml b/views/alert.haml new file mode 100644 index 0000000..4310ddc --- /dev/null +++ b/views/alert.haml @@ -0,0 +1,56 @@ +!!! +%html + %head + %title #{@title}: Alert #{@alert.id}: #{@alert.get_safe_html_summary} + %link{:rel => "stylesheet", :href => "/alerts2.css"} + %link{:rel => "stylesheet", :href => "/alerts-mobil.css", :media => "handheld"} + %script{:type => "text/javascript", :src => "/prototype.js"} + %script{:type => "text/javascript", :src => "/mauve_utils.js"} + %script{:type => "text/javascript", :src => "/datadumper.js"} + %body{:onLoad => "addAutoHover(); addRefresh();"} + .head= partial("head3") + #about_alert + %h1.summary #{@alert.get_safe_html_subject}: #{@alert.get_safe_html_summary} + %h2.details Alert Details + - if @alert.detail + .detail= @alert.get_details + - if @alert.source != @alert.subject + %h2.source Source + .source= @alert.get_safe_html_source + %h2.external Alert ID external + = @alert.get_safe_html_alert_id + %h2.internal Alert ID internal + = @alert.id + %h2.raised_at Last raised + = @alert.raised? ? @alert.raised_at : "Not yet raised" + %h2.acknowledged_at Acknowledged + = @alert.acknowledged? ? @alert.acknowledged_at : "Not acknowledged" + %h2.cleared_at Cleared + = @alert.cleared? ? @alert.cleared_at : "Not cleared" + %h2.alert_group Alert group + The first matching one is used: + = AlertGroup.matches(@alert).map{|g| g.name}.join("; ") + %h2.changes Notifications sent out + .changes + %ul + - @alert.changes.each do |change| + - if change.was_relevant? + %li + %strong= change.level + = change.update_type + notification to + = change.person + at + = change.at + %h2.actions Actions (ugly) + - if !@alert.acknowledged? + %form{:method => :post, :action => "/alert/#{@alert.id}/acknowledge"} + %input{:type => :submit, :value => "Acknowledge this alert"} + %h2.actions_dangerous Dangerous actions (also ugly) + - if @alert.raised? + %form{:method => :post, :action => "/alert/#{@alert.id}/clear"} + %input.dangerous{:type => :submit, :value => "Clear this alert"} + - if @alert.cleared? + %form{:method => :post, :action => "/alert/#{@alert.id}/raise"} + %input.dangerous{:type => :submit, :value => "Raise this alert"} + diff --git a/views/alerts.haml b/views/alerts.haml new file mode 100644 index 0000000..af65dea --- /dev/null +++ b/views/alerts.haml @@ -0,0 +1,24 @@ +!!! +%html + %head + %title #{@title}: Current alerts (logged in as #{@person.username}) + %link{:rel => "stylesheet", :href => "/alerts.css"} + %link{:rel => "stylesheet", :href => "/alerts-mobil.css", :media => "handheld"} + %script{:type => "text/javascript", :src => "/prototype.js"} + %script{:type => "text/javascript", :src => "/mauve_utils.js"} + %script{:type => "text/javascript", :src => "/datadumper.js"} + %body{:onLoad => "addAutoHover(); addRefresh();"} + #header + %h1= @title + #errors{:style => "display: none;"} + %h1 + Trouble! + ( + %a{:href=>"#", :onClick => "clearErrors();"} hide + ) + %ul#errors_list + %p Either the alert server or your internet connection is malfunctioning, so you may want to try refreshing the page. + #alert_counts= partial("alert_counts") + #navigation= partial("navigation") + #alert_summary= partial("alert_summary") + //%p The session is #{session.inspect()} diff --git a/views/alerts2.haml b/views/alerts2.haml new file mode 100644 index 0000000..ad0f0d3 --- /dev/null +++ b/views/alerts2.haml @@ -0,0 +1,18 @@ +!!! +%html + %head + %meta{:name=>"viewport", :content=>"width=device-width"} + %title Current alerts (logged in as #{@person.username}) + %link{:rel => "stylesheet", :href => "/alerts2.css"} + %script{:type => "text/javascript", :src => "/jquery-1.4.2.min.js"} + %script{:type => "text/javascript", :src => "/jquery.treeview.pack.js"} + %script{:type => "text/javascript", :src => "/jquery.cookie.js"} + %script{:type => "text/javascript", :src => "/jquery.pop.js"} + //%script{:type => "text/javascript", :src => "/jquery.countdown.js"} + %script{:type => "text/javascript", :src => "/mauve.js"} + %body + .darkMask + .head= partial("head3") + .unacknowledged= partial("unacknowledged") + .acknowledge= partial("acknowledge") + .updateAlertStatus= partial("jqChangeStatus") diff --git a/views/events.haml b/views/events.haml new file mode 100644 index 0000000..324e3b5 --- /dev/null +++ b/views/events.haml @@ -0,0 +1,22 @@ +!!! +%html + %head + %title #{@title}: Last events (logged in as #{@person.username}) + %link{:rel => "stylesheet", :href => "/alerts.css"} + %script{:type => "text/javascript", :src => "/prototype.js"} + %script{:type => "text/javascript", :src => "/mauve_utils.js"} + %body{:onLoad => "addAutoHover(); addRefresh();"} + #header + %h1= @title + #errors{:style => "display: none;"} + %h1 + Trouble! + ( + %a{:href=>"#", :onClick => "clearErrors();"} hide + ) + %ul#errors_list + %p Either the alert server or your internet connection is malfunctioning, so you may want to try refreshing the page. + #alert_counts= partial("alert_counts") + #navigation= partial("navigation") + #event_list= partial("event_list", :locals => {:alerts => @alerts}) + diff --git a/views/please_authenticate.haml b/views/please_authenticate.haml new file mode 100644 index 0000000..9416c8e --- /dev/null +++ b/views/please_authenticate.haml @@ -0,0 +1,25 @@ +!!! +%html + %head + %meta{:name=>"viewport", :content=>"width=device-width"} + %title #{@title}: Authentication required + %link{:rel => "stylesheet", :href => "/alerts2.css"} + %body + .loginForm + %form{:action => '/login', :method => :POST} + Username + %input{:name => 'username', :type => 'text'} + %br + Password + %input{:name => 'password', :type => 'password'} + %br + %input{:type => 'submit', :value => 'Log into the Mauve alert panel', :class=>"submitLoginButton"} + .loginNotes + This is either your single sign on or a PIN. + %br + You + %strong + must + be logged in to view alerts. + + diff --git a/views/preferences.haml b/views/preferences.haml new file mode 100644 index 0000000..c39c108 --- /dev/null +++ b/views/preferences.haml @@ -0,0 +1,24 @@ +!!! +%html + %head + %title #{@title}: My preferences (logged in as #{@person.username}) + %link{:rel => "stylesheet", :href => "/alerts.css"} + %script{:type => "text/javascript", :src => "/prototype.js"} + %script{:type => "text/javascript", :src => "/mauve_utils.js"} + %script{:type => "text/javascript", :src => "/datadumper.js"} + %body{:onLoad => "addAutoHover(); addRefresh();"} + #header + %h1= @title + #errors{:style => "display: none;"} + %h1 + Trouble! + ( + %a{:href=>"#", :onClick => "clearErrors();"} hide + ) + %ul#errors_list + %p Either the alert server or your internet connection is malfunctioning, so you may want to try refreshing the page. + #alert_counts= partial("alert_counts") + #navigation= partial("navigation") + %h1 Not implemented yet + + |