parent
89f7038154
commit
44f68794e7
@ -0,0 +1,696 @@ |
||||
timelinesApp.factory('Timeline', ['Constants', 'FilterQueryStringBuilder', 'TreeNode', 'UI', 'Color', 'HistoricalPlanningElement', 'PlanningElement', 'PlanningElementType', 'Project', 'ProjectAssociation', 'ProjectType', 'Reporting', 'Status', 'User', function(Constants, FilterQueryStringBuilder, TreeNode, UI, Color, HistoricalPlanningElement, PlanningElement, PlanningElementType, Project,ProjectAssociation, ProjectType, Reporting, Status, User) { |
||||
|
||||
Timeline = {}; |
||||
|
||||
angular.extend(Timeline, Constants); |
||||
Timeline.FilterQueryStringBuilder = FilterQueryStringBuilder; |
||||
angular.extend(Timeline, {TreeNode: TreeNode}); |
||||
angular.extend(Timeline, UI); |
||||
|
||||
// model mix ins
|
||||
angular.extend(Timeline, {Color: Color}); |
||||
angular.extend(Timeline, {HistoricalPlanningElement: HistoricalPlanningElement}); |
||||
angular.extend(Timeline, {PlanningElement: PlanningElement}); |
||||
angular.extend(Timeline, {PlanningElementType: PlanningElementType}); |
||||
angular.extend(Timeline, {Project: Project}); |
||||
angular.extend(Timeline, {ProjectAssociation: ProjectAssociation}); |
||||
angular.extend(Timeline, {ProjectType: ProjectType}); |
||||
angular.extend(Timeline, {Reporting: Reporting}); |
||||
angular.extend(Timeline, {Status: Status}); |
||||
angular.extend(Timeline, {User: User}); |
||||
|
||||
//startup
|
||||
angular.extend(Timeline, { |
||||
instances: [], |
||||
get: function(n) { |
||||
if (typeof n !== "number") { |
||||
n = 0; |
||||
} |
||||
return this.instances[n]; |
||||
}, |
||||
isInstance: function(n) { |
||||
return (n === undefined) ? |
||||
Timeline.instances.indexOf(this) : |
||||
this === Timeline.get(n); |
||||
}, |
||||
isGrouping: function() { |
||||
if ((this.options.grouping_one_enabled === 'yes' && |
||||
this.options.grouping_one_selection !== undefined) || |
||||
(this.options.grouping_two_enabled === 'yes' && |
||||
this.options.grouping_two_selection !== undefined)) { |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
}, |
||||
isComparing: function() { |
||||
return ((this.options.comparison !== undefined) && |
||||
(this.options.comparison !== 'none')); |
||||
}, |
||||
comparisonCurrentTime: function() { |
||||
var value; |
||||
if (!this.isComparing()) { |
||||
return undefined; |
||||
} |
||||
if (this.options.comparison === 'historical') { |
||||
value = this.options.compare_to_historical_two; |
||||
} |
||||
else { |
||||
// default is no (undefined) current time, which corresponds to today.
|
||||
return undefined; |
||||
} |
||||
return +Date.parse(value) / 1000; |
||||
}, |
||||
calculateTimeFilter: function () { |
||||
if (!this.frameSet) { |
||||
if (this.options.planning_element_time === "absolute") { |
||||
this.frameStart = Date.parse(this.options.planning_element_time_absolute_one); |
||||
this.frameEnd = Date.parse(this.options.planning_element_time_absolute_two); |
||||
} else if (this.options.planning_element_time === "relative") { |
||||
var startR = parseInt(this.options.planning_element_time_relative_one, 10); |
||||
var endR = parseInt(this.options.planning_element_time_relative_two, 10); |
||||
if (!isNaN(startR)) { |
||||
this.frameStart = Date.now(); |
||||
|
||||
switch (this.options.planning_element_time_relative_one_unit[0]) { |
||||
case "0": |
||||
this.frameStart.add(-1 * startR).days(); |
||||
break; |
||||
case "1": |
||||
this.frameStart.add(-1 * startR).weeks(); |
||||
break; |
||||
case "2": |
||||
this.frameStart.add(-1 * startR).months(); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (!isNaN(endR)) { |
||||
this.frameEnd = Date.now(); |
||||
|
||||
switch (this.options.planning_element_time_relative_two_unit[0]) { |
||||
case "0": |
||||
this.frameEnd.add(endR).days(); |
||||
break; |
||||
case "1": |
||||
this.frameEnd.add(endR).weeks(); |
||||
break; |
||||
case "2": |
||||
this.frameEnd.add(endR).months(); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
this.frameSet = true; |
||||
} |
||||
}, |
||||
inTimeFilter: function (start, end) { |
||||
this.calculateTimeFilter(); |
||||
|
||||
if (!start && !end) { |
||||
return false; |
||||
} |
||||
|
||||
if (!start) { |
||||
start = end; |
||||
} |
||||
|
||||
if (!end) { |
||||
end = start; |
||||
} |
||||
|
||||
if (this.frameStart) { |
||||
if (start < this.frameStart && end < this.frameStart) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
if (this.frameEnd) { |
||||
if (start > this.frameEnd && end > this.frameEnd) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
}, |
||||
verticalPlanningElementIds: function() { |
||||
|
||||
return this.options.vertical_planning_elements ? |
||||
jQuery.map( |
||||
this.options.vertical_planning_elements.split(/\,/), |
||||
function(a) { |
||||
try { |
||||
return parseInt(a.match(/\s*\*?(\d*)\s*/)[1], 10); |
||||
} catch (e) { |
||||
return; |
||||
} |
||||
} |
||||
) : []; |
||||
}, |
||||
comparisonTarget: function() { |
||||
var result, value, unit; |
||||
if (!this.isComparing()) { |
||||
return undefined; |
||||
} |
||||
switch (this.options.comparison) { |
||||
case 'relative': |
||||
result = new Date(); |
||||
value = Timeline.pnum(this.options.compare_to_relative); |
||||
unit = Timeline.pnum(this.options.compare_to_relative_unit[0]); |
||||
switch (unit) { |
||||
case 0: |
||||
return Math.floor(result.add(-value).days() / 1000); |
||||
case 1: |
||||
return Math.floor(result.add(-value).weeks() / 1000); |
||||
case 2: |
||||
return Math.floor(result.add(-value).months() / 1000); |
||||
default: |
||||
return this.die(this.i18n('timelines.errors.report_comparison')); |
||||
} |
||||
break; // to please jslint
|
||||
case 'absolute': |
||||
value = this.options.compare_to_absolute; |
||||
break; |
||||
case 'historical': |
||||
value = this.options.compare_to_historical_one; |
||||
break; |
||||
default: |
||||
return this.die(this.i18n('timelines.errors.report_comparison')); |
||||
} |
||||
return +Date.parse(value)/1000; |
||||
}, |
||||
create: function(options) { |
||||
// configuration
|
||||
|
||||
if (!options) { |
||||
throw new Error('No configuration options given'); |
||||
} |
||||
|
||||
options = jQuery.extend({}, this.defaults, options); |
||||
|
||||
if (options.username) { |
||||
this.ajax_defaults.username = options.username; |
||||
} |
||||
if (options.password) { |
||||
this.ajax_defaults.password = options.password; |
||||
} |
||||
if (options.api_key) { |
||||
this.ajax_defaults.headers = { |
||||
'X-ChiliProject-API-Key': options.api_key, |
||||
'X-OpenProject-API-Key': options.api_key, |
||||
'X-Redmine-API-Key': options.api_key |
||||
}; |
||||
} |
||||
|
||||
this.options = options; |
||||
|
||||
// we're hiding the root if there is a grouping.
|
||||
this.options.hide_tree_root = this.isGrouping(); |
||||
|
||||
var timeline = Object.create(Timeline); |
||||
|
||||
// some private fields.
|
||||
timeline.listeners = []; |
||||
timeline.data = {}; |
||||
|
||||
Timeline.instances.push(timeline); |
||||
return timeline; |
||||
}, |
||||
registerTimelineContainer: function(uiRoot) { |
||||
this.uiRoot = uiRoot; |
||||
this.registerDrawPaper(); |
||||
}, |
||||
checkPrerequisites: function() { |
||||
if (jQuery === undefined) { |
||||
throw new Error('jQuery seems to be missing (jQuery is undefined)'); |
||||
} else if (jQuery().slider === undefined) { |
||||
throw new Error('jQuery UI seems to be missing (jQuery().slider is undefined)'); |
||||
} else if ((1).month === undefined) { |
||||
throw new Error('date.js seems to be missing ((1).month is undefined)'); |
||||
} else if (Raphael === undefined) { |
||||
throw new Error('Raphael seems to be missing (Raphael is undefined)'); |
||||
} |
||||
return true; |
||||
}, |
||||
reload: function() { |
||||
delete this.lefthandTree; |
||||
|
||||
var timelineLoader = this.provideTimelineLoader(); |
||||
|
||||
jQuery(timelineLoader).on('complete', jQuery.proxy(function (e, data) { |
||||
|
||||
jQuery.extend(this, data); |
||||
jQuery(this).trigger('dataReLoaded'); |
||||
|
||||
if (this.isGrouping() && this.options.grouping_two_enabled) { |
||||
this.secondLevelGroupingAdjustments(); |
||||
} |
||||
|
||||
this.adjustForPlanningElements(); |
||||
|
||||
this.rebuildAll(); |
||||
|
||||
}, this)); |
||||
|
||||
timelineLoader.load(); |
||||
}, |
||||
provideTimelineLoader: function() { |
||||
return new Timeline.TimelineLoader( |
||||
this, |
||||
{ |
||||
api_prefix : this.options.api_prefix, |
||||
url_prefix : this.options.url_prefix, |
||||
project_prefix : this.options.project_prefix, |
||||
planning_element_prefix : this.options.planning_element_prefix, |
||||
project_id : this.options.project_id, |
||||
project_types : this.options.project_types, |
||||
project_statuses : this.options.project_status, |
||||
project_responsibles : this.options.project_responsibles, |
||||
project_parents : this.options.parents, |
||||
planning_element_types : this.options.planning_element_types, |
||||
planning_element_responsibles : this.options.planning_element_responsibles, |
||||
planning_element_status : this.options.planning_element_status, |
||||
grouping_one : (this.options.grouping_one_enabled ? this.options.grouping_one_selection : undefined), |
||||
grouping_two : (this.options.grouping_two_enabled ? this.options.grouping_two_selection : undefined), |
||||
ajax_defaults : this.ajax_defaults, |
||||
current_time : this.comparisonCurrentTime(), |
||||
target_time : this.comparisonTarget(), |
||||
include_planning_elements : this.verticalPlanningElementIds() |
||||
} |
||||
); |
||||
}, |
||||
getTimelineLoaderOptions: function() { |
||||
return { |
||||
api_prefix : this.options.api_prefix, |
||||
url_prefix : this.options.url_prefix, |
||||
project_prefix : this.options.project_prefix, |
||||
planning_element_prefix : this.options.planning_element_prefix, |
||||
project_id : this.options.project_id, |
||||
project_types : this.options.project_types, |
||||
project_statuses : this.options.project_status, |
||||
project_responsibles : this.options.project_responsibles, |
||||
project_parents : this.options.parents, |
||||
planning_element_types : this.options.planning_element_types, |
||||
planning_element_responsibles : this.options.planning_element_responsibles, |
||||
planning_element_status : this.options.planning_element_status, |
||||
grouping_one : (this.options.grouping_one_enabled ? this.options.grouping_one_selection : undefined), |
||||
grouping_two : (this.options.grouping_two_enabled ? this.options.grouping_two_selection : undefined), |
||||
ajax_defaults : this.ajax_defaults, |
||||
current_time : this.comparisonCurrentTime(), |
||||
target_time : this.comparisonTarget(), |
||||
include_planning_elements : this.verticalPlanningElementIds() |
||||
}; |
||||
}, |
||||
defer: function(action, delay) { |
||||
var timeline = this; |
||||
var result; |
||||
if (delay === undefined) { |
||||
delay = 0; |
||||
} |
||||
result = window.setTimeout(function() { |
||||
try { |
||||
action.call(); |
||||
} catch(e) { |
||||
timeline.die(e); |
||||
} |
||||
}, 0); |
||||
return result; |
||||
}, |
||||
die: function(error, classes) { |
||||
var message = (typeof error === 'string') ? error : |
||||
this.i18n('timelines.errors.report_epicfail'); // + '<br>' + error.message;
|
||||
classes = classes || 'flash error'; |
||||
|
||||
this.warn(message, classes); |
||||
|
||||
// assume this won't happen anymore.
|
||||
this.onLoadComplete = function() {}; |
||||
|
||||
if (console && console.log) { |
||||
console.log(error.stack); |
||||
} |
||||
|
||||
throw error; |
||||
}, |
||||
warn: function(message, classes) { |
||||
var root = this.getUiRoot(); |
||||
|
||||
window.setTimeout(function() { |
||||
|
||||
// generate and display the error message.
|
||||
var warning = jQuery('<div class="' + classes + '">' + message + '</div>'); |
||||
root.empty().append(warning); |
||||
|
||||
}, Timeline.DISPLAY_ERROR_DELAY); |
||||
}, |
||||
onLoadComplete: function() { |
||||
// everything here should be wrapped in try/catch, to never
|
||||
var tree; |
||||
try { |
||||
window.clearTimeout(this.safetyHook); |
||||
|
||||
if (this.isGrouping() && this.options.grouping_two_enabled) { |
||||
this.secondLevelGroupingAdjustments(); |
||||
} |
||||
|
||||
tree = this.getLefthandTree(); |
||||
if (tree.containsPlanningElements() || tree.containsProjects()) { |
||||
this.adjustForPlanningElements(); |
||||
this.completeUI(); |
||||
} else { |
||||
this.warn(this.i18n('label_no_data'), 'warning'); |
||||
} |
||||
} catch (e) { |
||||
this.die(e); |
||||
} |
||||
}, |
||||
secondLevelGroupingAdjustments : function () { |
||||
var grouping = jQuery.map(this.options.grouping_two_selection || [], Timeline.pnum); |
||||
var root = this.getProject(); |
||||
var associations = Timeline.ProjectAssociation.all(this); |
||||
var listToRemove = []; |
||||
|
||||
// for all projects on the first level
|
||||
jQuery.each(root.getReporters(), function (i, e) { |
||||
|
||||
// find all projects that are associated
|
||||
jQuery.each(associations, function (j, a) { |
||||
|
||||
if (a.involves(e)) { |
||||
var other = a.getOther(e); |
||||
if (typeof other.getProjectType === "function") { |
||||
var pt = other.getProjectType(); |
||||
var type = pt !== null ? pt.id : -1; |
||||
var relevant = false; |
||||
|
||||
jQuery.each(grouping, function(k, l) { |
||||
if (l === type) { |
||||
relevant = true; |
||||
} |
||||
}); |
||||
|
||||
if (relevant) { |
||||
|
||||
// add the other project as a simulated reporter to the current one.
|
||||
e.addReporter(other); |
||||
other.hasSecondLevelGroupingAdjustment = true; |
||||
// remove the project from the root level of the report.
|
||||
listToRemove.push(other); |
||||
|
||||
} |
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
// remove all children of root that we couldn't remove while still iterating.
|
||||
jQuery.each(listToRemove, function(i, e) { |
||||
root.removeReporter(e); |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
// ╭───────────────────────────────────────────────────────────────────╮
|
||||
// │ Defaults and random accessors │
|
||||
// ╰───────────────────────────────────────────────────────────────────╯
|
||||
jQuery.extend(Timeline, { |
||||
// defines how many levels are expanded when a tree is created, zero
|
||||
// corresponds to the root being collapsed.
|
||||
firstDateSeen: null, |
||||
lastDateSeen: null, |
||||
|
||||
getBeginning: function() { |
||||
return (Date.parse(this.options.timeframe_start) || |
||||
(this.firstDateSeen && this.firstDateSeen.clone() || |
||||
new Date()).last().monday()); |
||||
}, |
||||
getEnd: function() { |
||||
return (Date.parse(this.options.timeframe_end) || |
||||
(this.lastDateSeen && this.lastDateSeen.clone() || |
||||
new Date()).addWeeks(1).next().sunday()); |
||||
}, |
||||
getDaysBetween: function(a, b) { |
||||
// some meat around date calculations that will be floored out again
|
||||
// later. this hopefully takes care of floating point imprecisions
|
||||
// and possible leap seconds, as we're only interested in days.
|
||||
var da = a - 1000 * 60 * 60 * 4; |
||||
var db = b - 1000 * 60 * 60 * (-4); |
||||
return Math.floor((db - da) / (1000 * 60 * 60 * 24)); |
||||
}, |
||||
includeDate: function(date) { |
||||
if (date) { |
||||
if (this.firstDateSeen == null || |
||||
date.compareTo(this.firstDateSeen) < 0) { |
||||
this.firstDateSeen = date; |
||||
} else if (this.lastDateSeen == null || |
||||
date.compareTo(this.lastDateSeen) > 0) { |
||||
this.lastDateSeen = date; |
||||
} |
||||
} |
||||
}, |
||||
adjustForPlanningElements: function() { |
||||
var timeline = this; |
||||
var tree = this.getLefthandTree(); |
||||
|
||||
// nullify potential previous dates seen. this is relevant when
|
||||
// adjusting after the addition of a planning element via modal.
|
||||
|
||||
timeline.firstDateSeen = null; |
||||
timeline.lastDateSeen = null; |
||||
|
||||
tree.iterateWithChildren(function(node) { |
||||
var data = node.getData(); |
||||
if (data.is(Timeline.PlanningElement)) { |
||||
timeline.includeDate(data.start()); |
||||
timeline.includeDate(data.end()); |
||||
} |
||||
}, { |
||||
traverseCollapsed: true |
||||
}); |
||||
|
||||
}, |
||||
getReportings: function() { |
||||
return Timeline.Reporting.all(this); |
||||
}, |
||||
getReporting: function(id) { |
||||
return this.reportings[id]; |
||||
}, |
||||
getProjects: function() { |
||||
return Timeline.Project.all(this); |
||||
}, |
||||
getProject: function(id) { |
||||
if (id === undefined) { |
||||
return this.project; |
||||
} |
||||
else return this.projects[id]; |
||||
}, |
||||
getGroupForProject: function(p) { |
||||
var i, j = 0, projects, key, group; |
||||
var groups = this.getFirstLevelGroups(); |
||||
for (j = 0; j < groups.length; j += 1) { |
||||
projects = groups[j].projects; |
||||
group = this.getProject(groups[j].id); |
||||
|
||||
for (i = 0; i < projects.length; i++) { |
||||
if (p.id === projects[i].id) { |
||||
return { |
||||
'id': group.id, |
||||
'p': group, |
||||
'number': j + 1, |
||||
'name': group.name |
||||
}; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return { |
||||
'number': 0, |
||||
'id': 0, |
||||
'name': this.i18n('timelines.filter.grouping_other') |
||||
}; |
||||
}, |
||||
|
||||
firstLevelGroups: undefined, |
||||
getFirstLevelGroups: function() { |
||||
if (this.firstLevelGroups !== undefined) { |
||||
return this.firstLevelGroups; |
||||
} |
||||
var i, selection = this.options.grouping_one_selection; |
||||
var p, groups = [], children; |
||||
if (this.isGrouping()) { |
||||
for ( i = 0; i < selection.length; i++ ) { |
||||
p = this.getProject(selection[i]); |
||||
if (p === undefined) { |
||||
// projects may have subprojects that the current user knows
|
||||
// about, but which cannot be/ were not fetched in advance due
|
||||
// to lack of rights.
|
||||
continue; |
||||
} |
||||
children = this.getSubprojectsOf([selection[i]]); |
||||
if (children.length !== 0) { |
||||
groups.push({ |
||||
projects: children, |
||||
id: selection[i] |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
|
||||
this.firstLevelGroups = groups; |
||||
return groups; |
||||
}, |
||||
getNumberOfGroups: function() { |
||||
var result = this.options.hide_other_group? 0: 1; |
||||
var groups = this.getFirstLevelGroups(); |
||||
return result + groups.length; |
||||
}, |
||||
getSubprojectsOf: function(parents) { |
||||
var projects = this.getProjects(); |
||||
var result = []; |
||||
var timeline = this; |
||||
|
||||
// if parents is not an array, turn it into one with length 1, so
|
||||
// the following each does not fail.
|
||||
if (!(parents instanceof Array)) { |
||||
parents = [parents]; |
||||
} |
||||
|
||||
var ancestorIsIn = function(project, ancestors) { |
||||
|
||||
var parent = project.getParent(); |
||||
var r = false; |
||||
if (parent !== null) { |
||||
jQuery.each(ancestors, function(i, p) { |
||||
|
||||
// make sure this is a number. when coming from the options
|
||||
// array, it might actually be an array of strings.
|
||||
if (typeof p === 'string') { |
||||
p = timeline.pnum(p); |
||||
} |
||||
|
||||
if (parent && p === parent.id) { |
||||
r = true; |
||||
} |
||||
}); |
||||
|
||||
// check rest of project tree. this might break when a project
|
||||
// in between is not visible to the current user.
|
||||
if (parent) { |
||||
r = r || ancestorIsIn(parent, ancestors); |
||||
} |
||||
} |
||||
return r; |
||||
}; |
||||
|
||||
jQuery.each(projects, function(i, e) { |
||||
if (ancestorIsIn(e, parents)) { |
||||
result.push(e); |
||||
} |
||||
}); |
||||
|
||||
return result; |
||||
}, |
||||
getProjectTypes: function() { |
||||
return Timeline.ProjectType.all(this); |
||||
}, |
||||
getProjectType: function(id) { |
||||
return this.project_types[id]; |
||||
}, |
||||
getPlanningElementTypes: function() { |
||||
return Timeline.PlanningElementType.all(this); |
||||
}, |
||||
getPlanningElementType: function(id) { |
||||
return this.planning_element_types[id]; |
||||
}, |
||||
getPlanningElements: function() { |
||||
return Timeline.PlanningElement.all(this); |
||||
}, |
||||
getPlanningElement: function(id) { |
||||
return this.planning_elements[id]; |
||||
}, |
||||
getColors: function() { |
||||
return Timeline.Color.all(this); |
||||
}, |
||||
getProjectAssociations: function() { |
||||
return Timeline.ProjectAssociation.all(this); |
||||
}, |
||||
getLefthandTree: function() { |
||||
|
||||
if (!this.lefthandTree) { |
||||
|
||||
// as long as there are no stored filters or aggregates, we only use
|
||||
// the projects as roots.
|
||||
var project = this.getProject(); |
||||
var tree = Object.create(Timeline.TreeNode); |
||||
var parent_stack = []; |
||||
|
||||
tree.setData(project); |
||||
|
||||
// there might not be any payload, due to insufficient rights and
|
||||
// the fact that some user with more rights originally created the
|
||||
// report.
|
||||
if (!project) { |
||||
// FLAG raise some flag indicating that something is
|
||||
// wrong/missing.
|
||||
return tree; |
||||
} |
||||
|
||||
var count = 1; |
||||
// for the given node, appends the given planning_elements as children,
|
||||
// recursively. every node will have the planning_element as data.
|
||||
var treeConstructor = function(node, elements) { |
||||
count += 1; |
||||
|
||||
var MAXIMUMPROJECTCOUNT = 12000; |
||||
if (count > MAXIMUMPROJECTCOUNT) { |
||||
throw I18n.t('js.timelines.tooManyProjects', {count: MAXIMUMPROJECTCOUNT}); |
||||
} |
||||
|
||||
jQuery.each(elements, function(i, e) { |
||||
parent_stack.push(node.payload); |
||||
for (var j = 0; j < parent_stack.length; j++) { |
||||
if (parent_stack[j] === e) { |
||||
parent_stack.pop(); |
||||
return; // no more recursion!
|
||||
} |
||||
} |
||||
var newNode = Object.create(Timeline.TreeNode); |
||||
newNode.setData(e); |
||||
node.appendChild(newNode); |
||||
treeConstructor(newNode, newNode.getData().getSubElements()); |
||||
parent_stack.pop(); |
||||
}); |
||||
return node; |
||||
}; |
||||
|
||||
this.lefthandTree = treeConstructor(tree, project.getSubElements()); |
||||
|
||||
this.lefthandTree.expandTo(this.options.initial_outline_expansion); |
||||
} |
||||
|
||||
return this.lefthandTree; |
||||
} |
||||
}); |
||||
|
||||
// This polyfill covers the main use case which is creating a new object
|
||||
// for which the prototype has been chosen but doesn't take the second
|
||||
// argument into account:
|
||||
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create
|
||||
if (!Object.create) { |
||||
Object.create = function(o) { |
||||
if (arguments.length > 1) { |
||||
throw new Error( |
||||
'Object.create implementation only accepts the first parameter.' |
||||
); |
||||
} |
||||
function F() {} |
||||
F.prototype = o; |
||||
return new F(); |
||||
}; |
||||
} |
||||
|
||||
|
||||
|
||||
return Timeline; |
||||
}]); |
@ -0,0 +1,76 @@ |
||||
// //-- copyright
|
||||
// // OpenProject is a project management system.
|
||||
// // Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
|
||||
// //
|
||||
// // This program is free software; you can redistribute it and/or
|
||||
// // modify it under the terms of the GNU General Public License version 3.
|
||||
// //
|
||||
// // OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// // Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// // Copyright (C) 2010-2013 the ChiliProject Team
|
||||
// //
|
||||
// // This program is free software; you can redistribute it and/or
|
||||
// // modify it under the terms of the GNU General Public License
|
||||
// // as published by the Free Software Foundation; either version 2
|
||||
// // of the License, or (at your option) any later version.
|
||||
// //
|
||||
// // This program is distributed in the hope that it will be useful,
|
||||
// // but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// // GNU General Public License for more details.
|
||||
// //
|
||||
// // You should have received a copy of the GNU General Public License
|
||||
// // along with this program; if not, write to the Free Software
|
||||
// // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
// //
|
||||
// // See doc/COPYRIGHT.rdoc for more details.
|
||||
// //++
|
||||
|
||||
// // ╭───────────────────────────────────────────────────────────────╮
|
||||
// // │ _____ _ _ _ │
|
||||
// // │ |_ _(_)_ __ ___ ___| (_)_ __ ___ ___ │
|
||||
// // │ | | | | '_ ` _ \ / _ \ | | '_ \ / _ \/ __| │
|
||||
// // │ | | | | | | | | | __/ | | | | | __/\__ \ │
|
||||
// // │ |_| |_|_| |_| |_|\___|_|_|_| |_|\___||___/ │
|
||||
// // ├───────────────────────────────────────────────────────────────┤
|
||||
// // │ Javascript library that fetches and plots timelines for the │
|
||||
// // │ OpenProject timelines module. │
|
||||
// // ╰───────────────────────────────────────────────────────────────╯
|
||||
|
||||
timelinesApp.factory('Constants', [function() { |
||||
|
||||
Constants = { |
||||
//constants and defaults
|
||||
LOAD_ERROR_TIMEOUT: 60000, |
||||
DISPLAY_ERROR_DELAY: 2000, |
||||
PROJECT_ID_BLOCK_SIZE: 100, |
||||
USER_ATTRIBUTES: { |
||||
PROJECT: ["responsible_id"], |
||||
PLANNING_ELEMENT: ["responsible_id", "assigned_to_id"] |
||||
}, |
||||
|
||||
defaults: { |
||||
artificial_load_delay: 0, // no delay
|
||||
columns: [], |
||||
exclude_own_planning_elements: false, |
||||
exclude_reporters: false, |
||||
api_prefix: '/api/v2', |
||||
hide_other_group: false, |
||||
hide_tree_root: false, |
||||
i18n: {}, // undefined would be bad.
|
||||
initial_outline_expansion: 0, // aggregations only
|
||||
project_prefix: '/projects', |
||||
planning_element_prefix: '', |
||||
ui_root: jQuery('#timeline'), |
||||
url_prefix: '' // empty prefix so it is not undefined.
|
||||
}, |
||||
|
||||
ajax_defaults: { |
||||
cache: false, |
||||
context: this, |
||||
dataType: 'json' |
||||
}, |
||||
}; |
||||
|
||||
return Constants; |
||||
}]); |
File diff suppressed because it is too large
Load Diff
@ -1,750 +0,0 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
// ╭───────────────────────────────────────────────────────────────╮
|
||||
// │ _____ _ _ _ │
|
||||
// │ |_ _(_)_ __ ___ ___| (_)_ __ ___ ___ │
|
||||
// │ | | | | '_ ` _ \ / _ \ | | '_ \ / _ \/ __| │
|
||||
// │ | | | | | | | | | __/ | | | | | __/\__ \ │
|
||||
// │ |_| |_|_| |_| |_|\___|_|_|_| |_|\___||___/ │
|
||||
// ├───────────────────────────────────────────────────────────────┤
|
||||
// │ Javascript library that fetches and plots timelines for the │
|
||||
// │ OpenProject timelines module. │
|
||||
// ╰───────────────────────────────────────────────────────────────╯
|
||||
|
||||
//= require timelines/FilterQueryStringBuilder
|
||||
|
||||
//= require timelines/TreeNode
|
||||
|
||||
//= require timelines/constants
|
||||
//= require timelines/ui
|
||||
|
||||
|
||||
//= require timelines/model/Project
|
||||
//= require timelines/model/PlanningElement
|
||||
//= require timelines/model/HistoricalPlanningElement
|
||||
|
||||
//= require timelines/model/ProjectAssociation
|
||||
//= require timelines/model/Reporting
|
||||
//= require timelines/model/ProjectType
|
||||
//= require timelines/model/Color
|
||||
//= require timelines/model/Status
|
||||
//= require timelines/model/PlanningElementType
|
||||
//= require timelines/model/User
|
||||
|
||||
|
||||
// stricter than default
|
||||
/*jshint undef:true, |
||||
eqeqeq:true, |
||||
forin:true, |
||||
immed:true, |
||||
latedef:true, |
||||
trailing: true |
||||
*/ |
||||
|
||||
// looser than default
|
||||
/*jshint eqnull:true */ |
||||
|
||||
// environment and other global vars
|
||||
/*jshint browser:true, devel:true*/ |
||||
/*global jQuery:false, Raphael:false, Timeline:true*/ |
||||
|
||||
if (typeof Timeline === "undefined") { |
||||
Timeline = {}; |
||||
} |
||||
|
||||
//startup
|
||||
jQuery.extend(Timeline, { |
||||
instances: [], |
||||
get: function(n) { |
||||
if (typeof n !== "number") { |
||||
n = 0; |
||||
} |
||||
return this.instances[n]; |
||||
}, |
||||
isInstance: function(n) { |
||||
return (n === undefined) ? |
||||
Timeline.instances.indexOf(this) : |
||||
this === Timeline.get(n); |
||||
}, |
||||
isGrouping: function() { |
||||
if ((this.options.grouping_one_enabled === 'yes' && |
||||
this.options.grouping_one_selection !== undefined) || |
||||
(this.options.grouping_two_enabled === 'yes' && |
||||
this.options.grouping_two_selection !== undefined)) { |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
}, |
||||
isComparing: function() { |
||||
return ((this.options.comparison !== undefined) && |
||||
(this.options.comparison !== 'none')); |
||||
}, |
||||
comparisonCurrentTime: function() { |
||||
var value; |
||||
if (!this.isComparing()) { |
||||
return undefined; |
||||
} |
||||
if (this.options.comparison === 'historical') { |
||||
value = this.options.compare_to_historical_two; |
||||
} |
||||
else { |
||||
// default is no (undefined) current time, which corresponds to today.
|
||||
return undefined; |
||||
} |
||||
return +Date.parse(value) / 1000; |
||||
}, |
||||
calculateTimeFilter: function () { |
||||
if (!this.frameSet) { |
||||
if (this.options.planning_element_time === "absolute") { |
||||
this.frameStart = Date.parse(this.options.planning_element_time_absolute_one); |
||||
this.frameEnd = Date.parse(this.options.planning_element_time_absolute_two); |
||||
} else if (this.options.planning_element_time === "relative") { |
||||
var startR = parseInt(this.options.planning_element_time_relative_one, 10); |
||||
var endR = parseInt(this.options.planning_element_time_relative_two, 10); |
||||
if (!isNaN(startR)) { |
||||
this.frameStart = Date.now(); |
||||
|
||||
switch (this.options.planning_element_time_relative_one_unit[0]) { |
||||
case "0": |
||||
this.frameStart.add(-1 * startR).days(); |
||||
break; |
||||
case "1": |
||||
this.frameStart.add(-1 * startR).weeks(); |
||||
break; |
||||
case "2": |
||||
this.frameStart.add(-1 * startR).months(); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (!isNaN(endR)) { |
||||
this.frameEnd = Date.now(); |
||||
|
||||
switch (this.options.planning_element_time_relative_two_unit[0]) { |
||||
case "0": |
||||
this.frameEnd.add(endR).days(); |
||||
break; |
||||
case "1": |
||||
this.frameEnd.add(endR).weeks(); |
||||
break; |
||||
case "2": |
||||
this.frameEnd.add(endR).months(); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
this.frameSet = true; |
||||
} |
||||
}, |
||||
inTimeFilter: function (start, end) { |
||||
this.calculateTimeFilter(); |
||||
|
||||
if (!start && !end) { |
||||
return false; |
||||
} |
||||
|
||||
if (!start) { |
||||
start = end; |
||||
} |
||||
|
||||
if (!end) { |
||||
end = start; |
||||
} |
||||
|
||||
if (this.frameStart) { |
||||
if (start < this.frameStart && end < this.frameStart) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
if (this.frameEnd) { |
||||
if (start > this.frameEnd && end > this.frameEnd) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
}, |
||||
verticalPlanningElementIds: function() { |
||||
|
||||
return this.options.vertical_planning_elements ? |
||||
jQuery.map( |
||||
this.options.vertical_planning_elements.split(/\,/), |
||||
function(a) { |
||||
try { |
||||
return parseInt(a.match(/\s*\*?(\d*)\s*/)[1], 10); |
||||
} catch (e) { |
||||
return; |
||||
} |
||||
} |
||||
) : []; |
||||
}, |
||||
comparisonTarget: function() { |
||||
var result, value, unit; |
||||
if (!this.isComparing()) { |
||||
return undefined; |
||||
} |
||||
switch (this.options.comparison) { |
||||
case 'relative': |
||||
result = new Date(); |
||||
value = Timeline.pnum(this.options.compare_to_relative); |
||||
unit = Timeline.pnum(this.options.compare_to_relative_unit[0]); |
||||
switch (unit) { |
||||
case 0: |
||||
return Math.floor(result.add(-value).days() / 1000); |
||||
case 1: |
||||
return Math.floor(result.add(-value).weeks() / 1000); |
||||
case 2: |
||||
return Math.floor(result.add(-value).months() / 1000); |
||||
default: |
||||
return this.die(this.i18n('timelines.errors.report_comparison')); |
||||
} |
||||
break; // to please jslint
|
||||
case 'absolute': |
||||
value = this.options.compare_to_absolute; |
||||
break; |
||||
case 'historical': |
||||
value = this.options.compare_to_historical_one; |
||||
break; |
||||
default: |
||||
return this.die(this.i18n('timelines.errors.report_comparison')); |
||||
} |
||||
return +Date.parse(value)/1000; |
||||
}, |
||||
create: function(options) { |
||||
// configuration
|
||||
|
||||
if (!options) { |
||||
throw new Error('No configuration options given'); |
||||
} |
||||
|
||||
options = jQuery.extend({}, this.defaults, options); |
||||
|
||||
if (options.username) { |
||||
this.ajax_defaults.username = options.username; |
||||
} |
||||
if (options.password) { |
||||
this.ajax_defaults.password = options.password; |
||||
} |
||||
if (options.api_key) { |
||||
this.ajax_defaults.headers = { |
||||
'X-ChiliProject-API-Key': options.api_key, |
||||
'X-OpenProject-API-Key': options.api_key, |
||||
'X-Redmine-API-Key': options.api_key |
||||
}; |
||||
} |
||||
|
||||
this.options = options; |
||||
|
||||
// we're hiding the root if there is a grouping.
|
||||
this.options.hide_tree_root = this.isGrouping(); |
||||
|
||||
var timeline = Object.create(Timeline); |
||||
|
||||
// some private fields.
|
||||
timeline.listeners = []; |
||||
timeline.data = {}; |
||||
|
||||
Timeline.instances.push(timeline); |
||||
return timeline; |
||||
}, |
||||
registerTimelineContainer: function(uiRoot) { |
||||
this.uiRoot = uiRoot; |
||||
this.registerDrawPaper(); |
||||
}, |
||||
checkPrerequisites: function() { |
||||
if (jQuery === undefined) { |
||||
throw new Error('jQuery seems to be missing (jQuery is undefined)'); |
||||
} else if (jQuery().slider === undefined) { |
||||
throw new Error('jQuery UI seems to be missing (jQuery().slider is undefined)'); |
||||
} else if ((1).month === undefined) { |
||||
throw new Error('date.js seems to be missing ((1).month is undefined)'); |
||||
} else if (Raphael === undefined) { |
||||
throw new Error('Raphael seems to be missing (Raphael is undefined)'); |
||||
} |
||||
return true; |
||||
}, |
||||
reload: function() { |
||||
delete this.lefthandTree; |
||||
|
||||
var timelineLoader = this.provideTimelineLoader(); |
||||
|
||||
jQuery(timelineLoader).on('complete', jQuery.proxy(function (e, data) { |
||||
|
||||
jQuery.extend(this, data); |
||||
jQuery(this).trigger('dataReLoaded'); |
||||
|
||||
if (this.isGrouping() && this.options.grouping_two_enabled) { |
||||
this.secondLevelGroupingAdjustments(); |
||||
} |
||||
|
||||
this.adjustForPlanningElements(); |
||||
|
||||
this.rebuildAll(); |
||||
|
||||
}, this)); |
||||
|
||||
timelineLoader.load(); |
||||
}, |
||||
provideTimelineLoader: function() { |
||||
return new Timeline.TimelineLoader( |
||||
this, |
||||
{ |
||||
api_prefix : this.options.api_prefix, |
||||
url_prefix : this.options.url_prefix, |
||||
project_prefix : this.options.project_prefix, |
||||
planning_element_prefix : this.options.planning_element_prefix, |
||||
project_id : this.options.project_id, |
||||
project_types : this.options.project_types, |
||||
project_statuses : this.options.project_status, |
||||
project_responsibles : this.options.project_responsibles, |
||||
project_parents : this.options.parents, |
||||
planning_element_types : this.options.planning_element_types, |
||||
planning_element_responsibles : this.options.planning_element_responsibles, |
||||
planning_element_status : this.options.planning_element_status, |
||||
grouping_one : (this.options.grouping_one_enabled ? this.options.grouping_one_selection : undefined), |
||||
grouping_two : (this.options.grouping_two_enabled ? this.options.grouping_two_selection : undefined), |
||||
ajax_defaults : this.ajax_defaults, |
||||
current_time : this.comparisonCurrentTime(), |
||||
target_time : this.comparisonTarget(), |
||||
include_planning_elements : this.verticalPlanningElementIds() |
||||
} |
||||
); |
||||
}, |
||||
getTimelineLoaderOptions: function() { |
||||
return { |
||||
api_prefix : this.options.api_prefix, |
||||
url_prefix : this.options.url_prefix, |
||||
project_prefix : this.options.project_prefix, |
||||
planning_element_prefix : this.options.planning_element_prefix, |
||||
project_id : this.options.project_id, |
||||
project_types : this.options.project_types, |
||||
project_statuses : this.options.project_status, |
||||
project_responsibles : this.options.project_responsibles, |
||||
project_parents : this.options.parents, |
||||
planning_element_types : this.options.planning_element_types, |
||||
planning_element_responsibles : this.options.planning_element_responsibles, |
||||
planning_element_status : this.options.planning_element_status, |
||||
grouping_one : (this.options.grouping_one_enabled ? this.options.grouping_one_selection : undefined), |
||||
grouping_two : (this.options.grouping_two_enabled ? this.options.grouping_two_selection : undefined), |
||||
ajax_defaults : this.ajax_defaults, |
||||
current_time : this.comparisonCurrentTime(), |
||||
target_time : this.comparisonTarget(), |
||||
include_planning_elements : this.verticalPlanningElementIds() |
||||
} |
||||
}, |
||||
defer: function(action, delay) { |
||||
var timeline = this; |
||||
var result; |
||||
if (delay === undefined) { |
||||
delay = 0; |
||||
} |
||||
result = window.setTimeout(function() { |
||||
try { |
||||
action.call(); |
||||
} catch(e) { |
||||
timeline.die(e); |
||||
} |
||||
}, 0); |
||||
return result; |
||||
}, |
||||
die: function(error, classes) { |
||||
var message = (typeof error === 'string') ? error : |
||||
this.i18n('timelines.errors.report_epicfail'); // + '<br>' + error.message;
|
||||
classes = classes || 'flash error'; |
||||
|
||||
this.warn(message, classes); |
||||
|
||||
// assume this won't happen anymore.
|
||||
this.onLoadComplete = function() {}; |
||||
|
||||
if (console && console.log) { |
||||
console.log(error.stack); |
||||
} |
||||
|
||||
throw error; |
||||
}, |
||||
warn: function(message, classes) { |
||||
var root = this.getUiRoot(); |
||||
|
||||
window.setTimeout(function() { |
||||
|
||||
// generate and display the error message.
|
||||
var warning = jQuery('<div class="' + classes + '">' + message + '</div>'); |
||||
root.empty().append(warning); |
||||
|
||||
}, Timeline.DISPLAY_ERROR_DELAY); |
||||
}, |
||||
onLoadComplete: function() { |
||||
// everything here should be wrapped in try/catch, to never
|
||||
var tree; |
||||
try { |
||||
window.clearTimeout(this.safetyHook); |
||||
|
||||
if (this.isGrouping() && this.options.grouping_two_enabled) { |
||||
this.secondLevelGroupingAdjustments(); |
||||
} |
||||
|
||||
tree = this.getLefthandTree(); |
||||
if (tree.containsPlanningElements() || tree.containsProjects()) { |
||||
this.adjustForPlanningElements(); |
||||
this.completeUI(); |
||||
} else { |
||||
this.warn(this.i18n('label_no_data'), 'warning'); |
||||
} |
||||
} catch (e) { |
||||
this.die(e); |
||||
} |
||||
}, |
||||
secondLevelGroupingAdjustments : function () { |
||||
var grouping = jQuery.map(this.options.grouping_two_selection || [], Timeline.pnum); |
||||
var root = this.getProject(); |
||||
var associations = Timeline.ProjectAssociation.all(this); |
||||
var listToRemove = []; |
||||
|
||||
// for all projects on the first level
|
||||
jQuery.each(root.getReporters(), function (i, e) { |
||||
|
||||
// find all projects that are associated
|
||||
jQuery.each(associations, function (j, a) { |
||||
|
||||
if (a.involves(e)) { |
||||
var other = a.getOther(e); |
||||
if (typeof other.getProjectType === "function") { |
||||
var pt = other.getProjectType(); |
||||
var type = pt !== null ? pt.id : -1; |
||||
var relevant = false; |
||||
|
||||
jQuery.each(grouping, function(k, l) { |
||||
if (l === type) { |
||||
relevant = true; |
||||
} |
||||
}); |
||||
|
||||
if (relevant) { |
||||
|
||||
// add the other project as a simulated reporter to the current one.
|
||||
e.addReporter(other); |
||||
other.hasSecondLevelGroupingAdjustment = true; |
||||
// remove the project from the root level of the report.
|
||||
listToRemove.push(other); |
||||
|
||||
} |
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
// remove all children of root that we couldn't remove while still iterating.
|
||||
jQuery.each(listToRemove, function(i, e) { |
||||
root.removeReporter(e); |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
// ╭───────────────────────────────────────────────────────────────────╮
|
||||
// │ Defaults and random accessors │
|
||||
// ╰───────────────────────────────────────────────────────────────────╯
|
||||
jQuery.extend(Timeline, { |
||||
// defines how many levels are expanded when a tree is created, zero
|
||||
// corresponds to the root being collapsed.
|
||||
firstDateSeen: null, |
||||
lastDateSeen: null, |
||||
|
||||
getBeginning: function() { |
||||
return (Date.parse(this.options.timeframe_start) || |
||||
(this.firstDateSeen && this.firstDateSeen.clone() || |
||||
new Date()).last().monday()); |
||||
}, |
||||
getEnd: function() { |
||||
return (Date.parse(this.options.timeframe_end) || |
||||
(this.lastDateSeen && this.lastDateSeen.clone() || |
||||
new Date()).addWeeks(1).next().sunday()); |
||||
}, |
||||
getDaysBetween: function(a, b) { |
||||
// some meat around date calculations that will be floored out again
|
||||
// later. this hopefully takes care of floating point imprecisions
|
||||
// and possible leap seconds, as we're only interested in days.
|
||||
var da = a - 1000 * 60 * 60 * 4; |
||||
var db = b - 1000 * 60 * 60 * (-4); |
||||
return Math.floor((db - da) / (1000 * 60 * 60 * 24)); |
||||
}, |
||||
includeDate: function(date) { |
||||
if (date) { |
||||
if (this.firstDateSeen == null || |
||||
date.compareTo(this.firstDateSeen) < 0) { |
||||
this.firstDateSeen = date; |
||||
} else if (this.lastDateSeen == null || |
||||
date.compareTo(this.lastDateSeen) > 0) { |
||||
this.lastDateSeen = date; |
||||
} |
||||
} |
||||
}, |
||||
adjustForPlanningElements: function() { |
||||
var timeline = this; |
||||
var tree = this.getLefthandTree(); |
||||
|
||||
// nullify potential previous dates seen. this is relevant when
|
||||
// adjusting after the addition of a planning element via modal.
|
||||
|
||||
timeline.firstDateSeen = null; |
||||
timeline.lastDateSeen = null; |
||||
|
||||
tree.iterateWithChildren(function(node) { |
||||
var data = node.getData(); |
||||
if (data.is(Timeline.PlanningElement)) { |
||||
timeline.includeDate(data.start()); |
||||
timeline.includeDate(data.end()); |
||||
} |
||||
}, { |
||||
traverseCollapsed: true |
||||
}); |
||||
|
||||
}, |
||||
getReportings: function() { |
||||
return Timeline.Reporting.all(this); |
||||
}, |
||||
getReporting: function(id) { |
||||
return this.reportings[id]; |
||||
}, |
||||
getProjects: function() { |
||||
return Timeline.Project.all(this); |
||||
}, |
||||
getProject: function(id) { |
||||
if (id === undefined) { |
||||
return this.project; |
||||
} |
||||
else return this.projects[id]; |
||||
}, |
||||
getGroupForProject: function(p) { |
||||
var i, j = 0, projects, key, group; |
||||
var groups = this.getFirstLevelGroups(); |
||||
for (j = 0; j < groups.length; j += 1) { |
||||
projects = groups[j].projects; |
||||
group = this.getProject(groups[j].id); |
||||
|
||||
for (i = 0; i < projects.length; i++) { |
||||
if (p.id === projects[i].id) { |
||||
return { |
||||
'id': group.id, |
||||
'p': group, |
||||
'number': j + 1, |
||||
'name': group.name |
||||
}; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return { |
||||
'number': 0, |
||||
'id': 0, |
||||
'name': this.i18n('timelines.filter.grouping_other') |
||||
}; |
||||
}, |
||||
|
||||
firstLevelGroups: undefined, |
||||
getFirstLevelGroups: function() { |
||||
if (this.firstLevelGroups !== undefined) { |
||||
return this.firstLevelGroups; |
||||
} |
||||
var i, selection = this.options.grouping_one_selection; |
||||
var p, groups = [], children; |
||||
if (this.isGrouping()) { |
||||
for ( i = 0; i < selection.length; i++ ) { |
||||
p = this.getProject(selection[i]); |
||||
if (p === undefined) { |
||||
// projects may have subprojects that the current user knows
|
||||
// about, but which cannot be/ were not fetched in advance due
|
||||
// to lack of rights.
|
||||
continue; |
||||
} |
||||
children = this.getSubprojectsOf([selection[i]]); |
||||
if (children.length !== 0) { |
||||
groups.push({ |
||||
projects: children, |
||||
id: selection[i] |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
|
||||
this.firstLevelGroups = groups; |
||||
return groups; |
||||
}, |
||||
getNumberOfGroups: function() { |
||||
var result = this.options.hide_other_group? 0: 1; |
||||
var groups = this.getFirstLevelGroups(); |
||||
return result + groups.length; |
||||
}, |
||||
getSubprojectsOf: function(parents) { |
||||
var projects = this.getProjects(); |
||||
var result = []; |
||||
var timeline = this; |
||||
|
||||
// if parents is not an array, turn it into one with length 1, so
|
||||
// the following each does not fail.
|
||||
if (!(parents instanceof Array)) { |
||||
parents = [parents]; |
||||
} |
||||
|
||||
var ancestorIsIn = function(project, ancestors) { |
||||
|
||||
var parent = project.getParent(); |
||||
var r = false; |
||||
if (parent !== null) { |
||||
jQuery.each(ancestors, function(i, p) { |
||||
|
||||
// make sure this is a number. when coming from the options
|
||||
// array, it might actually be an array of strings.
|
||||
if (typeof p === 'string') { |
||||
p = timeline.pnum(p); |
||||
} |
||||
|
||||
if (parent && p === parent.id) { |
||||
r = true; |
||||
} |
||||
}); |
||||
|
||||
// check rest of project tree. this might break when a project
|
||||
// in between is not visible to the current user.
|
||||
if (parent) { |
||||
r = r || ancestorIsIn(parent, ancestors); |
||||
} |
||||
} |
||||
return r; |
||||
}; |
||||
|
||||
jQuery.each(projects, function(i, e) { |
||||
if (ancestorIsIn(e, parents)) { |
||||
result.push(e); |
||||
} |
||||
}); |
||||
|
||||
return result; |
||||
}, |
||||
getProjectTypes: function() { |
||||
return Timeline.ProjectType.all(this); |
||||
}, |
||||
getProjectType: function(id) { |
||||
return this.project_types[id]; |
||||
}, |
||||
getPlanningElementTypes: function() { |
||||
return Timeline.PlanningElementType.all(this); |
||||
}, |
||||
getPlanningElementType: function(id) { |
||||
return this.planning_element_types[id]; |
||||
}, |
||||
getPlanningElements: function() { |
||||
return Timeline.PlanningElement.all(this); |
||||
}, |
||||
getPlanningElement: function(id) { |
||||
return this.planning_elements[id]; |
||||
}, |
||||
getColors: function() { |
||||
return Timeline.Color.all(this); |
||||
}, |
||||
getProjectAssociations: function() { |
||||
return Timeline.ProjectAssociation.all(this); |
||||
}, |
||||
getLefthandTree: function() { |
||||
|
||||
if (!this.lefthandTree) { |
||||
|
||||
// as long as there are no stored filters or aggregates, we only use
|
||||
// the projects as roots.
|
||||
var project = this.getProject(); |
||||
var tree = Object.create(Timeline.TreeNode); |
||||
var parent_stack = []; |
||||
|
||||
tree.setData(project); |
||||
|
||||
// there might not be any payload, due to insufficient rights and
|
||||
// the fact that some user with more rights originally created the
|
||||
// report.
|
||||
if (!project) { |
||||
// FLAG raise some flag indicating that something is
|
||||
// wrong/missing.
|
||||
return tree; |
||||
} |
||||
|
||||
var count = 1; |
||||
// for the given node, appends the given planning_elements as children,
|
||||
// recursively. every node will have the planning_element as data.
|
||||
var treeConstructor = function(node, elements) { |
||||
count += 1; |
||||
|
||||
var MAXIMUMPROJECTCOUNT = 12000; |
||||
if (count > MAXIMUMPROJECTCOUNT) { |
||||
throw I18n.t('js.timelines.tooManyProjects', {count: MAXIMUMPROJECTCOUNT}); |
||||
} |
||||
|
||||
jQuery.each(elements, function(i, e) { |
||||
parent_stack.push(node.payload); |
||||
for (var j = 0; j < parent_stack.length; j++) { |
||||
if (parent_stack[j] === e) { |
||||
parent_stack.pop(); |
||||
return; // no more recursion!
|
||||
} |
||||
} |
||||
var newNode = Object.create(Timeline.TreeNode); |
||||
newNode.setData(e); |
||||
node.appendChild(newNode); |
||||
treeConstructor(newNode, newNode.getData().getSubElements()); |
||||
parent_stack.pop(); |
||||
}); |
||||
return node; |
||||
}; |
||||
|
||||
this.lefthandTree = treeConstructor(tree, project.getSubElements()); |
||||
|
||||
this.lefthandTree.expandTo(this.options.initial_outline_expansion); |
||||
} |
||||
|
||||
return this.lefthandTree; |
||||
} |
||||
}); |
||||
|
||||
// This polyfill covers the main use case which is creating a new object
|
||||
// for which the prototype has been chosen but doesn't take the second
|
||||
// argument into account:
|
||||
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/create
|
||||
if (!Object.create) { |
||||
Object.create = function(o) { |
||||
if (arguments.length > 1) { |
||||
throw new Error( |
||||
'Object.create implementation only accepts the first parameter.' |
||||
); |
||||
} |
||||
function F() {} |
||||
F.prototype = o; |
||||
return new F(); |
||||
}; |
||||
} |
@ -1,91 +0,0 @@ |
||||
//-- copyright
|
||||
// OpenProject is a project management system.
|
||||
// Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License version 3.
|
||||
//
|
||||
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
// Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
// Copyright (C) 2010-2013 the ChiliProject Team
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// See doc/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
// ╭───────────────────────────────────────────────────────────────╮
|
||||
// │ _____ _ _ _ │
|
||||
// │ |_ _(_)_ __ ___ ___| (_)_ __ ___ ___ │
|
||||
// │ | | | | '_ ` _ \ / _ \ | | '_ \ / _ \/ __| │
|
||||
// │ | | | | | | | | | __/ | | | | | __/\__ \ │
|
||||
// │ |_| |_|_| |_| |_|\___|_|_|_| |_|\___||___/ │
|
||||
// ├───────────────────────────────────────────────────────────────┤
|
||||
// │ Javascript library that fetches and plots timelines for the │
|
||||
// │ OpenProject timelines module. │
|
||||
// ╰───────────────────────────────────────────────────────────────╯
|
||||
|
||||
// stricter than default
|
||||
/*jshint undef:true, |
||||
eqeqeq:true, |
||||
forin:true, |
||||
immed:true, |
||||
latedef:true, |
||||
trailing: true |
||||
*/ |
||||
|
||||
// looser than default
|
||||
/*jshint eqnull:true */ |
||||
|
||||
// environment and other global vars
|
||||
/*jshint browser:true, devel:true*/ |
||||
/*global jQuery:false, Raphael:false, Timeline:true*/ |
||||
|
||||
if (typeof Timeline === "undefined") { |
||||
Timeline = {}; |
||||
} |
||||
|
||||
//constants and defaults
|
||||
jQuery.extend(Timeline, { |
||||
LOAD_ERROR_TIMEOUT: 60000, |
||||
DISPLAY_ERROR_DELAY: 2000, |
||||
PROJECT_ID_BLOCK_SIZE: 100, |
||||
USER_ATTRIBUTES: { |
||||
PROJECT: ["responsible_id"], |
||||
PLANNING_ELEMENT: ["responsible_id", "assigned_to_id"] |
||||
}, |
||||
|
||||
defaults: { |
||||
artificial_load_delay: 0, // no delay
|
||||
columns: [], |
||||
exclude_own_planning_elements: false, |
||||
exclude_reporters: false, |
||||
api_prefix: '/api/v2', |
||||
hide_other_group: false, |
||||
hide_tree_root: false, |
||||
i18n: {}, // undefined would be bad.
|
||||
initial_outline_expansion: 0, // aggregations only
|
||||
project_prefix: '/projects', |
||||
planning_element_prefix: '', |
||||
ui_root: jQuery('#timeline'), |
||||
url_prefix: '' // empty prefix so it is not undefined.
|
||||
}, |
||||
|
||||
ajax_defaults: { |
||||
cache: false, |
||||
context: this, |
||||
dataType: 'json' |
||||
}, |
||||
}); |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue