protected override async Task <ReportResult> GenerateAsync(GenerateReOpenedWorkItemsReport command, IDataSource dataSource, ProfileViewModel profile) { // TODO: This part is duplicated between work item reporters, move to base class if (profile.Members == null || !profile.Members.Any()) { Logger.LogWarning("Profile '{ProfileName}({Profile})' does not have any members.", profile.Name, profile.Id); return(WorkItemsReport.Empty); } var workItems = await GetAllWorkItems(dataSource, profile.Members); if (!workItems.Any()) { Logger.LogWarning("No work items found for members in '{ProfileName}({Profile})'", profile.Name, profile.Id); return(WorkItemsReport.Empty); } var team = await GetAllTeamMembers(dataSource, profile.Members); var scope = new ClassificationScope(team, command.Start, command.End); var resolvedList = new List <(string email, int id)>(workItems.Count); var report = new ReOpenedWorkItemsReport(); foreach (var workItem in workItems) { try { var resolutions = _workItemClassificationContext.Classify(workItem, scope); var details = resolutions.OfType <WorkItemReOpenedEvent>() .Select(e => new ReOpenedWorkItemDetail { WorkItemId = e.WorkItem.Id, WorkItemTitle = e.WorkItem.Title, WorkItemType = e.WorkItem.Type, WorkItemProject = string.Empty, ReOpenedDate = e.Date, AssociatedUser = e.AssociatedUser }); report.Details.AddRange(details); var resolvedByUser = resolutions.OfType <WorkItemResolvedEvent>().Select(e => (e.AssociatedUser.Email, e.WorkItem.Id)); resolvedList.AddRange(resolvedByUser); } catch (Exception ex) { Logger.LogError(ex, "Error generating details for workitem {WorkItemId}", workItem.WorkItemId); } } var resolvedLookup = resolvedList .Distinct() .GroupBy(r => r.email) .ToDictionary(k => k.Key, v => v.Count()); report.ResolvedWorkItemsLookup = resolvedLookup; report.MembersLookup = team.OrderBy(t => t.DisplayName).ToDictionary(k => k.Email, v => v.DisplayName); return(report); }
public static CodeClassificationBuilder IsInClassificationScope( this CodeClassificationBuilder @this, ClassificationScope scope) { return(@this.As <ICodeClassificationBuilder>() .IsInClassificationScope(scope)); }
private async Task <AggregatedWorkitemsETAReport> GenerateReport(GenerateAggregatedWorkitemsETAReport command, IDataSource dataSource, ProfileViewModel profile) { var workItems = await GetAllWorkItems(dataSource, profile.Members); if (!workItems.Any()) { return(AggregatedWorkitemsETAReport.Empty); } var team = await GetAllTeamMembers(dataSource, profile.Members); var scope = new ClassificationScope(team, command.Start, command.End); var resolutions = workItems.SelectMany(w => _workItemClassificationContext.Classify(w, scope)) .GroupBy(r => r.AssociatedUser.Email) .ToDictionary(k => k.Key, v => v.AsEnumerable()); var report = new AggregatedWorkitemsETAReport(team.Count()); foreach (var member in team) { var individualReport = GetIndividualReport(resolutions, workItems, dataSource, member, team); report.IndividualReports.Add(individualReport); } return(report); }
CodeClassificationBuilder ICodeClassificationBuilder.IsInClassificationScope( ClassificationScope scope) { _scope.EnsureNotAssigned(nameof(scope)); _scope = scope; return(this); }
public IEnumerable <WorkItemResolution> Classify(VSTSWorkItem item, ClassificationScope scope) { var rs = from c in _classifiers let r = c.Classify(new WorkItemResolutionRequest { WorkItem = item, Team = scope.Team, StartDate = scope.StartDate, EndDate = scope.EndDate }) where !r.IsNone && (IsInRange(r, scope) || r.IsError) select r; return(rs.ToList()); }
private async Task <WorkItemsReport> GenerateReport(IDataSource dataSource, ProfileViewModel profile, GenerateWorkItemsReport command) { if (profile.Members == null || !profile.Members.Any()) { Logger.LogWarning("Profile '{ProfileName}({Profile})' does not have any members.", profile.Name, profile.Id); return(WorkItemsReport.Empty); } var workItems = await GetAllWorkItems(dataSource, profile.Members); if (!workItems.Any()) { Logger.LogWarning("No work items found for members in '{ProfileName}({Profile})'", profile.Name, profile.Id); return(WorkItemsReport.Empty); } var team = await GetAllTeamMembers(dataSource, profile.Members); var scope = new ClassificationScope(team, command.Start, command.End); var report = WorkItemsReport.Empty; foreach (var workItem in workItems) { var isInCodeReview = await dataSource.IsInCodeReview(workItem); if (isInCodeReview && dataSource.IsAssignedToTeamMember(workItem, team)) { report.WorkItemsInReview.Add(dataSource.CreateWorkItemDetail(workItem, team)); } else if (dataSource.IsActive(workItem) && dataSource.IsAssignedToTeamMember(workItem, team)) { report.ActiveWorkItems.Add(dataSource.CreateWorkItemDetail(workItem, team)); } else { var resolutions = _workItemClassificationContext.Classify(workItem, scope); if (dataSource.IsResolved(resolutions)) { report.ResolvedWorkItems.Add(dataSource.CreateWorkItemDetail(workItem, team)); } } } return(report); }
private bool IsInRange(WorkItemResolution r, ClassificationScope scope) { return((r.ResolutionDate >= scope.StartDate && r.ResolutionDate <= scope.EndDate) || r.ResolutionDate == VSTSMaxDate); }
protected override async Task <ReportResult> ReportInternal() { if (!Input.Members.Any() || !Input.Repositories.Any()) { return(AggregatedWorkitemsETAReport.Empty); } var settings = await _repository.GetSingleAsync <Settings>(_ => true); var etaFields = settings?.WorkItemsSettings?.ETAFields; if (etaFields == null || !etaFields.Any()) { throw new MissingETASettingsException(); } await _progressReporter.Report("Fetching workitems..."); var workItemIds = Input.Members.SelectMany(m => m.RelatedWorkItemIds); var workitems = await _repository.GetAsync <VSTSWorkItem>(w => workItemIds.Contains(w.WorkItemId)); if (!workitems.Any()) { return(AggregatedWorkitemsETAReport.Empty); } await _progressReporter.Report("Looking for work item resolutions..."); var scope = new ClassificationScope(Input.Members, Input.Query.StartDate, Input.ActualEndDate); var resolutions = workitems.SelectMany(w => _classificationContext.Classify(w, scope)) .Where(r => r.Resolution == WorkItemStates.Resolved || (r.WorkItemType == WorkItemTypes.Task && r.Resolution == WorkItemStates.Closed)) .GroupBy(r => r.MemberEmail) .ToDictionary(k => k.Key, v => v.AsEnumerable()); var result = new AggregatedWorkitemsETAReport(); result.IndividualReports = new List <AggregatedWorkitemsETAReport.IndividualETAReport>(Input.Members.Count()); foreach (var member in Input.Members) { await _progressReporter.Report($"Calculating metrics for {member.DisplayName}", GetProgressStep()); var individualReport = GetIndividualReport(member); result.IndividualReports.Add(individualReport); } return(result); // Local methods AggregatedWorkitemsETAReport.IndividualETAReport GetIndividualReport(TeamMember member) { if (!resolutions.ContainsKey(member.Email)) { return(AggregatedWorkitemsETAReport.IndividualETAReport.GetEmptyFor(member)); } var individualReport = new AggregatedWorkitemsETAReport.IndividualETAReport { MemberEmail = member.Email, MemberName = member.DisplayName }; PopulateMetrics(member.Email, individualReport); return(individualReport); } void PopulateMetrics(string email, AggregatedWorkitemsETAReport.IndividualETAReport report) { var resolved = resolutions[email]; report.TotalResolved = resolved.Count(); report.TotalResolvedBugs = resolved.Count(w => string.Equals(w.WorkItemType, "Bug", StringComparison.OrdinalIgnoreCase)); report.TotalResolvedTasks = resolved.Count(w => string.Equals(w.WorkItemType, "Task", StringComparison.OrdinalIgnoreCase)); report.Details = new List <AggregatedWorkitemsETAReport.IndividualReportDetail>(report.TotalResolved); foreach (var item in resolved) { var workitem = workitems.Single(w => w.WorkItemId == item.WorkItemId); var timeSpent = GetActiveDuration(workitem); var originalEstimate = GetEtaValue(workitem, ETAFieldType.OriginalEstimate); var completedWork = GetEtaValue(workitem, ETAFieldType.CompletedWork); var remainingWork = GetEtaValue(workitem, ETAFieldType.RemainingWork); if (IsETAEmpty(workitem)) { report.WithoutETA++; report.CompletedWithoutEstimates += timeSpent; } else { var estimatedByDev = completedWork + remainingWork; if (estimatedByDev == 0) { estimatedByDev = originalEstimate; } if (originalEstimate != 0) { report.WithOriginalEstimate++; } report.OriginalEstimated += originalEstimate; report.EstimatedToComplete += estimatedByDev; report.CompletedWithEstimates += timeSpent; } report.Details.Add(new AggregatedWorkitemsETAReport.IndividualReportDetail { WorkItemId = item.WorkItemId, WorkItemTitle = item.WorkItemTitle, WorkItemType = item.WorkItemType, OriginalEstimate = originalEstimate, EstimatedToComplete = remainingWork + completedWork, TimeSpent = timeSpent, }); } } bool IsETAEmpty(VSTSWorkItem wi) => IsNullOrEmpty(wi, FieldNameFor(wi.WorkItemType, ETAFieldType.OriginalEstimate)) && IsNullOrEmpty(wi, FieldNameFor(wi.WorkItemType, ETAFieldType.CompletedWork)) && IsNullOrEmpty(wi, FieldNameFor(wi.WorkItemType, ETAFieldType.RemainingWork)); bool IsNullOrEmpty(VSTSWorkItem wi, string fieldName) => !wi.Fields.ContainsKey(fieldName) || string.IsNullOrEmpty(wi.Fields[fieldName]); string FieldNameFor(string workItemType, ETAFieldType fieldType) => etaFields.First(f => f.WorkitemType == workItemType && f.FieldType == fieldType).FieldName; float GetEtaValue(VSTSWorkItem wi, ETAFieldType etaType) { var fieldName = FieldNameFor(wi.WorkItemType, etaType); if (!wi.Fields.ContainsKey(fieldName)) { return(0); } var value = wi.Fields[fieldName]; if (string.IsNullOrEmpty(value)) { return(0); } return(float.Parse(value)); } }
protected override async Task <ReportResult> ReportInternal() { if (!Input.Members.Any() || !Input.Repositories.Any()) { return(WeeklyStatusReport.Empty); } var settings = await _repository.GetSingleAsync <Settings>(_ => true); var etaFields = settings?.WorkItemsSettings?.ETAFields; if (etaFields == null || !etaFields.Any()) { throw new MissingETASettingsException(); } await _progressReporter.Report("Fetching workitems..."); var workItemIds = Input.Members.SelectMany(m => m.RelatedWorkItemIds); var workitems = (await _repository.GetAsync <VSTSWorkItem>(w => workItemIds.Contains(w.WorkItemId))).Where(w => w.WorkItemType != null && (w.WorkItemType == WorkItemTypes.Bug || w.WorkItemType == WorkItemTypes.Task)); if (!workitems.Any()) { return(WeeklyStatusReport.Empty); } await _progressReporter.Report("Looking for work item resolutions..."); var scope = new ClassificationScope(Input.Members, Input.Query.StartDate, Input.ActualEndDate); var report = WeeklyStatusReport.Empty; foreach (var workItem in workitems) { if (workItem.IsInCodeReview() && workItem.IsAssignedToTeamMember(Input.Members)) { report.WorkItemsInReview.Add(CreateWorkItemDetail(workItem)); } else if (workItem.State == WorkItemStates.Active && workItem.IsAssignedToTeamMember(Input.Members)) { report.ActiveWorkItems.Add(CreateWorkItemDetail(workItem)); } else { var resolutions = _classificationContext.Classify(workItem, scope); if (resolutions.Any(r => r.Resolution == WorkItemStates.Resolved || (r.WorkItemType == WorkItemTypes.Task && r.Resolution == WorkItemStates.Closed))) { report.ResolvedWorkItems.Add(CreateWorkItemDetail(workItem));; } } } WeeklyStatusReport.WorkItemDetail CreateWorkItemDetail(VSTSWorkItem item) { var timeSpent = item.GetActiveDuration(Input.Members); var originalEstimate = item.GetEtaValue(ETAFieldType.OriginalEstimate, settings); var completedWork = item.GetEtaValue(ETAFieldType.CompletedWork, settings); var remainingWork = item.GetEtaValue(ETAFieldType.RemainingWork, settings); if (remainingWork < 1) { remainingWork = originalEstimate; } return(new WeeklyStatusReport.WorkItemDetail { WorkItemId = item.WorkItemId, WorkItemTitle = item.Title, WorkItemType = item.WorkItemType, WorkItemProject = string.IsNullOrWhiteSpace(item.AreaPath) ? null : item.AreaPath.Split('\\')[0], Tags = item.Updates.LastOrDefault(u => !string.IsNullOrWhiteSpace(u.Tags.NewValue))?.Tags?.NewValue, OriginalEstimate = originalEstimate, EstimatedToComplete = remainingWork + completedWork, TimeSpent = timeSpent }); } return(report); }