diff --git a/addons/account/__openerp__.py b/addons/account/__openerp__.py index a380a69dc03..e06e41cf001 100644 --- a/addons/account/__openerp__.py +++ b/addons/account/__openerp__.py @@ -146,6 +146,7 @@ for a particular financial year and for preparation of vouchers there is a modul 'account_unit_test.xml', ], 'test': [ + 'test/account_test_users.yml', 'test/account_customer_invoice.yml', 'test/account_supplier_invoice.yml', 'test/account_change_currency.yml', diff --git a/addons/account/account.py b/addons/account/account.py index 82957fc93d3..df623f32174 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -2788,6 +2788,7 @@ class account_chart_template(osv.osv): 'parent_id': fields.many2one('account.chart.template', 'Parent Chart Template'), 'code_digits': fields.integer('# of Digits', required=True, help="No. of Digits to use for account code"), 'visible': fields.boolean('Can be Visible?', help="Set this to False if you don't want this template to be used actively in the wizard that generate Chart of Accounts from templates, this is useful when you want to generate accounts of this template only when loading its child template."), + 'currency_id': fields.many2one('res.currency', 'Currency'), 'complete_tax_set': fields.boolean('Complete Set of Taxes', help='This boolean helps you to choose if you want to propose to the user to encode the sale and purchase rates or choose from list of taxes. This last choice assumes that the set of tax defined on this template is complete'), 'account_root_id': fields.many2one('account.account.template', 'Root Account', domain=[('parent_id','=',False)]), 'tax_code_root_id': fields.many2one('account.tax.code.template', 'Root Tax Code', domain=[('parent_id','=',False)]), @@ -3043,12 +3044,6 @@ class wizard_multi_charts_accounts(osv.osv_memory): 'complete_tax_set': fields.boolean('Complete Set of Taxes', help='This boolean helps you to choose if you want to propose to the user to encode the sales and purchase rates or use the usual m2o fields. This last choice assumes that the set of tax defined for the chosen template is complete'), } - def onchange_company_id(self, cr, uid, ids, company_id, context=None): - currency_id = False - if company_id: - currency_id = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id.id - return {'value': {'currency_id': currency_id}} - def onchange_tax_rate(self, cr, uid, ids, rate=False, context=None): return {'value': {'purchase_tax_rate': rate or False}} @@ -3058,7 +3053,8 @@ class wizard_multi_charts_accounts(osv.osv_memory): res['value'] = {'complete_tax_set': False, 'sale_tax': False, 'purchase_tax': False} if chart_template_id: data = self.pool.get('account.chart.template').browse(cr, uid, chart_template_id, context=context) - res['value'].update({'complete_tax_set': data.complete_tax_set}) + currency_id = data.currency_id and data.currency_id.id or self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id + res['value'].update({'complete_tax_set': data.complete_tax_set, 'currency_id': currency_id}) if data.complete_tax_set: # default tax is given by the lowest sequence. For same sequence we will take the latest created as it will be the case for tax created while isntalling the generic chart of account sale_tax_ids = tax_templ_obj.search(cr, uid, [("chart_template_id" @@ -3090,7 +3086,13 @@ class wizard_multi_charts_accounts(osv.osv_memory): ids = self.pool.get('account.chart.template').search(cr, uid, [('visible', '=', True)], context=context) if ids: if 'chart_template_id' in fields: - res.update({'only_one_chart_template': len(ids) == 1, 'chart_template_id': ids[0]}) + #in order to get set default chart which was last created set max of ids. + chart_id = max(ids) + if context.get("default_charts"): + model_data = self.pool.get('ir.model.data').search_read(cr, uid, [('model','=','account.chart.template'),('module','=',context.get("default_charts"))], ['res_id'], context=context) + if model_data: + chart_id = model_data[0]['res_id'] + res.update({'only_one_chart_template': len(ids) == 1, 'chart_template_id': chart_id}) if 'sale_tax' in fields: sale_tax_ids = tax_templ_obj.search(cr, uid, [("chart_template_id" , "=", ids[0]), ('type_tax_use', 'in', ('sale','all'))], order="sequence") diff --git a/addons/account/account_installer.xml b/addons/account/account_installer.xml index b03babc63ac..8d1b25b299f 100644 --- a/addons/account/account_installer.xml +++ b/addons/account/account_installer.xml @@ -10,7 +10,7 @@ diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index 4fae65917bf..ed2d88b7386 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -2120,7 +2120,7 @@ - + diff --git a/addons/contacts/i18n/da.po b/addons/contacts/i18n/da.po new file mode 100644 index 00000000000..3a04b0556ad --- /dev/null +++ b/addons/contacts/i18n/da.po @@ -0,0 +1,45 @@ +# Danish translation for openobject-addons +# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:05+0000\n" +"PO-Revision-Date: 2013-09-15 20:14+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Danish \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2013-09-16 05:42+0000\n" +"X-Generator: Launchpad (build 16761)\n" + +#. module: contacts +#: model:ir.actions.act_window,help:contacts.action_contacts +msgid "" +"

\n" +" Click to add a contact in your address book.\n" +"

\n" +" OpenERP helps you easily track all activities related to\n" +" a customer; discussions, history of business opportunities,\n" +" documents, etc.\n" +"

\n" +" " +msgstr "" +"

\n" +" Klik for at tilføje kontakt i din adressebog.\n" +"

\n" +" OpenERP hjælper med at spore alle aktiviteter i forbindelse med\n" +" kunde korrespondance, historik på forretnings muligheder,\n" +" dokumenter, o.s.v.\n" +"

\n" +" " + +#. module: contacts +#: model:ir.actions.act_window,name:contacts.action_contacts +#: model:ir.ui.menu,name:contacts.menu_contacts +msgid "Contacts" +msgstr "Adressebog" diff --git a/addons/crm/__openerp__.py b/addons/crm/__openerp__.py index 1b4ba191b82..e3443d5200f 100644 --- a/addons/crm/__openerp__.py +++ b/addons/crm/__openerp__.py @@ -57,7 +57,8 @@ Dashboard for CRM will include: 'base_calendar', 'resource', 'board', - 'fetchmail' + 'fetchmail', + 'web_kanban_sparkline', ], 'data': [ 'crm_data.xml', @@ -105,6 +106,7 @@ Dashboard for CRM will include: 'crm_action_rule_demo.xml', ], 'test': [ + 'test/crm_access_group_users.yml', 'test/crm_lead_message.yml', 'test/lead2opportunity2win.yml', 'test/lead2opportunity_assign_salesmen.yml', @@ -121,7 +123,6 @@ Dashboard for CRM will include: 'static/src/css/crm.css' ], 'js': [ - 'static/lib/sparkline/jquery.sparkline.js', 'static/src/js/crm_case_section.js', ], 'installable': True, diff --git a/addons/crm/crm_case_section_view.xml b/addons/crm/crm_case_section_view.xml index 24862a39e89..99b3296c3c1 100644 --- a/addons/crm/crm_case_section_view.xml +++ b/addons/crm/crm_case_section_view.xml @@ -99,11 +99,11 @@ diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index e49ae818cae..9d17526c19f 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -253,6 +253,8 @@ class crm_lead(format_address, osv.osv): multi='day_close', type="float", store=True), 'date_last_stage_update': fields.datetime('Last Stage Update', select=True), + # Messaging and marketing + 'message_bounce': fields.integer('Bounce'), # Only used for type opportunity 'probability': fields.float('Success Rate (%)', group_operator="avg"), 'planned_revenue': fields.float('Expected Revenue', track_visibility='always'), diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index d885627a2c2..c0eefcc8d4c 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -169,16 +169,17 @@ - - - - - - - - + + + + + + + + + diff --git a/addons/crm/security/crm_security.xml b/addons/crm/security/crm_security.xml index 34137dc0fac..ef7eb8b24db 100644 --- a/addons/crm/security/crm_security.xml +++ b/addons/crm/security/crm_security.xml @@ -5,6 +5,7 @@ User: Own Leads Only + the user will have access to his own data in the sales application. diff --git a/addons/crm/static/lib/sparkline/jquery.sparkline.js b/addons/crm/static/lib/sparkline/jquery.sparkline.js deleted file mode 100644 index c003923e03b..00000000000 --- a/addons/crm/static/lib/sparkline/jquery.sparkline.js +++ /dev/null @@ -1,3047 +0,0 @@ -/** -* -* jquery.sparkline.js -* -* v2.1.1 -* (c) Splunk, Inc -* Contact: Gareth Watts (gareth@splunk.com) -* http://omnipotent.net/jquery.sparkline/ -* -* Generates inline sparkline charts from data supplied either to the method -* or inline in HTML -* -* Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag -* (Firefox 2.0+, Safari, Opera, etc) -* -* License: New BSD License -* -* Copyright (c) 2012, Splunk Inc. -* All rights reserved. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* * Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* * Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* * Neither the name of Splunk Inc nor the names of its contributors may -* be used to endorse or promote products derived from this software without -* specific prior written permission. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY -* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT -* SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT -* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -* -* -* Usage: -* $(selector).sparkline(values, options) -* -* If values is undefined or set to 'html' then the data values are read from the specified tag: -*

Sparkline: 1,4,6,6,8,5,3,5

-* $('.sparkline').sparkline(); -* There must be no spaces in the enclosed data set -* -* Otherwise values must be an array of numbers or null values -*

Sparkline: This text replaced if the browser is compatible

-* $('#sparkline1').sparkline([1,4,6,6,8,5,3,5]) -* $('#sparkline2').sparkline([1,4,6,null,null,5,3,5]) -* -* Values can also be specified in an HTML comment, or as a values attribute: -*

Sparkline:

-*

Sparkline:

-* $('.sparkline').sparkline(); -* -* For line charts, x values can also be specified: -*

Sparkline: 1:1,2.7:4,3.4:6,5:6,6:8,8.7:5,9:3,10:5

-* $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ]) -* -* By default, options should be passed in as teh second argument to the sparkline function: -* $('.sparkline').sparkline([1,2,3,4], {type: 'bar'}) -* -* Options can also be set by passing them on the tag itself. This feature is disabled by default though -* as there's a slight performance overhead: -* $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true}) -*

Sparkline: loading

-* Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionPrefix) -* -* Supported options: -* lineColor - Color of the line used for the chart -* fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart -* width - Width of the chart - Defaults to 3 times the number of values in pixels -* height - Height of the chart - Defaults to the height of the containing element -* chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied -* chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied -* chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax -* chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied -* chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied -* composite - If true then don't erase any existing chart attached to the tag, but draw -* another chart over the top - Note that width and height are ignored if an -* existing chart is detected. -* tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values' -* enableTagOptions - Whether to check tags for sparkline options -* tagOptionPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark' -* disableHiddenCheck - If set to true, then the plugin will assume that charts will never be drawn into a -* hidden dom element, avoding a browser reflow -* disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled, -* making the plugin perform much like it did in 1.x -* disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled) -* disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled -* defaults to false (highlights enabled) -* highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase -* tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body -* tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied -* tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis -* tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis -* tooltipFormatter - Optional callback that allows you to override the HTML displayed in the tooltip -* callback is given arguments of (sparkline, options, fields) -* tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title -* tooltipFormat - A format string or SPFormat object (or an array thereof for multiple entries) -* to control the format of the tooltip -* tooltipPrefix - A string to prepend to each field displayed in a tooltip -* tooltipSuffix - A string to append to each field displayed in a tooltip -* tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true) -* tooltipValueLookups - An object or range map to map field values to tooltip strings -* (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win") -* numberFormatter - Optional callback for formatting numbers in tooltips -* numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to "," -* numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "." -* numberDigitGroupCount - Number of digits between group separator - Defaults to 3 -* -* There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default), -* 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box' -* line - Line chart. Options: -* spotColor - Set to '' to not end each line in a circular spot -* minSpotColor - If set, color of spot at minimum value -* maxSpotColor - If set, color of spot at maximum value -* spotRadius - Radius in pixels -* lineWidth - Width of line in pixels -* normalRangeMin -* normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal" -* or expected range of values -* normalRangeColor - Color to use for the above bar -* drawNormalOnTop - Draw the normal range above the chart fill color if true -* defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart -* highlightSpotColor - The color to use for drawing a highlight spot on mouseover - Set to null to disable -* highlightLineColor - The color to use for drawing a highlight line on mouseover - Set to null to disable -* valueSpots - Specify which points to draw spots on, and in which color. Accepts a range map -* -* bar - Bar chart. Options: -* barColor - Color of bars for postive values -* negBarColor - Color of bars for negative values -* zeroColor - Color of bars with zero values -* nullColor - Color of bars with null values - Defaults to omitting the bar entirely -* barWidth - Width of bars in pixels -* colorMap - Optional mappnig of values to colors to override the *BarColor values above -* can be an Array of values to control the color of individual bars or a range map -* to specify colors for individual ranges of values -* barSpacing - Gap between bars in pixels -* zeroAxis - Centers the y-axis around zero if true -* -* tristate - Charts values of win (>0), lose (<0) or draw (=0) -* posBarColor - Color of win values -* negBarColor - Color of lose values -* zeroBarColor - Color of draw values -* barWidth - Width of bars in pixels -* barSpacing - Gap between bars in pixels -* colorMap - Optional mappnig of values to colors to override the *BarColor values above -* can be an Array of values to control the color of individual bars or a range map -* to specify colors for individual ranges of values -* -* discrete - Options: -* lineHeight - Height of each line in pixels - Defaults to 30% of the graph height -* thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor -* thresholdColor -* -* bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ... -* options: -* targetColor - The color of the vertical target marker -* targetWidth - The width of the target marker in pixels -* performanceColor - The color of the performance measure horizontal bar -* rangeColors - Colors to use for each qualitative range background color -* -* pie - Pie chart. Options: -* sliceColors - An array of colors to use for pie slices -* offset - Angle in degrees to offset the first slice - Try -90 or +90 -* borderWidth - Width of border to draw around the pie chart, in pixels - Defaults to 0 (no border) -* borderColor - Color to use for the pie chart border - Defaults to #000 -* -* box - Box plot. Options: -* raw - Set to true to supply pre-computed plot points as values -* values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier -* When set to false you can supply any number of values and the box plot will -* be computed for you. Default is false. -* showOutliers - Set to true (default) to display outliers as circles -* outlierIQR - Interquartile range used to determine outliers. Default 1.5 -* boxLineColor - Outline color of the box -* boxFillColor - Fill color for the box -* whiskerColor - Line color used for whiskers -* outlierLineColor - Outline color of outlier circles -* outlierFillColor - Fill color of the outlier circles -* spotRadius - Radius of outlier circles -* medianColor - Line color of the median line -* target - Draw a target cross hair at the supplied value (default undefined) -* -* -* -* Examples: -* $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false }); -* $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 }); -* $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }): -* $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' }); -* $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' }); -* $('#pie').sparkline([1,1,2], { type:'pie' }); -*/ - -/*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */ - -(function(factory) { - if(typeof define === 'function' && define.amd) { - define(['jquery'], factory); - } - else { - factory(jQuery); - } -} -(function($) { - 'use strict'; - - var UNSET_OPTION = {}, - getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues, - remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap, - MouseHandler, Tooltip, barHighlightMixin, - line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles, - VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0; - - /** - * Default configuration settings - */ - getDefaults = function () { - return { - // Settings common to most/all chart types - common: { - type: 'line', - lineColor: '#00f', - fillColor: '#cdf', - defaultPixelsPerValue: 3, - width: 'auto', - height: 'auto', - composite: false, - tagValuesAttribute: 'values', - tagOptionsPrefix: 'spark', - enableTagOptions: false, - enableHighlight: true, - highlightLighten: 1.4, - tooltipSkipNull: true, - tooltipPrefix: '', - tooltipSuffix: '', - disableHiddenCheck: false, - numberFormatter: false, - numberDigitGroupCount: 3, - numberDigitGroupSep: ',', - numberDecimalMark: '.', - disableTooltips: false, - disableInteraction: false - }, - // Defaults for line charts - line: { - spotColor: '#f80', - highlightSpotColor: '#5f5', - highlightLineColor: '#f22', - spotRadius: 1.5, - minSpotColor: '#f80', - maxSpotColor: '#f80', - lineWidth: 1, - normalRangeMin: undefined, - normalRangeMax: undefined, - normalRangeColor: '#ccc', - drawNormalOnTop: false, - chartRangeMin: undefined, - chartRangeMax: undefined, - chartRangeMinX: undefined, - chartRangeMaxX: undefined, - tooltipFormat: new SPFormat('● {{prefix}}{{y}}{{suffix}}') - }, - // Defaults for bar charts - bar: { - barColor: '#3366cc', - negBarColor: '#f44', - stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00', - '#dd4477', '#0099c6', '#990099'], - zeroColor: undefined, - nullColor: undefined, - zeroAxis: true, - barWidth: 4, - barSpacing: 1, - chartRangeMax: undefined, - chartRangeMin: undefined, - chartRangeClip: false, - colorMap: undefined, - tooltipFormat: new SPFormat('● {{prefix}}{{value}}{{suffix}}') - }, - // Defaults for tristate charts - tristate: { - barWidth: 4, - barSpacing: 1, - posBarColor: '#6f6', - negBarColor: '#f44', - zeroBarColor: '#999', - colorMap: {}, - tooltipFormat: new SPFormat('● {{value:map}}'), - tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } } - }, - // Defaults for discrete charts - discrete: { - lineHeight: 'auto', - thresholdColor: undefined, - thresholdValue: 0, - chartRangeMax: undefined, - chartRangeMin: undefined, - chartRangeClip: false, - tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}') - }, - // Defaults for bullet charts - bullet: { - targetColor: '#f33', - targetWidth: 3, // width of the target bar in pixels - performanceColor: '#33f', - rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'], - base: undefined, // set this to a number to change the base start number - tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'), - tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} } - }, - // Defaults for pie charts - pie: { - offset: 0, - sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00', - '#dd4477', '#0099c6', '#990099'], - borderWidth: 0, - borderColor: '#000', - tooltipFormat: new SPFormat('● {{value}} ({{percent.1}}%)') - }, - // Defaults for box plots - box: { - raw: false, - boxLineColor: '#000', - boxFillColor: '#cdf', - whiskerColor: '#000', - outlierLineColor: '#333', - outlierFillColor: '#fff', - medianColor: '#f00', - showOutliers: true, - outlierIQR: 1.5, - spotRadius: 1.5, - target: undefined, - targetColor: '#4a2', - chartRangeMax: undefined, - chartRangeMin: undefined, - tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'), - tooltipFormatFieldlistKey: 'field', - tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median', - uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier', - lw: 'Left Whisker', rw: 'Right Whisker'} } - } - }; - }; - - // You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname - defaultStyles = '.jqstooltip { ' + - 'position: absolute;' + - 'left: 0px;' + - 'top: 0px;' + - 'visibility: hidden;' + - 'background: rgb(0, 0, 0) transparent;' + - 'background-color: rgba(0,0,0,0.6);' + - 'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' + - '-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' + - 'color: white;' + - 'font: 10px arial, san serif;' + - 'text-align: left;' + - 'white-space: nowrap;' + - 'padding: 5px;' + - 'border: 1px solid white;' + - 'z-index: 10000;' + - '}' + - '.jqsfield { ' + - 'color: white;' + - 'font: 10px arial, san serif;' + - 'text-align: left;' + - '}'; - - /** - * Utilities - */ - - createClass = function (/* [baseclass, [mixin, ...]], definition */) { - var Class, args; - Class = function () { - this.init.apply(this, arguments); - }; - if (arguments.length > 1) { - if (arguments[0]) { - Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]); - Class._super = arguments[0].prototype; - } else { - Class.prototype = arguments[arguments.length - 1]; - } - if (arguments.length > 2) { - args = Array.prototype.slice.call(arguments, 1, -1); - args.unshift(Class.prototype); - $.extend.apply($, args); - } - } else { - Class.prototype = arguments[0]; - } - Class.prototype.cls = Class; - return Class; - }; - - /** - * Wraps a format string for tooltips - * {{x}} - * {{x.2} - * {{x:months}} - */ - $.SPFormatClass = SPFormat = createClass({ - fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g, - precre: /(\w+)\.(\d+)/, - - init: function (format, fclass) { - this.format = format; - this.fclass = fclass; - }, - - render: function (fieldset, lookups, options) { - var self = this, - fields = fieldset, - match, token, lookupkey, fieldvalue, prec; - return this.format.replace(this.fre, function () { - var lookup; - token = arguments[1]; - lookupkey = arguments[3]; - match = self.precre.exec(token); - if (match) { - prec = match[2]; - token = match[1]; - } else { - prec = false; - } - fieldvalue = fields[token]; - if (fieldvalue === undefined) { - return ''; - } - if (lookupkey && lookups && lookups[lookupkey]) { - lookup = lookups[lookupkey]; - if (lookup.get) { // RangeMap - return lookups[lookupkey].get(fieldvalue) || fieldvalue; - } else { - return lookups[lookupkey][fieldvalue] || fieldvalue; - } - } - if (isNumber(fieldvalue)) { - if (options.get('numberFormatter')) { - fieldvalue = options.get('numberFormatter')(fieldvalue); - } else { - fieldvalue = formatNumber(fieldvalue, prec, - options.get('numberDigitGroupCount'), - options.get('numberDigitGroupSep'), - options.get('numberDecimalMark')); - } - } - return fieldvalue; - }); - } - }); - - // convience method to avoid needing the new operator - $.spformat = function(format, fclass) { - return new SPFormat(format, fclass); - }; - - clipval = function (val, min, max) { - if (val < min) { - return min; - } - if (val > max) { - return max; - } - return val; - }; - - quartile = function (values, q) { - var vl; - if (q === 2) { - vl = Math.floor(values.length / 2); - return values.length % 2 ? values[vl] : (values[vl-1] + values[vl]) / 2; - } else { - if (values.length % 2 ) { // odd - vl = (values.length * q + q) / 4; - return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1]; - } else { //even - vl = (values.length * q + 2) / 4; - return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1]; - - } - } - }; - - normalizeValue = function (val) { - var nf; - switch (val) { - case 'undefined': - val = undefined; - break; - case 'null': - val = null; - break; - case 'true': - val = true; - break; - case 'false': - val = false; - break; - default: - nf = parseFloat(val); - if (val == nf) { - val = nf; - } - } - return val; - }; - - normalizeValues = function (vals) { - var i, result = []; - for (i = vals.length; i--;) { - result[i] = normalizeValue(vals[i]); - } - return result; - }; - - remove = function (vals, filter) { - var i, vl, result = []; - for (i = 0, vl = vals.length; i < vl; i++) { - if (vals[i] !== filter) { - result.push(vals[i]); - } - } - return result; - }; - - isNumber = function (num) { - return !isNaN(parseFloat(num)) && isFinite(num); - }; - - formatNumber = function (num, prec, groupsize, groupsep, decsep) { - var p, i; - num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split(''); - p = (p = $.inArray('.', num)) < 0 ? num.length : p; - if (p < num.length) { - num[p] = decsep; - } - for (i = p - groupsize; i > 0; i -= groupsize) { - num.splice(i, 0, groupsep); - } - return num.join(''); - }; - - // determine if all values of an array match a value - // returns true if the array is empty - all = function (val, arr, ignoreNull) { - var i; - for (i = arr.length; i--; ) { - if (ignoreNull && arr[i] === null) continue; - if (arr[i] !== val) { - return false; - } - } - return true; - }; - - // sums the numeric values in an array, ignoring other values - sum = function (vals) { - var total = 0, i; - for (i = vals.length; i--;) { - total += typeof vals[i] === 'number' ? vals[i] : 0; - } - return total; - }; - - ensureArray = function (val) { - return $.isArray(val) ? val : [val]; - }; - - // http://paulirish.com/2008/bookmarklet-inject-new-css-rules/ - addCSS = function(css) { - var tag; - //if ('\v' == 'v') /* ie only */ { - if (document.createStyleSheet) { - document.createStyleSheet().cssText = css; - } else { - tag = document.createElement('style'); - tag.type = 'text/css'; - document.getElementsByTagName('head')[0].appendChild(tag); - tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css; - } - }; - - // Provide a cross-browser interface to a few simple drawing primitives - $.fn.simpledraw = function (width, height, useExisting, interact) { - var target, mhandler; - if (useExisting && (target = this.data('_jqs_vcanvas'))) { - return target; - } - if (width === undefined) { - width = $(this).innerWidth(); - } - if (height === undefined) { - height = $(this).innerHeight(); - } - if ($.fn.sparkline.hasCanvas) { - target = new VCanvas_canvas(width, height, this, interact); - } else if ($.fn.sparkline.hasVML) { - target = new VCanvas_vml(width, height, this); - } else { - return false; - } - mhandler = $(this).data('_jqs_mhandler'); - if (mhandler) { - mhandler.registerCanvas(target); - } - return target; - }; - - $.fn.cleardraw = function () { - var target = this.data('_jqs_vcanvas'); - if (target) { - target.reset(); - } - }; - - $.RangeMapClass = RangeMap = createClass({ - init: function (map) { - var key, range, rangelist = []; - for (key in map) { - if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) { - range = key.split(':'); - range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]); - range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]); - range[2] = map[key]; - rangelist.push(range); - } - } - this.map = map; - this.rangelist = rangelist || false; - }, - - get: function (value) { - var rangelist = this.rangelist, - i, range, result; - if ((result = this.map[value]) !== undefined) { - return result; - } - if (rangelist) { - for (i = rangelist.length; i--;) { - range = rangelist[i]; - if (range[0] <= value && range[1] >= value) { - return range[2]; - } - } - } - return undefined; - } - }); - - // Convenience function - $.range_map = function(map) { - return new RangeMap(map); - }; - - MouseHandler = createClass({ - init: function (el, options) { - var $el = $(el); - this.$el = $el; - this.options = options; - this.currentPageX = 0; - this.currentPageY = 0; - this.el = el; - this.splist = []; - this.tooltip = null; - this.over = false; - this.displayTooltips = !options.get('disableTooltips'); - this.highlightEnabled = !options.get('disableHighlight'); - }, - - registerSparkline: function (sp) { - this.splist.push(sp); - if (this.over) { - this.updateDisplay(); - } - }, - - registerCanvas: function (canvas) { - var $canvas = $(canvas.canvas); - this.canvas = canvas; - this.$canvas = $canvas; - $canvas.mouseenter($.proxy(this.mouseenter, this)); - $canvas.mouseleave($.proxy(this.mouseleave, this)); - $canvas.click($.proxy(this.mouseclick, this)); - }, - - reset: function (removeTooltip) { - this.splist = []; - if (this.tooltip && removeTooltip) { - this.tooltip.remove(); - this.tooltip = undefined; - } - }, - - mouseclick: function (e) { - var clickEvent = $.Event('sparklineClick'); - clickEvent.originalEvent = e; - clickEvent.sparklines = this.splist; - this.$el.trigger(clickEvent); - }, - - mouseenter: function (e) { - $(document.body).unbind('mousemove.jqs'); - $(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this)); - this.over = true; - this.currentPageX = e.pageX; - this.currentPageY = e.pageY; - this.currentEl = e.target; - if (!this.tooltip && this.displayTooltips) { - this.tooltip = new Tooltip(this.options); - this.tooltip.updatePosition(e.pageX, e.pageY); - } - this.updateDisplay(); - }, - - mouseleave: function () { - $(document.body).unbind('mousemove.jqs'); - var splist = this.splist, - spcount = splist.length, - needsRefresh = false, - sp, i; - this.over = false; - this.currentEl = null; - - if (this.tooltip) { - this.tooltip.remove(); - this.tooltip = null; - } - - for (i = 0; i < spcount; i++) { - sp = splist[i]; - if (sp.clearRegionHighlight()) { - needsRefresh = true; - } - } - - if (needsRefresh) { - this.canvas.render(); - } - }, - - mousemove: function (e) { - this.currentPageX = e.pageX; - this.currentPageY = e.pageY; - this.currentEl = e.target; - if (this.tooltip) { - this.tooltip.updatePosition(e.pageX, e.pageY); - } - this.updateDisplay(); - }, - - updateDisplay: function () { - var splist = this.splist, - spcount = splist.length, - needsRefresh = false, - offset = this.$canvas.offset(), - localX = this.currentPageX - offset.left, - localY = this.currentPageY - offset.top, - tooltiphtml, sp, i, result, changeEvent; - if (!this.over) { - return; - } - for (i = 0; i < spcount; i++) { - sp = splist[i]; - result = sp.setRegionHighlight(this.currentEl, localX, localY); - if (result) { - needsRefresh = true; - } - } - if (needsRefresh) { - changeEvent = $.Event('sparklineRegionChange'); - changeEvent.sparklines = this.splist; - this.$el.trigger(changeEvent); - if (this.tooltip) { - tooltiphtml = ''; - for (i = 0; i < spcount; i++) { - sp = splist[i]; - tooltiphtml += sp.getCurrentRegionTooltip(); - } - this.tooltip.setContent(tooltiphtml); - } - if (!this.disableHighlight) { - this.canvas.render(); - } - } - if (result === null) { - this.mouseleave(); - } - } - }); - - - Tooltip = createClass({ - sizeStyle: 'position: static !important;' + - 'display: block !important;' + - 'visibility: hidden !important;' + - 'float: left !important;', - - init: function (options) { - var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'), - sizetipStyle = this.sizeStyle, - offset; - this.container = options.get('tooltipContainer') || document.body; - this.tooltipOffsetX = options.get('tooltipOffsetX', 10); - this.tooltipOffsetY = options.get('tooltipOffsetY', 12); - // remove any previous lingering tooltip - $('#jqssizetip').remove(); - $('#jqstooltip').remove(); - this.sizetip = $('
', { - id: 'jqssizetip', - style: sizetipStyle, - 'class': tooltipClassname - }); - this.tooltip = $('
', { - id: 'jqstooltip', - 'class': tooltipClassname - }).appendTo(this.container); - // account for the container's location - offset = this.tooltip.offset(); - this.offsetLeft = offset.left; - this.offsetTop = offset.top; - this.hidden = true; - $(window).unbind('resize.jqs scroll.jqs'); - $(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this)); - this.updateWindowDims(); - }, - - updateWindowDims: function () { - this.scrollTop = $(window).scrollTop(); - this.scrollLeft = $(window).scrollLeft(); - this.scrollRight = this.scrollLeft + $(window).width(); - this.updatePosition(); - }, - - getSize: function (content) { - this.sizetip.html(content).appendTo(this.container); - this.width = this.sizetip.width() + 1; - this.height = this.sizetip.height(); - this.sizetip.remove(); - }, - - setContent: function (content) { - if (!content) { - this.tooltip.css('visibility', 'hidden'); - this.hidden = true; - return; - } - this.getSize(content); - this.tooltip.html(content) - .css({ - 'width': this.width, - 'height': this.height, - 'visibility': 'visible' - }); - if (this.hidden) { - this.hidden = false; - this.updatePosition(); - } - }, - - updatePosition: function (x, y) { - if (x === undefined) { - if (this.mousex === undefined) { - return; - } - x = this.mousex - this.offsetLeft; - y = this.mousey - this.offsetTop; - - } else { - this.mousex = x = x - this.offsetLeft; - this.mousey = y = y - this.offsetTop; - } - if (!this.height || !this.width || this.hidden) { - return; - } - - y -= this.height + this.tooltipOffsetY; - x += this.tooltipOffsetX; - - if (y < this.scrollTop) { - y = this.scrollTop; - } - if (x < this.scrollLeft) { - x = this.scrollLeft; - } else if (x + this.width > this.scrollRight) { - x = this.scrollRight - this.width; - } - - this.tooltip.css({ - 'left': x, - 'top': y - }); - }, - - remove: function () { - this.tooltip.remove(); - this.sizetip.remove(); - this.sizetip = this.tooltip = undefined; - $(window).unbind('resize.jqs scroll.jqs'); - } - }); - - initStyles = function() { - addCSS(defaultStyles); - }; - - $(initStyles); - - pending = []; - $.fn.sparkline = function (userValues, userOptions) { - return this.each(function () { - var options = new $.fn.sparkline.options(this, userOptions), - $this = $(this), - render, i; - render = function () { - var values, width, height, tmp, mhandler, sp, vals; - if (userValues === 'html' || userValues === undefined) { - vals = this.getAttribute(options.get('tagValuesAttribute')); - if (vals === undefined || vals === null) { - vals = $this.html(); - } - values = vals.replace(/(^\s*\s*$)|\s+/g, '').split(','); - } else { - values = userValues; - } - - width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width'); - if (options.get('height') === 'auto') { - if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) { - // must be a better way to get the line height - tmp = document.createElement('span'); - tmp.innerHTML = 'a'; - $this.html(tmp); - height = $(tmp).innerHeight() || $(tmp).height(); - $(tmp).remove(); - tmp = null; - } - } else { - height = options.get('height'); - } - - if (!options.get('disableInteraction')) { - mhandler = $.data(this, '_jqs_mhandler'); - if (!mhandler) { - mhandler = new MouseHandler(this, options); - $.data(this, '_jqs_mhandler', mhandler); - } else if (!options.get('composite')) { - mhandler.reset(); - } - } else { - mhandler = false; - } - - if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) { - if (!$.data(this, '_jqs_errnotify')) { - alert('Attempted to attach a composite sparkline to an element with no existing sparkline'); - $.data(this, '_jqs_errnotify', true); - } - return; - } - - sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height); - - sp.render(); - - if (mhandler) { - mhandler.registerSparkline(sp); - } - }; - // jQuery 1.3.0 completely changed the meaning of :hidden :-/ - if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || ($.fn.jquery < '1.3.0' && $(this).parents().is(':hidden')) || !$(this).parents('body').length) { - if (!options.get('composite') && $.data(this, '_jqs_pending')) { - // remove any existing references to the element - for (i = pending.length; i; i--) { - if (pending[i - 1][0] == this) { - pending.splice(i - 1, 1); - } - } - } - pending.push([this, render]); - $.data(this, '_jqs_pending', true); - } else { - render.call(this); - } - }); - }; - - $.fn.sparkline.defaults = getDefaults(); - - - $.sparkline_display_visible = function () { - var el, i, pl; - var done = []; - for (i = 0, pl = pending.length; i < pl; i++) { - el = pending[i][0]; - if ($(el).is(':visible') && !$(el).parents().is(':hidden')) { - pending[i][1].call(el); - $.data(pending[i][0], '_jqs_pending', false); - done.push(i); - } else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) { - // element has been inserted and removed from the DOM - // If it was not yet inserted into the dom then the .data request - // will return true. - // removing from the dom causes the data to be removed. - $.data(pending[i][0], '_jqs_pending', false); - done.push(i); - } - } - for (i = done.length; i; i--) { - pending.splice(done[i - 1], 1); - } - }; - - - /** - * User option handler - */ - $.fn.sparkline.options = createClass({ - init: function (tag, userOptions) { - var extendedOptions, defaults, base, tagOptionType; - this.userOptions = userOptions = userOptions || {}; - this.tag = tag; - this.tagValCache = {}; - defaults = $.fn.sparkline.defaults; - base = defaults.common; - this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix); - - tagOptionType = this.getTagSetting('type'); - if (tagOptionType === UNSET_OPTION) { - extendedOptions = defaults[userOptions.type || base.type]; - } else { - extendedOptions = defaults[tagOptionType]; - } - this.mergedOptions = $.extend({}, base, extendedOptions, userOptions); - }, - - - getTagSetting: function (key) { - var prefix = this.tagOptionsPrefix, - val, i, pairs, keyval; - if (prefix === false || prefix === undefined) { - return UNSET_OPTION; - } - if (this.tagValCache.hasOwnProperty(key)) { - val = this.tagValCache.key; - } else { - val = this.tag.getAttribute(prefix + key); - if (val === undefined || val === null) { - val = UNSET_OPTION; - } else if (val.substr(0, 1) === '[') { - val = val.substr(1, val.length - 2).split(','); - for (i = val.length; i--;) { - val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, '')); - } - } else if (val.substr(0, 1) === '{') { - pairs = val.substr(1, val.length - 2).split(','); - val = {}; - for (i = pairs.length; i--;) { - keyval = pairs[i].split(':', 2); - val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, '')); - } - } else { - val = normalizeValue(val); - } - this.tagValCache.key = val; - } - return val; - }, - - get: function (key, defaultval) { - var tagOption = this.getTagSetting(key), - result; - if (tagOption !== UNSET_OPTION) { - return tagOption; - } - return (result = this.mergedOptions[key]) === undefined ? defaultval : result; - } - }); - - - $.fn.sparkline._base = createClass({ - disabled: false, - - init: function (el, values, options, width, height) { - this.el = el; - this.$el = $(el); - this.values = values; - this.options = options; - this.width = width; - this.height = height; - this.currentRegion = undefined; - }, - - /** - * Setup the canvas - */ - initTarget: function () { - var interactive = !this.options.get('disableInteraction'); - if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) { - this.disabled = true; - } else { - this.canvasWidth = this.target.pixelWidth; - this.canvasHeight = this.target.pixelHeight; - } - }, - - /** - * Actually render the chart to the canvas - */ - render: function () { - if (this.disabled) { - this.el.innerHTML = ''; - return false; - } - return true; - }, - - /** - * Return a region id for a given x/y co-ordinate - */ - getRegion: function (x, y) { - }, - - /** - * Highlight an item based on the moused-over x,y co-ordinate - */ - setRegionHighlight: function (el, x, y) { - var currentRegion = this.currentRegion, - highlightEnabled = !this.options.get('disableHighlight'), - newRegion; - if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) { - return null; - } - newRegion = this.getRegion(el, x, y); - if (currentRegion !== newRegion) { - if (currentRegion !== undefined && highlightEnabled) { - this.removeHighlight(); - } - this.currentRegion = newRegion; - if (newRegion !== undefined && highlightEnabled) { - this.renderHighlight(); - } - return true; - } - return false; - }, - - /** - * Reset any currently highlighted item - */ - clearRegionHighlight: function () { - if (this.currentRegion !== undefined) { - this.removeHighlight(); - this.currentRegion = undefined; - return true; - } - return false; - }, - - renderHighlight: function () { - this.changeHighlight(true); - }, - - removeHighlight: function () { - this.changeHighlight(false); - }, - - changeHighlight: function (highlight) {}, - - /** - * Fetch the HTML to display as a tooltip - */ - getCurrentRegionTooltip: function () { - var options = this.options, - header = '', - entries = [], - fields, formats, formatlen, fclass, text, i, - showFields, showFieldsKey, newFields, fv, - formatter, format, fieldlen, j; - if (this.currentRegion === undefined) { - return ''; - } - fields = this.getCurrentRegionFields(); - formatter = options.get('tooltipFormatter'); - if (formatter) { - return formatter(this, options, fields); - } - if (options.get('tooltipChartTitle')) { - header += '
' + options.get('tooltipChartTitle') + '
\n'; - } - formats = this.options.get('tooltipFormat'); - if (!formats) { - return ''; - } - if (!$.isArray(formats)) { - formats = [formats]; - } - if (!$.isArray(fields)) { - fields = [fields]; - } - showFields = this.options.get('tooltipFormatFieldlist'); - showFieldsKey = this.options.get('tooltipFormatFieldlistKey'); - if (showFields && showFieldsKey) { - // user-selected ordering of fields - newFields = []; - for (i = fields.length; i--;) { - fv = fields[i][showFieldsKey]; - if ((j = $.inArray(fv, showFields)) != -1) { - newFields[j] = fields[i]; - } - } - fields = newFields; - } - formatlen = formats.length; - fieldlen = fields.length; - for (i = 0; i < formatlen; i++) { - format = formats[i]; - if (typeof format === 'string') { - format = new SPFormat(format); - } - fclass = format.fclass || 'jqsfield'; - for (j = 0; j < fieldlen; j++) { - if (!fields[j].isNull || !options.get('tooltipSkipNull')) { - $.extend(fields[j], { - prefix: options.get('tooltipPrefix'), - suffix: options.get('tooltipSuffix') - }); - text = format.render(fields[j], options.get('tooltipValueLookups'), options); - entries.push('
' + text + '
'); - } - } - } - if (entries.length) { - return header + entries.join('\n'); - } - return ''; - }, - - getCurrentRegionFields: function () {}, - - calcHighlightColor: function (color, options) { - var highlightColor = options.get('highlightColor'), - lighten = options.get('highlightLighten'), - parse, mult, rgbnew, i; - if (highlightColor) { - return highlightColor; - } - if (lighten) { - // extract RGB values - parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color); - if (parse) { - rgbnew = []; - mult = color.length === 4 ? 16 : 1; - for (i = 0; i < 3; i++) { - rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255); - } - return 'rgb(' + rgbnew.join(',') + ')'; - } - - } - return color; - } - - }); - - barHighlightMixin = { - changeHighlight: function (highlight) { - var currentRegion = this.currentRegion, - target = this.target, - shapeids = this.regionShapes[currentRegion], - newShapes; - // will be null if the region value was null - if (shapeids) { - newShapes = this.renderRegion(currentRegion, highlight); - if ($.isArray(newShapes) || $.isArray(shapeids)) { - target.replaceWithShapes(shapeids, newShapes); - this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) { - return newShape.id; - }); - } else { - target.replaceWithShape(shapeids, newShapes); - this.regionShapes[currentRegion] = newShapes.id; - } - } - }, - - render: function () { - var values = this.values, - target = this.target, - regionShapes = this.regionShapes, - shapes, ids, i, j; - - if (!this.cls._super.render.call(this)) { - return; - } - for (i = values.length; i--;) { - shapes = this.renderRegion(i); - if (shapes) { - if ($.isArray(shapes)) { - ids = []; - for (j = shapes.length; j--;) { - shapes[j].append(); - ids.push(shapes[j].id); - } - regionShapes[i] = ids; - } else { - shapes.append(); - regionShapes[i] = shapes.id; // store just the shapeid - } - } else { - // null value - regionShapes[i] = null; - } - } - target.render(); - } - }; - - /** - * Line charts - */ - $.fn.sparkline.line = line = createClass($.fn.sparkline._base, { - type: 'line', - - init: function (el, values, options, width, height) { - line._super.init.call(this, el, values, options, width, height); - this.vertices = []; - this.regionMap = []; - this.xvalues = []; - this.yvalues = []; - this.yminmax = []; - this.hightlightSpotId = null; - this.lastShapeId = null; - this.initTarget(); - }, - - getRegion: function (el, x, y) { - var i, - regionMap = this.regionMap; // maps regions to value positions - for (i = regionMap.length; i--;) { - if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) { - return regionMap[i][2]; - } - } - return undefined; - }, - - getCurrentRegionFields: function () { - var currentRegion = this.currentRegion; - return { - isNull: this.yvalues[currentRegion] === null, - x: this.xvalues[currentRegion], - y: this.yvalues[currentRegion], - color: this.options.get('lineColor'), - fillColor: this.options.get('fillColor'), - offset: currentRegion - }; - }, - - renderHighlight: function () { - var currentRegion = this.currentRegion, - target = this.target, - vertex = this.vertices[currentRegion], - options = this.options, - spotRadius = options.get('spotRadius'), - highlightSpotColor = options.get('highlightSpotColor'), - highlightLineColor = options.get('highlightLineColor'), - highlightSpot, highlightLine; - - if (!vertex) { - return; - } - if (spotRadius && highlightSpotColor) { - highlightSpot = target.drawCircle(vertex[0], vertex[1], - spotRadius, undefined, highlightSpotColor); - this.highlightSpotId = highlightSpot.id; - target.insertAfterShape(this.lastShapeId, highlightSpot); - } - if (highlightLineColor) { - highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0], - this.canvasTop + this.canvasHeight, highlightLineColor); - this.highlightLineId = highlightLine.id; - target.insertAfterShape(this.lastShapeId, highlightLine); - } - }, - - removeHighlight: function () { - var target = this.target; - if (this.highlightSpotId) { - target.removeShapeId(this.highlightSpotId); - this.highlightSpotId = null; - } - if (this.highlightLineId) { - target.removeShapeId(this.highlightLineId); - this.highlightLineId = null; - } - }, - - scanValues: function () { - var values = this.values, - valcount = values.length, - xvalues = this.xvalues, - yvalues = this.yvalues, - yminmax = this.yminmax, - i, val, isStr, isArray, sp; - for (i = 0; i < valcount; i++) { - val = values[i]; - isStr = typeof(values[i]) === 'string'; - isArray = typeof(values[i]) === 'object' && values[i] instanceof Array; - sp = isStr && values[i].split(':'); - if (isStr && sp.length === 2) { // x:y - xvalues.push(Number(sp[0])); - yvalues.push(Number(sp[1])); - yminmax.push(Number(sp[1])); - } else if (isArray) { - xvalues.push(val[0]); - yvalues.push(val[1]); - yminmax.push(val[1]); - } else { - xvalues.push(i); - if (values[i] === null || values[i] === 'null') { - yvalues.push(null); - } else { - yvalues.push(Number(val)); - yminmax.push(Number(val)); - } - } - } - if (this.options.get('xvalues')) { - xvalues = this.options.get('xvalues'); - } - - this.maxy = this.maxyorg = Math.max.apply(Math, yminmax); - this.miny = this.minyorg = Math.min.apply(Math, yminmax); - - this.maxx = Math.max.apply(Math, xvalues); - this.minx = Math.min.apply(Math, xvalues); - - this.xvalues = xvalues; - this.yvalues = yvalues; - this.yminmax = yminmax; - - }, - - processRangeOptions: function () { - var options = this.options, - normalRangeMin = options.get('normalRangeMin'), - normalRangeMax = options.get('normalRangeMax'); - - if (normalRangeMin !== undefined) { - if (normalRangeMin < this.miny) { - this.miny = normalRangeMin; - } - if (normalRangeMax > this.maxy) { - this.maxy = normalRangeMax; - } - } - if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) { - this.miny = options.get('chartRangeMin'); - } - if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) { - this.maxy = options.get('chartRangeMax'); - } - if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) { - this.minx = options.get('chartRangeMinX'); - } - if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) { - this.maxx = options.get('chartRangeMaxX'); - } - - }, - - drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) { - var normalRangeMin = this.options.get('normalRangeMin'), - normalRangeMax = this.options.get('normalRangeMax'), - ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))), - height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey); - this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append(); - }, - - render: function () { - var options = this.options, - target = this.target, - canvasWidth = this.canvasWidth, - canvasHeight = this.canvasHeight, - vertices = this.vertices, - spotRadius = options.get('spotRadius'), - regionMap = this.regionMap, - rangex, rangey, yvallast, - canvasTop, canvasLeft, - vertex, path, paths, x, y, xnext, xpos, xposnext, - last, next, yvalcount, lineShapes, fillShapes, plen, - valueSpots, hlSpotsEnabled, color, xvalues, yvalues, i; - - if (!line._super.render.call(this)) { - return; - } - - this.scanValues(); - this.processRangeOptions(); - - xvalues = this.xvalues; - yvalues = this.yvalues; - - if (!this.yminmax.length || this.yvalues.length < 2) { - // empty or all null valuess - return; - } - - canvasTop = canvasLeft = 0; - - rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx; - rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny; - yvallast = this.yvalues.length - 1; - - if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) { - spotRadius = 0; - } - if (spotRadius) { - // adjust the canvas size as required so that spots will fit - hlSpotsEnabled = options.get('highlightSpotColor') && !options.get('disableInteraction'); - if (hlSpotsEnabled || options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) { - canvasHeight -= Math.ceil(spotRadius); - } - if (hlSpotsEnabled || options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) { - canvasHeight -= Math.ceil(spotRadius); - canvasTop += Math.ceil(spotRadius); - } - if (hlSpotsEnabled || - ((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy))) { - canvasLeft += Math.ceil(spotRadius); - canvasWidth -= Math.ceil(spotRadius); - } - if (hlSpotsEnabled || options.get('spotColor') || - (options.get('minSpotColor') || options.get('maxSpotColor') && - (yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) { - canvasWidth -= Math.ceil(spotRadius); - } - } - - - canvasHeight--; - - if (options.get('normalRangeMin') !== undefined && !options.get('drawNormalOnTop')) { - this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey); - } - - path = []; - paths = [path]; - last = next = null; - yvalcount = yvalues.length; - for (i = 0; i < yvalcount; i++) { - x = xvalues[i]; - xnext = xvalues[i + 1]; - y = yvalues[i]; - xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)); - xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth; - next = xpos + ((xposnext - xpos) / 2); - regionMap[i] = [last || 0, next, i]; - last = next; - if (y === null) { - if (i) { - if (yvalues[i - 1] !== null) { - path = []; - paths.push(path); - } - vertices.push(null); - } - } else { - if (y < this.miny) { - y = this.miny; - } - if (y > this.maxy) { - y = this.maxy; - } - if (!path.length) { - // previous value was null - path.push([xpos, canvasTop + canvasHeight]); - } - vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))]; - path.push(vertex); - vertices.push(vertex); - } - } - - lineShapes = []; - fillShapes = []; - plen = paths.length; - for (i = 0; i < plen; i++) { - path = paths[i]; - if (path.length) { - if (options.get('fillColor')) { - path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]); - fillShapes.push(path.slice(0)); - path.pop(); - } - // if there's only a single point in this path, then we want to display it - // as a vertical line which means we keep path[0] as is - if (path.length > 2) { - // else we want the first value - path[0] = [path[0][0], path[1][1]]; - } - lineShapes.push(path); - } - } - - // draw the fill first, then optionally the normal range, then the line on top of that - plen = fillShapes.length; - for (i = 0; i < plen; i++) { - target.drawShape(fillShapes[i], - options.get('fillColor'), options.get('fillColor')).append(); - } - - if (options.get('normalRangeMin') !== undefined && options.get('drawNormalOnTop')) { - this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey); - } - - plen = lineShapes.length; - for (i = 0; i < plen; i++) { - target.drawShape(lineShapes[i], options.get('lineColor'), undefined, - options.get('lineWidth')).append(); - } - - if (spotRadius && options.get('valueSpots')) { - valueSpots = options.get('valueSpots'); - if (valueSpots.get === undefined) { - valueSpots = new RangeMap(valueSpots); - } - for (i = 0; i < yvalcount; i++) { - color = valueSpots.get(yvalues[i]); - if (color) { - target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)), - canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))), - spotRadius, undefined, - color).append(); - } - } - - } - if (spotRadius && options.get('spotColor') && yvalues[yvallast] !== null) { - target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)), - canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))), - spotRadius, undefined, - options.get('spotColor')).append(); - } - if (this.maxy !== this.minyorg) { - if (spotRadius && options.get('minSpotColor')) { - x = xvalues[$.inArray(this.minyorg, yvalues)]; - target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)), - canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))), - spotRadius, undefined, - options.get('minSpotColor')).append(); - } - if (spotRadius && options.get('maxSpotColor')) { - x = xvalues[$.inArray(this.maxyorg, yvalues)]; - target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)), - canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))), - spotRadius, undefined, - options.get('maxSpotColor')).append(); - } - } - - this.lastShapeId = target.getLastShapeId(); - this.canvasTop = canvasTop; - target.render(); - } - }); - - /** - * Bar charts - */ - $.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, { - type: 'bar', - - init: function (el, values, options, width, height) { - var barWidth = parseInt(options.get('barWidth'), 10), - barSpacing = parseInt(options.get('barSpacing'), 10), - chartRangeMin = options.get('chartRangeMin'), - chartRangeMax = options.get('chartRangeMax'), - chartRangeClip = options.get('chartRangeClip'), - stackMin = Infinity, - stackMax = -Infinity, - isStackString, groupMin, groupMax, stackRanges, - numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax, - stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf; - bar._super.init.call(this, el, values, options, width, height); - - // scan values to determine whether to stack bars - for (i = 0, vlen = values.length; i < vlen; i++) { - val = values[i]; - isStackString = typeof(val) === 'string' && val.indexOf(':') > -1; - if (isStackString || $.isArray(val)) { - stacked = true; - if (isStackString) { - val = values[i] = normalizeValues(val.split(':')); - } - val = remove(val, null); // min/max will treat null as zero - groupMin = Math.min.apply(Math, val); - groupMax = Math.max.apply(Math, val); - if (groupMin < stackMin) { - stackMin = groupMin; - } - if (groupMax > stackMax) { - stackMax = groupMax; - } - } - } - - this.stacked = stacked; - this.regionShapes = {}; - this.barWidth = barWidth; - this.barSpacing = barSpacing; - this.totalBarWidth = barWidth + barSpacing; - this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing); - - this.initTarget(); - - if (chartRangeClip) { - clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin; - clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax; - } - - numValues = []; - stackRanges = stacked ? [] : numValues; - var stackTotals = []; - var stackRangesNeg = []; - for (i = 0, vlen = values.length; i < vlen; i++) { - if (stacked) { - vlist = values[i]; - values[i] = svals = []; - stackTotals[i] = 0; - stackRanges[i] = stackRangesNeg[i] = 0; - for (j = 0, slen = vlist.length; j < slen; j++) { - val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j]; - if (val !== null) { - if (val > 0) { - stackTotals[i] += val; - } - if (stackMin < 0 && stackMax > 0) { - if (val < 0) { - stackRangesNeg[i] += Math.abs(val); - } else { - stackRanges[i] += val; - } - } else { - stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin)); - } - numValues.push(val); - } - } - } else { - val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i]; - val = values[i] = normalizeValue(val); - if (val !== null) { - numValues.push(val); - } - } - } - this.max = max = Math.max.apply(Math, numValues); - this.min = min = Math.min.apply(Math, numValues); - this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max; - this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min; - - if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) { - min = options.get('chartRangeMin'); - } - if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) { - max = options.get('chartRangeMax'); - } - - this.zeroAxis = zeroAxis = options.get('zeroAxis', true); - if (min <= 0 && max >= 0 && zeroAxis) { - xaxisOffset = 0; - } else if (zeroAxis == false) { - xaxisOffset = min; - } else if (min > 0) { - xaxisOffset = min; - } else { - xaxisOffset = max; - } - this.xaxisOffset = xaxisOffset; - - range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min; - - // as we plot zero/min values a single pixel line, we add a pixel to all other - // values - Reduce the effective canvas size to suit - this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1; - - if (min < xaxisOffset) { - yMaxCalc = (stacked && max >= 0) ? stackMax : max; - yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight; - if (yoffset !== Math.ceil(yoffset)) { - this.canvasHeightEf -= 2; - yoffset = Math.ceil(yoffset); - } - } else { - yoffset = this.canvasHeight; - } - this.yoffset = yoffset; - - if ($.isArray(options.get('colorMap'))) { - this.colorMapByIndex = options.get('colorMap'); - this.colorMapByValue = null; - } else { - this.colorMapByIndex = null; - this.colorMapByValue = options.get('colorMap'); - if (this.colorMapByValue && this.colorMapByValue.get === undefined) { - this.colorMapByValue = new RangeMap(this.colorMapByValue); - } - } - - this.range = range; - }, - - getRegion: function (el, x, y) { - var result = Math.floor(x / this.totalBarWidth); - return (result < 0 || result >= this.values.length) ? undefined : result; - }, - - getCurrentRegionFields: function () { - var currentRegion = this.currentRegion, - values = ensureArray(this.values[currentRegion]), - result = [], - value, i; - for (i = values.length; i--;) { - value = values[i]; - result.push({ - isNull: value === null, - value: value, - color: this.calcColor(i, value, currentRegion), - offset: currentRegion - }); - } - return result; - }, - - calcColor: function (stacknum, value, valuenum) { - var colorMapByIndex = this.colorMapByIndex, - colorMapByValue = this.colorMapByValue, - options = this.options, - color, newColor; - if (this.stacked) { - color = options.get('stackedBarColor'); - } else { - color = (value < 0) ? options.get('negBarColor') : options.get('barColor'); - } - if (value === 0 && options.get('zeroColor') !== undefined) { - color = options.get('zeroColor'); - } - if (colorMapByValue && (newColor = colorMapByValue.get(value))) { - color = newColor; - } else if (colorMapByIndex && colorMapByIndex.length > valuenum) { - color = colorMapByIndex[valuenum]; - } - return $.isArray(color) ? color[stacknum % color.length] : color; - }, - - /** - * Render bar(s) for a region - */ - renderRegion: function (valuenum, highlight) { - var vals = this.values[valuenum], - options = this.options, - xaxisOffset = this.xaxisOffset, - result = [], - range = this.range, - stacked = this.stacked, - target = this.target, - x = valuenum * this.totalBarWidth, - canvasHeightEf = this.canvasHeightEf, - yoffset = this.yoffset, - y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin; - - vals = $.isArray(vals) ? vals : [vals]; - valcount = vals.length; - val = vals[0]; - isNull = all(null, vals); - allMin = all(xaxisOffset, vals, true); - - if (isNull) { - if (options.get('nullColor')) { - color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options); - y = (yoffset > 0) ? yoffset - 1 : yoffset; - return target.drawRect(x, y, this.barWidth - 1, 0, color, color); - } else { - return undefined; - } - } - yoffsetNeg = yoffset; - for (i = 0; i < valcount; i++) { - val = vals[i]; - - if (stacked && val === xaxisOffset) { - if (!allMin || minPlotted) { - continue; - } - minPlotted = true; - } - - if (range > 0) { - height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1; - } else { - height = 1; - } - if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) { - y = yoffsetNeg; - yoffsetNeg += height; - } else { - y = yoffset - height; - yoffset -= height; - } - color = this.calcColor(i, val, valuenum); - if (highlight) { - color = this.calcHighlightColor(color, options); - } - result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color)); - } - if (result.length === 1) { - return result[0]; - } - return result; - } - }); - - /** - * Tristate charts - */ - $.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, { - type: 'tristate', - - init: function (el, values, options, width, height) { - var barWidth = parseInt(options.get('barWidth'), 10), - barSpacing = parseInt(options.get('barSpacing'), 10); - tristate._super.init.call(this, el, values, options, width, height); - - this.regionShapes = {}; - this.barWidth = barWidth; - this.barSpacing = barSpacing; - this.totalBarWidth = barWidth + barSpacing; - this.values = $.map(values, Number); - this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing); - - if ($.isArray(options.get('colorMap'))) { - this.colorMapByIndex = options.get('colorMap'); - this.colorMapByValue = null; - } else { - this.colorMapByIndex = null; - this.colorMapByValue = options.get('colorMap'); - if (this.colorMapByValue && this.colorMapByValue.get === undefined) { - this.colorMapByValue = new RangeMap(this.colorMapByValue); - } - } - this.initTarget(); - }, - - getRegion: function (el, x, y) { - return Math.floor(x / this.totalBarWidth); - }, - - getCurrentRegionFields: function () { - var currentRegion = this.currentRegion; - return { - isNull: this.values[currentRegion] === undefined, - value: this.values[currentRegion], - color: this.calcColor(this.values[currentRegion], currentRegion), - offset: currentRegion - }; - }, - - calcColor: function (value, valuenum) { - var values = this.values, - options = this.options, - colorMapByIndex = this.colorMapByIndex, - colorMapByValue = this.colorMapByValue, - color, newColor; - - if (colorMapByValue && (newColor = colorMapByValue.get(value))) { - color = newColor; - } else if (colorMapByIndex && colorMapByIndex.length > valuenum) { - color = colorMapByIndex[valuenum]; - } else if (values[valuenum] < 0) { - color = options.get('negBarColor'); - } else if (values[valuenum] > 0) { - color = options.get('posBarColor'); - } else { - color = options.get('zeroBarColor'); - } - return color; - }, - - renderRegion: function (valuenum, highlight) { - var values = this.values, - options = this.options, - target = this.target, - canvasHeight, height, halfHeight, - x, y, color; - - canvasHeight = target.pixelHeight; - halfHeight = Math.round(canvasHeight / 2); - - x = valuenum * this.totalBarWidth; - if (values[valuenum] < 0) { - y = halfHeight; - height = halfHeight - 1; - } else if (values[valuenum] > 0) { - y = 0; - height = halfHeight - 1; - } else { - y = halfHeight - 1; - height = 2; - } - color = this.calcColor(values[valuenum], valuenum); - if (color === null) { - return; - } - if (highlight) { - color = this.calcHighlightColor(color, options); - } - return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color); - } - }); - - /** - * Discrete charts - */ - $.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, { - type: 'discrete', - - init: function (el, values, options, width, height) { - discrete._super.init.call(this, el, values, options, width, height); - - this.regionShapes = {}; - this.values = values = $.map(values, Number); - this.min = Math.min.apply(Math, values); - this.max = Math.max.apply(Math, values); - this.range = this.max - this.min; - this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width; - this.interval = Math.floor(width / values.length); - this.itemWidth = width / values.length; - if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) { - this.min = options.get('chartRangeMin'); - } - if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) { - this.max = options.get('chartRangeMax'); - } - this.initTarget(); - if (this.target) { - this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight'); - } - }, - - getRegion: function (el, x, y) { - return Math.floor(x / this.itemWidth); - }, - - getCurrentRegionFields: function () { - var currentRegion = this.currentRegion; - return { - isNull: this.values[currentRegion] === undefined, - value: this.values[currentRegion], - offset: currentRegion - }; - }, - - renderRegion: function (valuenum, highlight) { - var values = this.values, - options = this.options, - min = this.min, - max = this.max, - range = this.range, - interval = this.interval, - target = this.target, - canvasHeight = this.canvasHeight, - lineHeight = this.lineHeight, - pheight = canvasHeight - lineHeight, - ytop, val, color, x; - - val = clipval(values[valuenum], min, max); - x = valuenum * interval; - ytop = Math.round(pheight - pheight * ((val - min) / range)); - color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor'); - if (highlight) { - color = this.calcHighlightColor(color, options); - } - return target.drawLine(x, ytop, x, ytop + lineHeight, color); - } - }); - - /** - * Bullet charts - */ - $.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, { - type: 'bullet', - - init: function (el, values, options, width, height) { - var min, max, vals; - bullet._super.init.call(this, el, values, options, width, height); - - // values: target, performance, range1, range2, range3 - this.values = values = normalizeValues(values); - // target or performance could be null - vals = values.slice(); - vals[0] = vals[0] === null ? vals[2] : vals[0]; - vals[1] = values[1] === null ? vals[2] : vals[1]; - min = Math.min.apply(Math, values); - max = Math.max.apply(Math, values); - if (options.get('base') === undefined) { - min = min < 0 ? min : 0; - } else { - min = options.get('base'); - } - this.min = min; - this.max = max; - this.range = max - min; - this.shapes = {}; - this.valueShapes = {}; - this.regiondata = {}; - this.width = width = options.get('width') === 'auto' ? '4.0em' : width; - this.target = this.$el.simpledraw(width, height, options.get('composite')); - if (!values.length) { - this.disabled = true; - } - this.initTarget(); - }, - - getRegion: function (el, x, y) { - var shapeid = this.target.getShapeAt(el, x, y); - return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined; - }, - - getCurrentRegionFields: function () { - var currentRegion = this.currentRegion; - return { - fieldkey: currentRegion.substr(0, 1), - value: this.values[currentRegion.substr(1)], - region: currentRegion - }; - }, - - changeHighlight: function (highlight) { - var currentRegion = this.currentRegion, - shapeid = this.valueShapes[currentRegion], - shape; - delete this.shapes[shapeid]; - switch (currentRegion.substr(0, 1)) { - case 'r': - shape = this.renderRange(currentRegion.substr(1), highlight); - break; - case 'p': - shape = this.renderPerformance(highlight); - break; - case 't': - shape = this.renderTarget(highlight); - break; - } - this.valueShapes[currentRegion] = shape.id; - this.shapes[shape.id] = currentRegion; - this.target.replaceWithShape(shapeid, shape); - }, - - renderRange: function (rn, highlight) { - var rangeval = this.values[rn], - rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)), - color = this.options.get('rangeColors')[rn - 2]; - if (highlight) { - color = this.calcHighlightColor(color, this.options); - } - return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color); - }, - - renderPerformance: function (highlight) { - var perfval = this.values[1], - perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)), - color = this.options.get('performanceColor'); - if (highlight) { - color = this.calcHighlightColor(color, this.options); - } - return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1, - Math.round(this.canvasHeight * 0.4) - 1, color, color); - }, - - renderTarget: function (highlight) { - var targetval = this.values[0], - x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)), - targettop = Math.round(this.canvasHeight * 0.10), - targetheight = this.canvasHeight - (targettop * 2), - color = this.options.get('targetColor'); - if (highlight) { - color = this.calcHighlightColor(color, this.options); - } - return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color); - }, - - render: function () { - var vlen = this.values.length, - target = this.target, - i, shape; - if (!bullet._super.render.call(this)) { - return; - } - for (i = 2; i < vlen; i++) { - shape = this.renderRange(i).append(); - this.shapes[shape.id] = 'r' + i; - this.valueShapes['r' + i] = shape.id; - } - if (this.values[1] !== null) { - shape = this.renderPerformance().append(); - this.shapes[shape.id] = 'p1'; - this.valueShapes.p1 = shape.id; - } - if (this.values[0] !== null) { - shape = this.renderTarget().append(); - this.shapes[shape.id] = 't0'; - this.valueShapes.t0 = shape.id; - } - target.render(); - } - }); - - /** - * Pie charts - */ - $.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, { - type: 'pie', - - init: function (el, values, options, width, height) { - var total = 0, i; - - pie._super.init.call(this, el, values, options, width, height); - - this.shapes = {}; // map shape ids to value offsets - this.valueShapes = {}; // maps value offsets to shape ids - this.values = values = $.map(values, Number); - - if (options.get('width') === 'auto') { - this.width = this.height; - } - - if (values.length > 0) { - for (i = values.length; i--;) { - total += values[i]; - } - } - this.total = total; - this.initTarget(); - this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2); - }, - - getRegion: function (el, x, y) { - var shapeid = this.target.getShapeAt(el, x, y); - return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined; - }, - - getCurrentRegionFields: function () { - var currentRegion = this.currentRegion; - return { - isNull: this.values[currentRegion] === undefined, - value: this.values[currentRegion], - percent: this.values[currentRegion] / this.total * 100, - color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length], - offset: currentRegion - }; - }, - - changeHighlight: function (highlight) { - var currentRegion = this.currentRegion, - newslice = this.renderSlice(currentRegion, highlight), - shapeid = this.valueShapes[currentRegion]; - delete this.shapes[shapeid]; - this.target.replaceWithShape(shapeid, newslice); - this.valueShapes[currentRegion] = newslice.id; - this.shapes[newslice.id] = currentRegion; - }, - - renderSlice: function (valuenum, highlight) { - var target = this.target, - options = this.options, - radius = this.radius, - borderWidth = options.get('borderWidth'), - offset = options.get('offset'), - circle = 2 * Math.PI, - values = this.values, - total = this.total, - next = offset ? (2*Math.PI)*(offset/360) : 0, - start, end, i, vlen, color; - - vlen = values.length; - for (i = 0; i < vlen; i++) { - start = next; - end = next; - if (total > 0) { // avoid divide by zero - end = next + (circle * (values[i] / total)); - } - if (valuenum === i) { - color = options.get('sliceColors')[i % options.get('sliceColors').length]; - if (highlight) { - color = this.calcHighlightColor(color, options); - } - - return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color); - } - next = end; - } - }, - - render: function () { - var target = this.target, - values = this.values, - options = this.options, - radius = this.radius, - borderWidth = options.get('borderWidth'), - shape, i; - - if (!pie._super.render.call(this)) { - return; - } - if (borderWidth) { - target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)), - options.get('borderColor'), undefined, borderWidth).append(); - } - for (i = values.length; i--;) { - if (values[i]) { // don't render zero values - shape = this.renderSlice(i).append(); - this.valueShapes[i] = shape.id; // store just the shapeid - this.shapes[shape.id] = i; - } - } - target.render(); - } - }); - - /** - * Box plots - */ - $.fn.sparkline.box = box = createClass($.fn.sparkline._base, { - type: 'box', - - init: function (el, values, options, width, height) { - box._super.init.call(this, el, values, options, width, height); - this.values = $.map(values, Number); - this.width = options.get('width') === 'auto' ? '4.0em' : width; - this.initTarget(); - if (!this.values.length) { - this.disabled = 1; - } - }, - - /** - * Simulate a single region - */ - getRegion: function () { - return 1; - }, - - getCurrentRegionFields: function () { - var result = [ - { field: 'lq', value: this.quartiles[0] }, - { field: 'med', value: this.quartiles[1] }, - { field: 'uq', value: this.quartiles[2] } - ]; - if (this.loutlier !== undefined) { - result.push({ field: 'lo', value: this.loutlier}); - } - if (this.routlier !== undefined) { - result.push({ field: 'ro', value: this.routlier}); - } - if (this.lwhisker !== undefined) { - result.push({ field: 'lw', value: this.lwhisker}); - } - if (this.rwhisker !== undefined) { - result.push({ field: 'rw', value: this.rwhisker}); - } - return result; - }, - - render: function () { - var target = this.target, - values = this.values, - vlen = values.length, - options = this.options, - canvasWidth = this.canvasWidth, - canvasHeight = this.canvasHeight, - minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'), - maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'), - canvasLeft = 0, - lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i, - size, unitSize; - - if (!box._super.render.call(this)) { - return; - } - - if (options.get('raw')) { - if (options.get('showOutliers') && values.length > 5) { - loutlier = values[0]; - lwhisker = values[1]; - q1 = values[2]; - q2 = values[3]; - q3 = values[4]; - rwhisker = values[5]; - routlier = values[6]; - } else { - lwhisker = values[0]; - q1 = values[1]; - q2 = values[2]; - q3 = values[3]; - rwhisker = values[4]; - } - } else { - values.sort(function (a, b) { return a - b; }); - q1 = quartile(values, 1); - q2 = quartile(values, 2); - q3 = quartile(values, 3); - iqr = q3 - q1; - if (options.get('showOutliers')) { - lwhisker = rwhisker = undefined; - for (i = 0; i < vlen; i++) { - if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) { - lwhisker = values[i]; - } - if (values[i] < q3 + (iqr * options.get('outlierIQR'))) { - rwhisker = values[i]; - } - } - loutlier = values[0]; - routlier = values[vlen - 1]; - } else { - lwhisker = values[0]; - rwhisker = values[vlen - 1]; - } - } - this.quartiles = [q1, q2, q3]; - this.lwhisker = lwhisker; - this.rwhisker = rwhisker; - this.loutlier = loutlier; - this.routlier = routlier; - - unitSize = canvasWidth / (maxValue - minValue + 1); - if (options.get('showOutliers')) { - canvasLeft = Math.ceil(options.get('spotRadius')); - canvasWidth -= 2 * Math.ceil(options.get('spotRadius')); - unitSize = canvasWidth / (maxValue - minValue + 1); - if (loutlier < lwhisker) { - target.drawCircle((loutlier - minValue) * unitSize + canvasLeft, - canvasHeight / 2, - options.get('spotRadius'), - options.get('outlierLineColor'), - options.get('outlierFillColor')).append(); - } - if (routlier > rwhisker) { - target.drawCircle((routlier - minValue) * unitSize + canvasLeft, - canvasHeight / 2, - options.get('spotRadius'), - options.get('outlierLineColor'), - options.get('outlierFillColor')).append(); - } - } - - // box - target.drawRect( - Math.round((q1 - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight * 0.1), - Math.round((q3 - q1) * unitSize), - Math.round(canvasHeight * 0.8), - options.get('boxLineColor'), - options.get('boxFillColor')).append(); - // left whisker - target.drawLine( - Math.round((lwhisker - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight / 2), - Math.round((q1 - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight / 2), - options.get('lineColor')).append(); - target.drawLine( - Math.round((lwhisker - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight / 4), - Math.round((lwhisker - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight - canvasHeight / 4), - options.get('whiskerColor')).append(); - // right whisker - target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight / 2), - Math.round((q3 - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight / 2), - options.get('lineColor')).append(); - target.drawLine( - Math.round((rwhisker - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight / 4), - Math.round((rwhisker - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight - canvasHeight / 4), - options.get('whiskerColor')).append(); - // median line - target.drawLine( - Math.round((q2 - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight * 0.1), - Math.round((q2 - minValue) * unitSize + canvasLeft), - Math.round(canvasHeight * 0.9), - options.get('medianColor')).append(); - if (options.get('target')) { - size = Math.ceil(options.get('spotRadius')); - target.drawLine( - Math.round((options.get('target') - minValue) * unitSize + canvasLeft), - Math.round((canvasHeight / 2) - size), - Math.round((options.get('target') - minValue) * unitSize + canvasLeft), - Math.round((canvasHeight / 2) + size), - options.get('targetColor')).append(); - target.drawLine( - Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size), - Math.round(canvasHeight / 2), - Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size), - Math.round(canvasHeight / 2), - options.get('targetColor')).append(); - } - target.render(); - } - }); - - // Setup a very simple "virtual canvas" to make drawing the few shapes we need easier - // This is accessible as $(foo).simpledraw() - - // Detect browser renderer support - (function() { - if (document.namespaces && !document.namespaces.v) { - $.fn.sparkline.hasVML = true; - document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML'); - } else { - $.fn.sparkline.hasVML = false; - } - - var el = document.createElement('canvas'); - $.fn.sparkline.hasCanvas = !!(el.getContext && el.getContext('2d')); - - })() - - VShape = createClass({ - init: function (target, id, type, args) { - this.target = target; - this.id = id; - this.type = type; - this.args = args; - }, - append: function () { - this.target.appendShape(this); - return this; - } - }); - - VCanvas_base = createClass({ - _pxregex: /(\d+)(px)?\s*$/i, - - init: function (width, height, target) { - if (!width) { - return; - } - this.width = width; - this.height = height; - this.target = target; - this.lastShapeId = null; - if (target[0]) { - target = target[0]; - } - $.data(target, '_jqs_vcanvas', this); - }, - - drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) { - return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth); - }, - - drawShape: function (path, lineColor, fillColor, lineWidth) { - return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]); - }, - - drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) { - return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]); - }, - - drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) { - return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]); - }, - - drawRect: function (x, y, width, height, lineColor, fillColor) { - return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]); - }, - - getElement: function () { - return this.canvas; - }, - - /** - * Return the most recently inserted shape id - */ - getLastShapeId: function () { - return this.lastShapeId; - }, - - /** - * Clear and reset the canvas - */ - reset: function () { - alert('reset not implemented'); - }, - - _insert: function (el, target) { - $(target).html(el); - }, - - /** - * Calculate the pixel dimensions of the canvas - */ - _calculatePixelDims: function (width, height, canvas) { - // XXX This should probably be a configurable option - var match; - match = this._pxregex.exec(height); - if (match) { - this.pixelHeight = match[1]; - } else { - this.pixelHeight = $(canvas).height(); - } - match = this._pxregex.exec(width); - if (match) { - this.pixelWidth = match[1]; - } else { - this.pixelWidth = $(canvas).width(); - } - }, - - /** - * Generate a shape object and id for later rendering - */ - _genShape: function (shapetype, shapeargs) { - var id = shapeCount++; - shapeargs.unshift(id); - return new VShape(this, id, shapetype, shapeargs); - }, - - /** - * Add a shape to the end of the render queue - */ - appendShape: function (shape) { - alert('appendShape not implemented'); - }, - - /** - * Replace one shape with another - */ - replaceWithShape: function (shapeid, shape) { - alert('replaceWithShape not implemented'); - }, - - /** - * Insert one shape after another in the render queue - */ - insertAfterShape: function (shapeid, shape) { - alert('insertAfterShape not implemented'); - }, - - /** - * Remove a shape from the queue - */ - removeShapeId: function (shapeid) { - alert('removeShapeId not implemented'); - }, - - /** - * Find a shape at the specified x/y co-ordinates - */ - getShapeAt: function (el, x, y) { - alert('getShapeAt not implemented'); - }, - - /** - * Render all queued shapes onto the canvas - */ - render: function () { - alert('render not implemented'); - } - }); - - VCanvas_canvas = createClass(VCanvas_base, { - init: function (width, height, target, interact) { - VCanvas_canvas._super.init.call(this, width, height, target); - this.canvas = document.createElement('canvas'); - if (target[0]) { - target = target[0]; - } - $.data(target, '_jqs_vcanvas', this); - $(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' }); - this._insert(this.canvas, target); - this._calculatePixelDims(width, height, this.canvas); - this.canvas.width = this.pixelWidth; - this.canvas.height = this.pixelHeight; - this.interact = interact; - this.shapes = {}; - this.shapeseq = []; - this.currentTargetShapeId = undefined; - $(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight}); - }, - - _getContext: function (lineColor, fillColor, lineWidth) { - var context = this.canvas.getContext('2d'); - if (lineColor !== undefined) { - context.strokeStyle = lineColor; - } - context.lineWidth = lineWidth === undefined ? 1 : lineWidth; - if (fillColor !== undefined) { - context.fillStyle = fillColor; - } - return context; - }, - - reset: function () { - var context = this._getContext(); - context.clearRect(0, 0, this.pixelWidth, this.pixelHeight); - this.shapes = {}; - this.shapeseq = []; - this.currentTargetShapeId = undefined; - }, - - _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) { - var context = this._getContext(lineColor, fillColor, lineWidth), - i, plen; - context.beginPath(); - context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5); - for (i = 1, plen = path.length; i < plen; i++) { - context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines - } - if (lineColor !== undefined) { - context.stroke(); - } - if (fillColor !== undefined) { - context.fill(); - } - if (this.targetX !== undefined && this.targetY !== undefined && - context.isPointInPath(this.targetX, this.targetY)) { - this.currentTargetShapeId = shapeid; - } - }, - - _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) { - var context = this._getContext(lineColor, fillColor, lineWidth); - context.beginPath(); - context.arc(x, y, radius, 0, 2 * Math.PI, false); - if (this.targetX !== undefined && this.targetY !== undefined && - context.isPointInPath(this.targetX, this.targetY)) { - this.currentTargetShapeId = shapeid; - } - if (lineColor !== undefined) { - context.stroke(); - } - if (fillColor !== undefined) { - context.fill(); - } - }, - - _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) { - var context = this._getContext(lineColor, fillColor); - context.beginPath(); - context.moveTo(x, y); - context.arc(x, y, radius, startAngle, endAngle, false); - context.lineTo(x, y); - context.closePath(); - if (lineColor !== undefined) { - context.stroke(); - } - if (fillColor) { - context.fill(); - } - if (this.targetX !== undefined && this.targetY !== undefined && - context.isPointInPath(this.targetX, this.targetY)) { - this.currentTargetShapeId = shapeid; - } - }, - - _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) { - return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor); - }, - - appendShape: function (shape) { - this.shapes[shape.id] = shape; - this.shapeseq.push(shape.id); - this.lastShapeId = shape.id; - return shape.id; - }, - - replaceWithShape: function (shapeid, shape) { - var shapeseq = this.shapeseq, - i; - this.shapes[shape.id] = shape; - for (i = shapeseq.length; i--;) { - if (shapeseq[i] == shapeid) { - shapeseq[i] = shape.id; - } - } - delete this.shapes[shapeid]; - }, - - replaceWithShapes: function (shapeids, shapes) { - var shapeseq = this.shapeseq, - shapemap = {}, - sid, i, first; - - for (i = shapeids.length; i--;) { - shapemap[shapeids[i]] = true; - } - for (i = shapeseq.length; i--;) { - sid = shapeseq[i]; - if (shapemap[sid]) { - shapeseq.splice(i, 1); - delete this.shapes[sid]; - first = i; - } - } - for (i = shapes.length; i--;) { - shapeseq.splice(first, 0, shapes[i].id); - this.shapes[shapes[i].id] = shapes[i]; - } - - }, - - insertAfterShape: function (shapeid, shape) { - var shapeseq = this.shapeseq, - i; - for (i = shapeseq.length; i--;) { - if (shapeseq[i] === shapeid) { - shapeseq.splice(i + 1, 0, shape.id); - this.shapes[shape.id] = shape; - return; - } - } - }, - - removeShapeId: function (shapeid) { - var shapeseq = this.shapeseq, - i; - for (i = shapeseq.length; i--;) { - if (shapeseq[i] === shapeid) { - shapeseq.splice(i, 1); - break; - } - } - delete this.shapes[shapeid]; - }, - - getShapeAt: function (el, x, y) { - this.targetX = x; - this.targetY = y; - this.render(); - return this.currentTargetShapeId; - }, - - render: function () { - var shapeseq = this.shapeseq, - shapes = this.shapes, - shapeCount = shapeseq.length, - context = this._getContext(), - shapeid, shape, i; - context.clearRect(0, 0, this.pixelWidth, this.pixelHeight); - for (i = 0; i < shapeCount; i++) { - shapeid = shapeseq[i]; - shape = shapes[shapeid]; - this['_draw' + shape.type].apply(this, shape.args); - } - if (!this.interact) { - // not interactive so no need to keep the shapes array - this.shapes = {}; - this.shapeseq = []; - } - } - - }); - - VCanvas_vml = createClass(VCanvas_base, { - init: function (width, height, target) { - var groupel; - VCanvas_vml._super.init.call(this, width, height, target); - if (target[0]) { - target = target[0]; - } - $.data(target, '_jqs_vcanvas', this); - this.canvas = document.createElement('span'); - $(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'}); - this._insert(this.canvas, target); - this._calculatePixelDims(width, height, this.canvas); - this.canvas.width = this.pixelWidth; - this.canvas.height = this.pixelHeight; - groupel = ''; - this.canvas.insertAdjacentHTML('beforeEnd', groupel); - this.group = $(this.canvas).children()[0]; - this.rendered = false; - this.prerender = ''; - }, - - _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) { - var vpath = [], - initial, stroke, fill, closed, vel, plen, i; - for (i = 0, plen = path.length; i < plen; i++) { - vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]); - } - initial = vpath.splice(0, 1); - lineWidth = lineWidth === undefined ? 1 : lineWidth; - stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" '; - fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" '; - closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : ''; - vel = '' + - ' '; - return vel; - }, - - _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) { - var stroke, fill, vel; - x -= radius; - y -= radius; - stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" '; - fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" '; - vel = ''; - return vel; - - }, - - _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) { - var vpath, startx, starty, endx, endy, stroke, fill, vel; - if (startAngle === endAngle) { - return ''; // VML seems to have problem when start angle equals end angle. - } - if ((endAngle - startAngle) === (2 * Math.PI)) { - startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0 - endAngle = (2 * Math.PI); - } - - startx = x + Math.round(Math.cos(startAngle) * radius); - starty = y + Math.round(Math.sin(startAngle) * radius); - endx = x + Math.round(Math.cos(endAngle) * radius); - endy = y + Math.round(Math.sin(endAngle) * radius); - - if (startx === endx && starty === endy) { - if ((endAngle - startAngle) < Math.PI) { - // Prevent very small slices from being mistaken as a whole pie - return ''; - } - // essentially going to be the entire circle, so ignore startAngle - startx = endx = x + radius; - starty = endy = y; - } - - if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) { - return ''; - } - - vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy]; - stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" '; - fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" '; - vel = '' + - ' '; - return vel; - }, - - _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) { - return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor); - }, - - reset: function () { - this.group.innerHTML = ''; - }, - - appendShape: function (shape) { - var vel = this['_draw' + shape.type].apply(this, shape.args); - if (this.rendered) { - this.group.insertAdjacentHTML('beforeEnd', vel); - } else { - this.prerender += vel; - } - this.lastShapeId = shape.id; - return shape.id; - }, - - replaceWithShape: function (shapeid, shape) { - var existing = $('#jqsshape' + shapeid), - vel = this['_draw' + shape.type].apply(this, shape.args); - existing[0].outerHTML = vel; - }, - - replaceWithShapes: function (shapeids, shapes) { - // replace the first shapeid with all the new shapes then toast the remaining old shapes - var existing = $('#jqsshape' + shapeids[0]), - replace = '', - slen = shapes.length, - i; - for (i = 0; i < slen; i++) { - replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args); - } - existing[0].outerHTML = replace; - for (i = 1; i < shapeids.length; i++) { - $('#jqsshape' + shapeids[i]).remove(); - } - }, - - insertAfterShape: function (shapeid, shape) { - var existing = $('#jqsshape' + shapeid), - vel = this['_draw' + shape.type].apply(this, shape.args); - existing[0].insertAdjacentHTML('afterEnd', vel); - }, - - removeShapeId: function (shapeid) { - var existing = $('#jqsshape' + shapeid); - this.group.removeChild(existing[0]); - }, - - getShapeAt: function (el, x, y) { - var shapeid = el.id.substr(8); - return shapeid; - }, - - render: function () { - if (!this.rendered) { - // batch the intial render into a single repaint - this.group.innerHTML = this.prerender; - this.rendered = true; - } - } - }); - -})); diff --git a/addons/crm/static/src/js/crm_case_section.js b/addons/crm/static/src/js/crm_case_section.js index 2b4ca741d75..deb21d38de2 100644 --- a/addons/crm/static/src/js/crm_case_section.js +++ b/addons/crm/static/src/js/crm_case_section.js @@ -9,26 +9,4 @@ openerp.crm = function(openerp) { }, }); - openerp.crm.SparklineBarWidget = openerp.web_kanban.AbstractField.extend({ - className: "oe_sparkline_bar", - start: function() { - var self = this; - var title = this.$node.html(); - setTimeout(function () { - var value = _.pluck(self.field.value, 'value'); - var tooltips = _.pluck(self.field.value, 'tooltip'); - self.$el.sparkline(value, { - type: 'bar', - barWidth: 5, - tooltipFormat: '{{offset:offset}} {{value}}', - tooltipValueLookups: { - 'offset': tooltips - }, - }); - self.$el.tipsy({'delayIn': 0, 'html': true, 'title': function(){return title}, 'gravity': 'n'}); - }, 0); - }, - }); - openerp.web_kanban.fields_registry.add("sparkline_bar", "openerp.crm.SparklineBarWidget"); - }; diff --git a/addons/crm/test/crm_access_group_users.yml b/addons/crm/test/crm_access_group_users.yml new file mode 100644 index 00000000000..51cf52c0abc --- /dev/null +++ b/addons/crm/test/crm_access_group_users.yml @@ -0,0 +1,31 @@ +- + Create a user as 'Crm Salesmanager' +- + !record {model: res.users, id: crm_res_users_salesmanager}: + company_id: base.main_company + name: Crm Sales manager + login: csm + password: csm + email: crmmanager@yourcompany.com +- + I added groups for Crm Salesmanager. +- + !record {model: res.users, id: crm_res_users_salesmanager}: + groups_id: + - base.group_sale_manager +- + Create a user as 'Crm Salesman' +- + !record {model: res.users, id: crm_res_users_salesman}: + company_id: base.main_company + name: Crm Salesman + login: csu + password: csu + email: crmuser@yourcompany.com +- + I added groups for Crm Salesman. +- + !record {model: res.users, id: crm_res_users_salesman}: + groups_id: + - base.group_sale_salesman_all_leads + - base.group_partner_manager diff --git a/addons/crm/test/crm_lead_cancel.yml b/addons/crm/test/crm_lead_cancel.yml index 127fe4d6726..dd475d54919 100644 --- a/addons/crm/test/crm_lead_cancel.yml +++ b/addons/crm/test/crm_lead_cancel.yml @@ -1,21 +1,24 @@ - - I set a new sale team (with Marketing at parent) . + I set a new sale team (with Marketing at parent) giving access rights of salesman. +- + !context + uid: 'crm_res_users_salesmanager' - !python {model: crm.lead}: | section_id = self.pool.get('crm.case.section').create(cr, uid, {'name': "Phone Marketing", 'parent_id': ref("crm.crm_case_section_2")}) self.write(cr, uid, [ref("crm_case_1")], {'section_id': section_id}) - - I check unqualified lead . + Salesman check unqualified lead . - !assert {model: crm.lead, id: crm.crm_case_1, string: Lead is in new stage}: - stage_id.sequence == 1 - - I escalate the lead to parent team. + Salesman escalate the lead to parent team. - !python {model: crm.lead}: | self.case_escalate(cr, uid, [ref("crm_case_1")]) - - I check the lead is correctly escalated to the parent team. + Salesman check the lead is correctly escalated to the parent team. - !assert {model: crm.lead, id: crm.crm_case_1, string: Escalate lead to parent team}: - section_id.name == "Marketing" diff --git a/addons/crm/test/crm_lead_merge.yml b/addons/crm/test/crm_lead_merge.yml index fb166e0486f..0659b55054c 100644 --- a/addons/crm/test/crm_lead_merge.yml +++ b/addons/crm/test/crm_lead_merge.yml @@ -1,6 +1,8 @@ - - - During a mixed merge (involving leads and opps), data should be handled a certain way following their type (m2o, m2m, text, ...) Start by creating two leads and an opp. + During a mixed merge (involving leads and opps), data should be handled a certain way following their type (m2o, m2m, text, ...) Start by creating two leads and an opp and giving the rights of Sales manager. +- + !context + uid: 'crm_res_users_salesmanager' - !record {model: crm.lead, id: test_crm_opp_1}: type: 'opportunity' diff --git a/addons/crm/test/crm_lead_message.yml b/addons/crm/test/crm_lead_message.yml index 0f7d4655398..fcfe658a8f5 100644 --- a/addons/crm/test/crm_lead_message.yml +++ b/addons/crm/test/crm_lead_message.yml @@ -1,3 +1,8 @@ +- + Give the access rights of Salesman to communicate with customer. +- + !context + uid: 'crm_res_users_salesman' - Customer interested in our product, so he sends request by email to get more details. - diff --git a/addons/crm/test/crm_lead_onchange.yml b/addons/crm/test/crm_lead_onchange.yml index d4843ec57e4..f6300d7b965 100644 --- a/addons/crm/test/crm_lead_onchange.yml +++ b/addons/crm/test/crm_lead_onchange.yml @@ -1,5 +1,8 @@ - - I create a lead record to call a partner onchange, stage onchange and mailing opt-in onchange method. + Sales manager create a lead record to call a partner onchange, stage onchange and mailing opt-in onchange method. +- + !context + uid: 'crm_res_users_salesmanager' - !record {model: crm.lead, id: crm_case_25}: name: 'Need more info about your pc2' @@ -7,7 +10,7 @@ type: opportunity stage_id: crm.stage_lead1 - - I create a lead record to call a mailing opt-out onchange method. + Sales manager create a lead record to call a mailing opt-out onchange method. - !record {model: crm.lead, id: crm_case_18}: name: 'Need 20 Days of Consultancy' @@ -15,7 +18,7 @@ stage_id: crm.stage_lead1 opt_out: True - - I create a phonecall record to call a partner onchange method. + Sales manager create a phonecall record to call a partner onchange method. - !record {model: crm.phonecall, id: crm_phonecall_5}: name: 'Bad time' diff --git a/addons/crm/test/crm_lead_unlink.yml b/addons/crm/test/crm_lead_unlink.yml index 49713039f22..be545e53773 100644 --- a/addons/crm/test/crm_lead_unlink.yml +++ b/addons/crm/test/crm_lead_unlink.yml @@ -1,5 +1,8 @@ - - I Unlink the Lead. + Only Sales manager Unlink the Lead so test with Manager's access rights'. +- + !context + uid: 'crm_res_users_salesmanager' - !python {model: crm.lead}: | self.unlink(cr, uid, [ref("crm_case_4")]) diff --git a/addons/crm/test/lead2opportunity2win.yml b/addons/crm/test/lead2opportunity2win.yml index a2499cf4aa9..0a078b9a47c 100644 --- a/addons/crm/test/lead2opportunity2win.yml +++ b/addons/crm/test/lead2opportunity2win.yml @@ -1,3 +1,8 @@ +- + Giving access rights of salesman to convert the lead into opportunity. +- + !context + uid: 'crm_res_users_salesman' - In order to test the conversion of a lead into a opportunity, - diff --git a/addons/crm/test/lead2opportunity_assign_salesmen.yml b/addons/crm/test/lead2opportunity_assign_salesmen.yml index 0edd3b8edce..5a750c94b72 100644 --- a/addons/crm/test/lead2opportunity_assign_salesmen.yml +++ b/addons/crm/test/lead2opportunity_assign_salesmen.yml @@ -1,5 +1,30 @@ - - During a lead to opp conversion, salesmen should be assigned to leads following the round-robin method. Start by creating 6 leads (1 to 6) and 4 salesmen (A to D). + During a lead to opp conversion, salesmen should be assigned to leads following the round-robin method. Start by creating 4 salesmen (A to D) and 6 leads (1 to 6). +- + !record {model: res.users, id: test_res_user_01}: + name: 'Test user A' + login: 'tua' + new_password: 'tua' +- + !record {model: res.users, id: test_res_user_02}: + name: 'Test user B' + login: 'tub' + new_password: 'tub' +- + !record {model: res.users, id: test_res_user_03}: + name: 'Test user C' + login: 'tuc' + new_password: 'tuc' +- + !record {model: res.users, id: test_res_user_04}: + name: 'Test user D' + login: 'tud' + new_password: 'tud' +- + Salesman also creates lead so giving access rights of salesman. +- + !context + uid: 'crm_res_users_salesman' - !record {model: crm.lead, id: test_crm_lead_01}: type: 'lead' @@ -37,27 +62,7 @@ partner_name: 'Agrolait SuperSeed SA' stage_id: stage_lead1 - - !record {model: res.users, id: test_res_user_01}: - name: 'Test user A' - login: 'tua' - new_password: 'tua' -- - !record {model: res.users, id: test_res_user_02}: - name: 'Test user B' - login: 'tub' - new_password: 'tub' -- - !record {model: res.users, id: test_res_user_03}: - name: 'Test user C' - login: 'tuc' - new_password: 'tuc' -- - !record {model: res.users, id: test_res_user_04}: - name: 'Test user D' - login: 'tud' - new_password: 'tud' -- - I create a mass convert wizard and convert all the leads. + Salesman create a mass convert wizard and convert all the leads. - !python {model: crm.lead2opportunity.partner.mass}: | context.update({'active_model': 'crm.lead', 'active_ids': [ref("test_crm_lead_01"), ref("test_crm_lead_02"), ref("test_crm_lead_03"), ref("test_crm_lead_04"), ref("test_crm_lead_05"), ref("test_crm_lead_06")], 'active_id': ref("test_crm_lead_01")}) diff --git a/addons/crm/test/phonecalls.yml b/addons/crm/test/phonecalls.yml index b1ea184f59b..dfc3d6e33b3 100644 --- a/addons/crm/test/phonecalls.yml +++ b/addons/crm/test/phonecalls.yml @@ -1,3 +1,8 @@ +- + Salesman check the phone calls data so test with the access rights of salesman. +- + !context + uid: 'crm_res_users_salesman' - I schedule a phone call with a customer. - diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index 5f264cbbc7c..ed35349f420 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -75,7 +75,7 @@ class email_template(osv.osv): _description = 'Email Templates' _order = 'name' - def render_template(self, cr, uid, template, model, res_id, context=None): + def render_template_batch(self, cr, uid, template, model, res_ids, context=None): """Render the given template text, replace mako expressions ``${expr}`` with the result of evaluating these expressions with an evaluation context containing: @@ -87,46 +87,60 @@ class email_template(osv.osv): :param str template: the template text to render :param str model: model name of the document record this mail is related to. - :param int res_id: id of the document record this mail is related to. + :param int res_ids: list of ids of document records those mails are related to. """ - if not template: - return u"" if context is None: context = {} - try: - template = tools.ustr(template) - record = None - if res_id: - record = self.pool[model].browse(cr, uid, res_id, context=context) - user = self.pool.get('res.users').browse(cr, uid, uid, context=context) - variables = { - 'object': record, - 'user': user, - 'ctx': context, # context kw would clash with mako internals - } - result = mako_template_env.from_string(template).render(variables) - if result == u"False": - result = u"" - return result - except Exception: - _logger.exception("failed to render mako template value %r", template) - return u"" + results = dict.fromkeys(res_ids, u"") - def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None): + # try to load the template + try: + template = mako_template_env.from_string(tools.ustr(template)) + except Exception: + _logger.exception("Failed to load template %r", template) + return results + + # prepare template variables + user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + records = self.pool[model].browse(cr, uid, res_ids, context=context) or [None] + variables = { + 'user': user, + 'ctx': context, # context kw would clash with mako internals + } + for record in records: + res_id = record.id if record else None + variables['object'] = record + try: + render_result = template.render(variables) + except Exception: + _logger.exception("Failed to render template %r using values %r" % (template, variables)) + render_result = u"" + if render_result == u"False": + render_result = u"" + results[res_id] = render_result + return results + + def get_email_template_batch(self, cr, uid, template_id=False, res_ids=None, context=None): if context is None: context = {} + if res_ids is None: + res_ids = [None] + results = dict.fromkeys(res_ids, False) + if not template_id: - return False + return results template = self.browse(cr, uid, template_id, context) - lang = self.render_template(cr, uid, template.lang, template.model, record_id, context) - if lang: - # Use translated template if necessary - ctx = context.copy() - ctx['lang'] = lang - template = self.browse(cr, uid, template.id, ctx) - else: - template = self.browse(cr, uid, int(template_id), context) - return template + langs = self.render_template_batch(cr, uid, template.lang, template.model, res_ids, context) + for res_id, lang in langs.iteritems(): + if lang: + # Use translated template if necessary + ctx = context.copy() + ctx['lang'] = lang + template = self.browse(cr, uid, template.id, ctx) + else: + template = self.browse(cr, uid, int(template_id), context) + results[res_id] = template + return results def onchange_model_id(self, cr, uid, ids, model_id, context=None): mod_name = False @@ -308,64 +322,75 @@ class email_template(osv.osv): }) return {'value': result} - def generate_email(self, cr, uid, template_id, res_id, context=None): - """Generates an email from the template for given (model, res_id) pair. + def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): + """Generates an email from the template for given the given model based on + records given by res_ids. - :param template_id: id of the template to render. - :param res_id: id of the record to use for rendering the template (model - is taken from template definition) - :returns: a dict containing all relevant fields for creating a new - mail.mail entry, with one extra key ``attachments``, in the - format expected by :py:meth:`mail_thread.message_post`. + :param template_id: id of the template to render. + :param res_id: id of the record to use for rendering the template (model + is taken from template definition) + :returns: a dict containing all relevant fields for creating a new + mail.mail entry, with one extra key ``attachments``, in the + format expected by :py:meth:`mail_thread.message_post`. """ if context is None: context = {} + if fields is None: + fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to'] + report_xml_pool = self.pool.get('ir.actions.report.xml') - template = self.get_email_template(cr, uid, template_id, res_id, context) - values = {} - for field in ['subject', 'body_html', 'email_from', - 'email_to', 'partner_to', 'email_cc', 'reply_to']: - values[field] = self.render_template(cr, uid, getattr(template, field), - template.model, res_id, context=context) \ - or False - if template.user_signature: - signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature - values['body_html'] = tools.append_content_to_html(values['body_html'], signature) + res_ids_to_templates = self.get_email_template_batch(cr, uid, template_id, res_ids, context) - if values['body_html']: - values['body'] = tools.html_sanitize(values['body_html']) + # templates: res_id -> template; template -> res_ids + templates_to_res_ids = {} + for res_id, template in res_ids_to_templates.iteritems(): + templates_to_res_ids.setdefault(template, []).append(res_id) - values.update(mail_server_id=template.mail_server_id.id or False, - auto_delete=template.auto_delete, - model=template.model, - res_id=res_id or False) + results = dict() + for template, template_res_ids in templates_to_res_ids.iteritems(): + # generate fields value for all res_ids linked to the current template + for field in ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']: + generated_field_values = self.render_template_batch(cr, uid, getattr(template, field), template.model, template_res_ids, context=context) + for res_id, field_value in generated_field_values.iteritems(): + results.setdefault(res_id, dict())[field] = field_value + # update values for all res_ids + for res_id in template_res_ids: + values = results[res_id] + if template.user_signature: + signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature + values['body_html'] = tools.append_content_to_html(values['body_html'], signature) + if values['body_html']: + values['body'] = tools.html_sanitize(values['body_html']) + values.update( + mail_server_id=template.mail_server_id.id or False, + auto_delete=template.auto_delete, + model=template.model, + res_id=res_id or False, + attachment_ids=[attach.id for attach in template.attachment_ids], + ) - attachments = [] - # Add report in attachments - if template.report_template: - report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context) - report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name - # Ensure report is rendered using template's language - ctx = context.copy() - if template.lang: - ctx['lang'] = self.render_template(cr, uid, template.lang, template.model, res_id, context) - result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx) - result = base64.b64encode(result) - if not report_name: - report_name = 'report.' + report_service - ext = "." + format - if not report_name.endswith(ext): - report_name += ext - attachments.append((report_name, result)) + # Add report in attachments + if template.report_template: + for res_id in template_res_ids: + attachments = [] + report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context) + report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name + # Ensure report is rendered using template's language + ctx = context.copy() + if template.lang: + ctx['lang'] = self.render_template_batch(cr, uid, template.lang, template.model, res_id, context) # take 0 ? + result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx) + result = base64.b64encode(result) + if not report_name: + report_name = 'report.' + report_service + ext = "." + format + if not report_name.endswith(ext): + report_name += ext + attachments.append((report_name, result)) - attachment_ids = [] - # Add template attachments - for attach in template.attachment_ids: - attachment_ids.append(attach.id) + values['attachments'] = attachments - values['attachments'] = attachments - values['attachment_ids'] = attachment_ids - return values + return results def send_mail(self, cr, uid, template_id, res_id, force_send=False, raise_exception=False, context=None): """Generates a new mail message for the given template and record, @@ -412,4 +437,14 @@ class email_template(osv.osv): mail_mail.send(cr, uid, [msg_id], raise_exception=raise_exception, context=context) return msg_id + # Compatibility method + def render_template(self, cr, uid, template, model, res_id, context=None): + return self.render_template_batch(cr, uid, template, model, [res_id], context)[res_id] + + def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None): + return self.get_email_template_batch(cr, uid, template_id, [record_id], context)[record_id] + + def generate_email(self, cr, uid, template_id, res_id, context=None): + return self.generate_email_batch(cr, uid, template_id, [res_id], context)[res_id] + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/email_template/i18n/en_GB.po b/addons/email_template/i18n/en_GB.po new file mode 100644 index 00000000000..5734ecbd334 --- /dev/null +++ b/addons/email_template/i18n/en_GB.po @@ -0,0 +1,490 @@ +# English (United Kingdom) translation for openobject-addons +# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:05+0000\n" +"PO-Revision-Date: 2013-09-14 06:46+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: English (United Kingdom) \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2013-09-15 05:49+0000\n" +"X-Generator: Launchpad (build 16761)\n" + +#. module: email_template +#: field:email.template,email_from:0 +#: field:email_template.preview,email_from:0 +msgid "From" +msgstr "From" + +#. module: email_template +#: field:mail.compose.message,template_id:0 +msgid "Template" +msgstr "" + +#. module: email_template +#: help:email.template,ref_ir_value:0 +#: help:email_template.preview,ref_ir_value:0 +msgid "Sidebar button to open the sidebar action" +msgstr "Sidebar button to open the sidebar action" + +#. module: email_template +#: field:res.partner,opt_out:0 +msgid "Opt-Out" +msgstr "Opt-Out" + +#. module: email_template +#: field:email.template,email_to:0 +#: field:email_template.preview,email_to:0 +msgid "To (Emails)" +msgstr "To (E-mails)" + +#. module: email_template +#: field:email.template,mail_server_id:0 +#: field:email_template.preview,mail_server_id:0 +msgid "Outgoing Mail Server" +msgstr "Outgoing Mail Server" + +#. module: email_template +#: help:email.template,ref_ir_act_window:0 +#: help:email_template.preview,ref_ir_act_window:0 +msgid "" +"Sidebar action to make this template available on records of the related " +"document model" +msgstr "" +"Sidebar action to make this template available on records of the related " +"document model" + +#. module: email_template +#: field:email.template,model_object_field:0 +#: field:email_template.preview,model_object_field:0 +msgid "Field" +msgstr "Field" + +#. module: email_template +#: help:email.template,email_from:0 +#: help:email_template.preview,email_from:0 +msgid "Sender address (placeholders may be used here)" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "Remove context action" +msgstr "Remove context action" + +#. module: email_template +#: help:email.template,mail_server_id:0 +#: help:email_template.preview,mail_server_id:0 +msgid "" +"Optional preferred server for outgoing mails. If not set, the highest " +"priority one will be used." +msgstr "" + +#. module: email_template +#: field:email.template,report_name:0 +#: field:email_template.preview,report_name:0 +msgid "Report Filename" +msgstr "Report Filename" + +#. module: email_template +#: view:email.template:0 +msgid "Preview" +msgstr "" + +#. module: email_template +#: field:email.template,reply_to:0 +#: field:email_template.preview,reply_to:0 +msgid "Reply-To" +msgstr "" + +#. module: email_template +#: view:mail.compose.message:0 +msgid "Use template" +msgstr "" + +#. module: email_template +#: field:email.template,body_html:0 +#: field:email_template.preview,body_html:0 +msgid "Body" +msgstr "" + +#. module: email_template +#: code:addons/email_template/email_template.py:247 +#, python-format +msgid "%s (copy)" +msgstr "" + +#. module: email_template +#: help:email.template,user_signature:0 +#: help:email_template.preview,user_signature:0 +msgid "" +"If checked, the user's signature will be appended to the text version of the " +"message" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "SMTP Server" +msgstr "" + +#. module: email_template +#: view:mail.compose.message:0 +msgid "Save as new template" +msgstr "" + +#. module: email_template +#: help:email.template,sub_object:0 +#: help:email_template.preview,sub_object:0 +msgid "" +"When a relationship field is selected as first field, this field shows the " +"document model the relationship goes to." +msgstr "" + +#. module: email_template +#: model:ir.model,name:email_template.model_email_template +msgid "Email Templates" +msgstr "" + +#. module: email_template +#: help:email.template,report_name:0 +#: help:email_template.preview,report_name:0 +msgid "" +"Name to use for the generated report file (may contain placeholders)\n" +"The extension can be omitted and will then come from the report type." +msgstr "" + +#. module: email_template +#: field:email.template,ref_ir_act_window:0 +#: field:email_template.preview,ref_ir_act_window:0 +msgid "Sidebar action" +msgstr "" + +#. module: email_template +#: help:email.template,lang:0 +#: help:email_template.preview,lang:0 +msgid "" +"Optional translation language (ISO code) to select when sending out an " +"email. If not set, the english version will be used. This should usually be " +"a placeholder expression that provides the appropriate language code, e.g. " +"${object.partner_id.lang.code}." +msgstr "" + +#. module: email_template +#: field:email_template.preview,res_id:0 +msgid "Sample Document" +msgstr "" + +#. module: email_template +#: help:email.template,model_object_field:0 +#: help:email_template.preview,model_object_field:0 +msgid "" +"Select target field from the related document model.\n" +"If it is a relationship field you will be able to select a target field at " +"the destination of the relationship." +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "Dynamic Value Builder" +msgstr "" + +#. module: email_template +#: model:ir.actions.act_window,name:email_template.wizard_email_template_preview +msgid "Template Preview" +msgstr "" + +#. module: email_template +#: view:mail.compose.message:0 +msgid "Save as a new template" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "" +"Display an option on related documents to open a composition wizard with " +"this template" +msgstr "" + +#. module: email_template +#: help:email.template,email_cc:0 +#: help:email_template.preview,email_cc:0 +msgid "Carbon copy recipients (placeholders may be used here)" +msgstr "" + +#. module: email_template +#: help:email.template,email_to:0 +#: help:email_template.preview,email_to:0 +msgid "Comma-separated recipient addresses (placeholders may be used here)" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "Advanced" +msgstr "" + +#. module: email_template +#: view:email_template.preview:0 +msgid "Preview of" +msgstr "" + +#. module: email_template +#: view:email_template.preview:0 +msgid "Using sample document" +msgstr "" + +#. module: email_template +#: view:email.template:0 +#: model:ir.actions.act_window,name:email_template.action_email_template_tree_all +#: model:ir.ui.menu,name:email_template.menu_email_templates +msgid "Templates" +msgstr "" + +#. module: email_template +#: field:email.template,name:0 +#: field:email_template.preview,name:0 +msgid "Name" +msgstr "" + +#. module: email_template +#: field:email.template,lang:0 +#: field:email_template.preview,lang:0 +msgid "Language" +msgstr "" + +#. module: email_template +#: model:ir.model,name:email_template.model_email_template_preview +msgid "Email Template Preview" +msgstr "" + +#. module: email_template +#: view:email_template.preview:0 +msgid "Email Preview" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "" +"Remove the contextual action to use this template on related documents" +msgstr "" + +#. module: email_template +#: field:email.template,copyvalue:0 +#: field:email_template.preview,copyvalue:0 +msgid "Placeholder Expression" +msgstr "" + +#. module: email_template +#: field:email.template,sub_object:0 +#: field:email_template.preview,sub_object:0 +msgid "Sub-model" +msgstr "" + +#. module: email_template +#: help:email.template,subject:0 +#: help:email_template.preview,subject:0 +msgid "Subject (placeholders may be used here)" +msgstr "" + +#. module: email_template +#: help:email.template,reply_to:0 +#: help:email_template.preview,reply_to:0 +msgid "Preferred response address (placeholders may be used here)" +msgstr "" + +#. module: email_template +#: field:email.template,ref_ir_value:0 +#: field:email_template.preview,ref_ir_value:0 +msgid "Sidebar Button" +msgstr "" + +#. module: email_template +#: field:email.template,report_template:0 +#: field:email_template.preview,report_template:0 +msgid "Optional report to print and attach" +msgstr "" + +#. module: email_template +#: help:email.template,null_value:0 +#: help:email_template.preview,null_value:0 +msgid "Optional value to use if the target field is empty" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "Model" +msgstr "" + +#. module: email_template +#: model:ir.model,name:email_template.model_mail_compose_message +msgid "Email composition wizard" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "Add context action" +msgstr "" + +#. module: email_template +#: help:email.template,model_id:0 +#: help:email_template.preview,model_id:0 +msgid "The kind of document with with this template can be used" +msgstr "" + +#. module: email_template +#: field:email.template,email_recipients:0 +#: field:email_template.preview,email_recipients:0 +msgid "To (Partners)" +msgstr "" + +#. module: email_template +#: field:email.template,auto_delete:0 +#: field:email_template.preview,auto_delete:0 +msgid "Auto Delete" +msgstr "" + +#. module: email_template +#: help:email.template,copyvalue:0 +#: help:email_template.preview,copyvalue:0 +msgid "" +"Final placeholder expression, to be copy-pasted in the desired template " +"field." +msgstr "" + +#. module: email_template +#: field:email.template,model:0 +#: field:email_template.preview,model:0 +msgid "Related Document Model" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "Addressing" +msgstr "" + +#. module: email_template +#: help:email.template,email_recipients:0 +#: help:email_template.preview,email_recipients:0 +msgid "" +"Comma-separated ids of recipient partners (placeholders may be used here)" +msgstr "" + +#. module: email_template +#: field:email.template,attachment_ids:0 +#: field:email_template.preview,attachment_ids:0 +msgid "Attachments" +msgstr "" + +#. module: email_template +#: code:addons/email_template/email_template.py:234 +#, python-format +msgid "Deletion of the action record failed." +msgstr "" + +#. module: email_template +#: field:email.template,email_cc:0 +#: field:email_template.preview,email_cc:0 +msgid "Cc" +msgstr "" + +#. module: email_template +#: field:email.template,model_id:0 +#: field:email_template.preview,model_id:0 +msgid "Applies to" +msgstr "" + +#. module: email_template +#: field:email.template,sub_model_object_field:0 +#: field:email_template.preview,sub_model_object_field:0 +msgid "Sub-field" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "Email Details" +msgstr "" + +#. module: email_template +#: code:addons/email_template/email_template.py:199 +#, python-format +msgid "Send Mail (%s)" +msgstr "" + +#. module: email_template +#: help:res.partner,opt_out:0 +msgid "" +"If checked, this partner will not receive any automated email notifications, " +"such as the availability of invoices." +msgstr "" + +#. module: email_template +#: help:email.template,auto_delete:0 +#: help:email_template.preview,auto_delete:0 +msgid "Permanently delete this email after sending it, to save space" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "Group by..." +msgstr "" + +#. module: email_template +#: help:email.template,sub_model_object_field:0 +#: help:email_template.preview,sub_model_object_field:0 +msgid "" +"When a relationship field is selected as first field, this field lets you " +"select the target field within the destination document model (sub-model)." +msgstr "" + +#. module: email_template +#: code:addons/email_template/email_template.py:234 +#, python-format +msgid "Warning" +msgstr "" + +#. module: email_template +#: field:email.template,user_signature:0 +#: field:email_template.preview,user_signature:0 +msgid "Add Signature" +msgstr "" + +#. module: email_template +#: model:ir.model,name:email_template.model_res_partner +msgid "Partner" +msgstr "" + +#. module: email_template +#: field:email.template,null_value:0 +#: field:email_template.preview,null_value:0 +msgid "Default Value" +msgstr "" + +#. module: email_template +#: help:email.template,attachment_ids:0 +#: help:email_template.preview,attachment_ids:0 +msgid "" +"You may attach files to this template, to be added to all emails created " +"from this template" +msgstr "" + +#. module: email_template +#: help:email.template,body_html:0 +#: help:email_template.preview,body_html:0 +msgid "Rich-text/HTML version of the message (placeholders may be used here)" +msgstr "" + +#. module: email_template +#: view:email.template:0 +msgid "Contents" +msgstr "" + +#. module: email_template +#: field:email.template,subject:0 +#: field:email_template.preview,subject:0 +msgid "Subject" +msgstr "" diff --git a/addons/email_template/tests/test_mail.py b/addons/email_template/tests/test_mail.py index d5feea9ebc4..ccc8a49b875 100644 --- a/addons/email_template/tests/test_mail.py +++ b/addons/email_template/tests/test_mail.py @@ -20,10 +20,10 @@ ############################################################################## import base64 -from openerp.addons.mail.tests.test_mail_base import TestMailBase +from openerp.addons.mail.tests.common import TestMail -class test_message_compose(TestMailBase): +class test_message_compose(TestMail): def setUp(self): super(test_message_compose, self).setUp() @@ -73,7 +73,7 @@ class test_message_compose(TestMailBase): # 1. Comment on pigs compose_id = mail_compose.create(cr, uid, - {'subject': 'Forget me subject', 'body': '

Dummy body

'}, + {'subject': 'Forget me subject', 'body': '

Dummy body

', 'post': True}, {'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_pigs_id, @@ -101,7 +101,7 @@ class test_message_compose(TestMailBase): 'default_template_id': email_template_id, 'active_ids': [self.group_pigs_id, self.group_bird_id] } - compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context) + compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, context) compose = mail_compose.browse(cr, uid, compose_id, context) onchange_res = compose.onchange_template_id(email_template_id, 'comment', 'mail.group', self.group_pigs_id)['value'] onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])] @@ -145,7 +145,7 @@ class test_message_compose(TestMailBase): 'default_partner_ids': [p_a_id], 'active_ids': [self.group_pigs_id, self.group_bird_id] } - compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context) + compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, context) compose = mail_compose.browse(cr, uid, compose_id, context) onchange_res = compose.onchange_template_id(email_template_id, 'mass_mail', 'mail.group', self.group_pigs_id)['value'] onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])] diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py index 0aab765efbf..bae7a0d8f9c 100644 --- a/addons/email_template/wizard/mail_compose_message.py +++ b/addons/email_template/wizard/mail_compose_message.py @@ -62,6 +62,7 @@ class mail_compose_message(osv.TransientModel): for wizard in self.browse(cr, uid, ids, context=context): if wizard.template_id: wizard_context['mail_notify_user_signature'] = False # template user_signature is added when generating body_html + wizard_context['mail_auto_delete'] = wizard.template_id.auto_delete # mass mailing: use template auto_delete value -> note, for emails mass mailing only if not wizard.attachment_ids or wizard.composition_mode == 'mass_mail' or not wizard.template_id: continue new_attachment_ids = [] @@ -81,7 +82,7 @@ class mail_compose_message(osv.TransientModel): template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context) values = dict((field, template_values[field]) for field in fields if template_values.get(field)) elif template_id: - values = self.generate_email_for_composer(cr, uid, template_id, res_id, context=context) + values = self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context=context)[res_id] # transform attachments into attachment_ids; not attached to the document because this will # be done further in the posting process, allowing to clean database if email not send values['attachment_ids'] = values.pop('attachment_ids', []) @@ -147,45 +148,55 @@ class mail_compose_message(osv.TransientModel): partner_ids.append(int(partner_id)) return partner_ids - def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None): + def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None): """ Call email_template.generate_email(), get fields relevant for mail.compose.message, transform email_cc and email_to into partner_ids """ - template_values = self.pool.get('email.template').generate_email(cr, uid, template_id, res_id, context=context) # filter template values fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'attachments', 'mail_server_id'] - values = dict((field, template_values[field]) for field in fields if template_values.get(field)) - values['body'] = values.pop('body_html', '') + values = dict.fromkeys(res_ids, False) - # transform email_to, email_cc into partner_ids - ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_')) - partner_ids = self._get_or_create_partners_from_values(cr, uid, values, context=ctx) - # legacy template behavior: void values do not erase existing values and the - # related key is removed from the values dict - if partner_ids: - values['partner_ids'] = list(partner_ids) + template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, context=context) + for res_id in res_ids: + res_id_values = dict((field, template_values[res_id][field]) for field in fields if template_values[res_id].get(field)) + res_id_values['body'] = res_id_values.pop('body_html', '') + # transform email_to, email_cc into partner_ids + ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_')) + partner_ids = self._get_or_create_partners_from_values(cr, uid, res_id_values, context=ctx) + # legacy template behavior: void values do not erase existing values and the + # related key is removed from the values dict + if partner_ids: + res_id_values['partner_ids'] = list(partner_ids) + + values[res_id] = res_id_values return values - def render_message(self, cr, uid, wizard, res_id, context=None): + def render_message_batch(self, cr, uid, wizard, res_ids, context=None): """ Override to handle templates. """ - # generate the composer email + # generate template-based values if wizard.template_id: - values = self.generate_email_for_composer(cr, uid, wizard.template_id.id, res_id, context=context) + template_values = self.generate_email_for_composer_batch(cr, uid, wizard.template_id.id, res_ids, context=context) else: - values = {} - # remove attachments as they should not be rendered - values.pop('attachment_ids', None) - # get values to return - email_dict = super(mail_compose_message, self).render_message(cr, uid, wizard, res_id, context) - # those values are not managed; they are readonly - email_dict.pop('email_to', None) - email_dict.pop('email_cc', None) - email_dict.pop('partner_to', None) - # update template values by wizard values - values.update(email_dict) - return values + template_values = dict.fromkeys(res_ids, dict()) + # generate composer values + composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context) - def render_template(self, cr, uid, template, model, res_id, context=None): - return self.pool.get('email.template').render_template(cr, uid, template, model, res_id, context=context) + for res_id in res_ids: + # remove attachments from template values as they should not be rendered + template_values[res_id].pop('attachment_ids', None) + # remove some keys from composer that are readonly + composer_values[res_id].pop('email_to', None) + composer_values[res_id].pop('email_cc', None) + composer_values[res_id].pop('partner_to', None) + # update template values by composer values + template_values[res_id].update(composer_values[res_id]) + return template_values + + def render_template_batch(self, cr, uid, template, model, res_ids, context=None): + return self.pool.get('email.template').render_template_batch(cr, uid, template, model, res_ids, context=context) + + # Compatibility methods + def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None): + return self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context)[res_id] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/email_template/wizard/mail_compose_message_view.xml b/addons/email_template/wizard/mail_compose_message_view.xml index 3cb83896ca2..b7c57af2679 100644 --- a/addons/email_template/wizard/mail_compose_message_view.xml +++ b/addons/email_template/wizard/mail_compose_message_view.xml @@ -11,7 +11,7 @@