【问题标题】:Rally custom Cycle/Lead Time data via Lookback API通过 Lookback API 收集自定义周期/交货时间数据
【发布时间】:2013-10-08 23:31:13
【问题描述】:

我正在尝试使用 JavaScript 中的 REST API 为表格和图表视图生成自定义(和高度精细的)周期/提前期数据。

我的故事和缺陷上有一个自定义(非必需)字段,c_KanbanStatus 具有以下可能的值 [null、“Kickoff”、“PO”、“Creative”、“Team Backlog”、“Coding”、“验收测试”、“接受”]。

我最近才添加了这个自定义字段,所以我的很多故事都没有这个字段(或者不是“天生”有它)。

我的想法是这样的:

  • 对每个看板状态转换执行回溯查询
  • 按 ObjectID 聚合
  • 对于每个 ObjectID,对于每个状态,计算对象进入该状态与对象进入后续状态之间的时间增量。

这是我的代码的摘录:

    var kanbanStates =
    [
        "Kickoff",
        "PO",
        "Creative",
        "Team Backlog",
        "Coding",
        "Acceptance Testing"
    ];

    var username = "**************";
    var password = "**************";

    var deferreds = [];
    for(var i = 0; i < kanbanStates.length; i++)
    {
        var find =
        {
            _ProjectHierarchy: ***************,
            "_PreviousValues.c_KanbanStatus": { $lt: kanbanStates[i] },
            c_KanbanStatus: kanbanStates[i]
        };

        var config =
        {
            url:"https://rally1.rallydev.com/analytics/v2.0/service/rally/workspace/********/artifact/snapshot/query.js?find=" + JSON.stringify(find) + "&fields=true&pagesize=999999",
            dataType: "jsonp",
            jsonp: "jsonp",
            contentType: "application/json",
            beforeSend: function(xhr)
            {
                xhr.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
            }
        };

        deferreds.push($.ajax(config));
    }

    var aggregateResultsByObjectID = function(results)
    {
        var resultsByItemID = {};
        for(var i = 0; i < results.length; i++)
        {
            if(!results[i][0].Results.c_KanbanStatus === kanbanStates[i])
                throw "States don't match!";

            for(var j = 0; j < results[i][0].Results.length; j++)
            {
                var itemID = results[i][0].Results[j].ObjectID;

                if(!resultsByItemID.hasOwnProperty(itemID))
                {
                    resultsByItemID[itemID] =
                    {
                        creationDate: results[i][0].Results[j].CreationDate,
                        name: results[i][0].Results[j].Name,
                        states: [],
                        results: []
                    };
                } 

                resultsByItemID[itemID].results.push(results[i][0].Results[j]);
                resultsByItemID[itemID].states.push(results[i][0].Results[j].c_KanbanStatus);
            }
        }
        return resultsByItemID;
    };

    $.when.apply($, deferreds).done(function()
    {
        var resultsByItemID = aggregateResultsByObjectID(arguments);
        console.log(resultsByItemID);
    });

这个查询的问题是我为每个状态的每个 ObjectID 返回多个结果,即使我指定我只想要那些具有不同 c_KanbanStatus 字段的快照。当我检查结果时,对于相同的 ObjectID 和相同的组合(如果 c_KanbanStatus 和 _PreviousValues.c_KanbanStatus),我得到了很多快照,每个快照都有一些其他字段编辑。

例如,对于同一个ObjectID,我得到这两个结果:

_PreviousValues:
{
    _User: 10301773174
    c_KanbanStatus: null
},
c_KanbanStatus: "Coding"

_PreviousValues:
{
    ScheduleState: 10148772688
    _User: 10148977759
},
c_KanbanStatus: "Coding"

我期待第一个结果(从没有设置 c_KanbanStatus 字段到“编码”时的快照)。第二个结果似乎暗示它从根本没有 c_KanbanStatus 字段变为“编码”,但为什么呢?

我感觉我在 Lookback API 中遗漏了一些东西。请帮我理解!

【问题讨论】:

标签: rally appsdk2 lookbackapi


【解决方案1】:

预计每个状态都会返回多个结果。每次修改故事时都会创建快照。假设有四个 c_Kanban 状态:backlogin-progressdonereleased。继续这个例子,我将一个故事移动到进行中列并设置故事的PlanEstimate,然后阻塞该故事下的任务,解除阻塞任务,将任务的状态设置为已完成,最后将故事移动到看板上的完成列木板。所有这些更改都会在c_Kanban === 'in-progress'时创建独特的快照

我在这个github repo 中有一个应用程序,它构建了一个网格,其中包含一个故事在每个 c_Kanban 状态中花费的时间。注意Blocked: true 在一个快照中,Blocked: false 在另一个快照中,而c_Kanban: "in-progress"

【讨论】:

  • 非常感谢您的意见,非常感谢!但是,我相信你错过了我的部分解释。我了解您所展示的内容,并且您的示例预计会返回多个快照,因为您只查询 c_Kanban === "in-progress",因此在项目仍然存在时进行的任何更改(阻止、名称、描述等)在“进行中”将触发这样的快照。但是,在我的特定情况下,我也在查询 _PreviousValue 对象,并且应该只返回一个项目(从“进行​​中”到“测试”的转换只能发生一次)。
  • 尝试添加此过滤器:{ property : '_PreviousValues.c_Kanban', operator : 'exists', value: true } 当我将此过滤器添加到我的过滤器数组时,每个状态只能获得一个快照。
  • 谢谢!我正在尝试一种不同的方法,但也会尝试使用该过滤器。
【解决方案2】:

我一直在想办法解决这个问题,这就是我想出的。它需要更多的调整来处理极端情况,但它接近我所需要的。我将继续开发并为此创建一个 GitHub 存储库,以防任何人使用它。

编辑:GitHub 存储库:https://github.com/bfanti/RallyCustomCycleTimeApp

<!DOCTYPE html>
<html>
<head>
    <title>My App</title>

    <!--App information-->
    <meta name="Name" content="App: Custom Cycle Time Table"/>
    <meta name="Version" content="1.0"/>
    <meta name="Vendor" content=""/>

    <!--Include SDK-->
    <script type="text/javascript" src="/apps/2.0rc1/sdk.js"></script>

    <!--App code-->
    <script type="text/javascript">
        var kanbanStatuses =
        [
            "New Feature",
            "Kickoff",
            "PO",
            "Creative",
            "Team Backlog",
            "Coding",
            "Acceptance Testing"
        ];

        var shirtSizeLUT =
        {
            3: "S",
            5: "M",
            8: "L"
        };

        Rally.onReady(function()
        {
            Ext.define("CustomApp",
            {
                extend: "Rally.app.App",
                componentCls: "app",

                launch: function()
                {
                    var self = this;

                    Ext.create("Rally.data.lookback.SnapshotStore",
                    {
                        fetch   : [ "_UnformattedID", "_TypeHierarchy", "Name", "PlanEstimate", "c_KanbanStatus", "ScheduleState" ],
                        hydrate : [ "c_KanbanStatus", "ScheduleState" ],
                        filters :
                        [
                            {
                                property : "_ProjectHierarchy",
                                value    : ***********
                            },
                            {
                                property: "_TypeHierarchy",
                                value: { $nin: [ -51009, -51012, -51031, -51078 ] }
                            },
                            {
                                property: "_ValidFrom",
                                value: { $gt: "2013-09-13" }
                            }
                        ],
                        sorters :
                        [
                            {
                                property  : "_ValidTo",
                                direction : "ASC"
                            }
                        ]
                    }).load(
                    {
                        params:
                        {
                            compress: false,
                            removeUnauthorizedSnapshots: true
                        },
                        callback : function(records, operation, success)
                        {
                            var aggregateCycleTimes = [];

                            var allObjectIDs = {};
                            Ext.Array.each(records, function(record) { allObjectIDs[record.get("ObjectID")] = record.get("ObjectID"); });

                            for(var storyIndex = 0; storyIndex < Object.keys(allObjectIDs).length; storyIndex++)
                            {
                                (function()
                                {
                                    var currentObjectID = parseInt(Object.keys(allObjectIDs)[storyIndex], 10);
                                    var recordsByStory = Ext.Array.filter(records, function(record) { return record.get("ObjectID") === currentObjectID; });

                                    var currentStateOfStory = "";
                                    var currentStateOfStorySnapshot = Ext.Array.filter(recordsByStory, function(record) { return record.get("_ValidTo").indexOf("9999") !== -1; })[0];
                                    if(currentStateOfStorySnapshot)
                                        currentStateOfStory = currentStateOfStorySnapshot.get("c_KanbanStatus");

                                    var formattedID = (Ext.Array.contains(recordsByStory[0].get("_TypeHierarchy"), -51006) ? "DE" : "US") + recordsByStory[0].get("_UnformattedID");
                                    var cycleTimes =
                                    {
                                        id: formattedID,
                                        name: recordsByStory[0].get("Name"),
                                        planEstimate: shirtSizeLUT[Rally.util.Array.last(recordsByStory).get("PlanEstimate")],
                                        currentStateOfStory: currentStateOfStory
                                    };

                                    var allStatusesAreNull = true;

                                    for(var i = 0; i < kanbanStatuses.length; i++)
                                    {
                                        var currentStatus = kanbanStatuses[i];

                                        var currentSnapshot = Ext.Array.filter(recordsByStory, function(record) { return record.get("c_KanbanStatus") === currentStatus; })[0];

                                        if(!currentSnapshot)
                                        {
                                            cycleTimes[currentStatus] = null;
                                            continue;
                                        }

                                        var firstDate = new Date(currentSnapshot.get("_ValidFrom"));

                                        var nextSnapshot = Rally.util.Array.last(Ext.Array.filter(recordsByStory, function(record) { return record.get("c_KanbanStatus") === currentStatus; }));
                                        var secondDate = new Date();

                                        if(nextSnapshot && (new Date(nextSnapshot.get("_ValidTo"))).getFullYear() !== 9998)
                                            secondDate = new Date(nextSnapshot.get("_ValidTo"));

                                        var cycleTime = Rally.util.DateTime.getDifference(secondDate, firstDate, "day");
                                        cycleTimes[currentStatus] = cycleTime;

                                        if(cycleTime !== null)
                                            allStatusesAreNull = false;
                                    }

                                    if(!allStatusesAreNull)
                                        aggregateCycleTimes.push(cycleTimes);
                                })();
                            }

                            var myStore = Ext.create("Rally.data.custom.Store",
                            {
                                data: aggregateCycleTimes,
                                pageSize: 100,
                            });

                            var columnConfig = 
                            [
                                {
                                    text: "ID",
                                    dataIndex: "id"
                                },
                                {
                                    text: "Name",
                                    dataIndex: "name",
                                    width: "280px"
                                },
                                {
                                    text: "Size",
                                    dataIndex: "planEstimate"
                                },
                                {
                                    text: "Current State",
                                    dataIndex: "currentStateOfStory"
                                }
                            ];

                            for (var i = 0; i < kanbanStatuses.length; i++)
                            {
                                var columnConfigElement = {}; 
                                columnConfigElement["text"] = kanbanStatuses[i];
                                columnConfigElement["dataIndex"] = kanbanStatuses[i];
                                columnConfig.push(columnConfigElement);
                            }

                            if (!self.grid)
                            {
                                self.grid = self.add(
                                {
                                    xtype: "rallygrid",
                                    itemId: "mygrid",
                                    store: myStore,
                                    columnCfgs: columnConfig
                                });
                            }
                        }
                    });
                }
            });

            Rally.launchApp("CustomApp", { name: "My Custom App" });
        });
    </script>
</head>
<body>
</body>
</html>

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-09-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多