diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff5b00c --- /dev/null +++ b/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Azure Functions localsettings file +local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..3a030cc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-vscode.csharp" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..894cbe6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to .NET Functions", + "type": "coreclr", + "request": "attach", + "processId": "${command:azureFunctions.pickProcess}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1816eba --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "azureFunctions.deploySubpath": "bin/Release/netcoreapp2.1/publish", + "azureFunctions.projectLanguage": "C#", + "azureFunctions.projectRuntime": "~2", + "debug.internalConsoleOptions": "neverOpen", + "azureFunctions.preDeployTask": "publish" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..6d29550 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,45 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "clean", + "command": "dotnet clean", + "type": "shell", + "problemMatcher": "$msCompile" + }, + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "dependsOn": "clean", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile" + }, + { + "label": "clean release", + "command": "dotnet clean --configuration Release", + "type": "shell", + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet publish --configuration Release", + "type": "shell", + "dependsOn": "clean release", + "problemMatcher": "$msCompile" + }, + { + "type": "func", + "dependsOn": "build", + "options": { + "cwd": "${workspaceFolder}/bin/Debug/netcoreapp2.1" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-watch" + } + ] +} \ No newline at end of file diff --git a/Constants/CoreConstants.cs b/Constants/CoreConstants.cs new file mode 100644 index 0000000..d6a1978 --- /dev/null +++ b/Constants/CoreConstants.cs @@ -0,0 +1,166 @@ +using System; + +namespace esdc_sa_appdev_reporting_api.Constants +{ + public sealed class CoreConstants + { + #region --- Enums ----------------------------------------------------- + + [Flags] + public enum OpFlag + { + None = 0x0, + Success = 0x1, + Halted = 0x2, + Exception = 0x4, + Error = 0x8, + Warning = 0x10, + Notice = 0x20, + ErrValidation = 0x40, + ErrBusiness = 0x80, + ErrSecurity = 0x100, + ErrProcess = 0x200, + } + + [Flags] + public enum Lang + { + None = 0x0, + En = 0x1, + Fr = 0x2 + } + + + [Flags] + public enum WcFilter + { + // Wild Card Filters { %x, %x%, x% } + + None = 0x0, + StartsWith = 0x1, + Contains = 0x2, + EndsWith = 0x4 + } + + + [Flags] + public enum RangeLimit + { + None = 0x0, + Min = 0x1, + Max = 0x2 + } + + + [Flags] + public enum SortOrder + { + None = 0x0, + Asc = 0x1, + Desc = 0x2 + } + + + public enum ConnectivityMode + { + Online, + Offline + } + + + public enum EntityOperation + { + None, + InitCreate, + Add, + Modify, + Delete, + Merge, + Purge + } + + + public enum TreeResultMode + { + NodeTraverse, + NodeConcat + } + + #endregion + + + #region --- Sets ------------------------------------------------------ + + public static class CultureIdent + { + public const string En = "en-CA"; + public const string Fr = "fr-CA"; + } + + + public static class LangThreeLetter + { + public const string En = "eng"; + public const string Fr = "fra"; + } + + + public static class LangIdent + { + public const string En = "en"; + public const string Fr = "fr"; + } + + + public static class YesNoIdent + { + public const string Yes = "Y"; + public const string No = "N"; + } + + + public static class RegEx + { + public const string Integer = "^[0-9]*$"; + public const string NullableInteger = "^[0-9]*$|^$"; + public const string PostalCode = "[abceghjklmnprstvxyABCEGHJKLMNPRSTVXY][0-9][abceghjklmnprstvwxyzABCEGHJKLMNPRSTVWXYZ][ ]?[0-9][abceghjklmnprstvwxyzABCEGHJKLMNPRSTVWXYZ][0-9]"; + public const string TelephoneNo = "[1]?[ ]?[\\(]{0,1}([0-9]){3}[\\)]{0,1}[ ]?([0-9]){3}[ ]?[-]?[ ]?([0-9]){4}[ ]?([xX][ ]?[0-9]{1,5})?"; + public const string Email = "([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)"; + public const string SecurityUserName = "[a-zA-Z]{1}[a-zA-Z0-9_]*"; + public const string SecurityPassword = ".*(?=.{8,20})(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z]).*"; + } + + + public static class Formats + { + public const string DateIso = "yyyy-MM-dd"; + public const string DtmIso = "yyyy-MM-dd HH:mm:ss"; + public const string DtmZuluIso = "yyyy-MM-ddTHH:mm:ss.fffZ"; + } + + + public static class DefaultValues + { + public const int Numeric = 0; + public const int NonZero = -1; + } + + + public static class Separators + { + public const string Default = "|"; + public const string OutputLabel = " / "; + } + + + public static class FileFilters + { + public const string FileFilterDefaultAll = "All / tout (*.*)|*.*"; + public const string FileFilterLibraryDocument = "Valid / valides|*.pdf;*.xls;*.doc;*.docx|pdf (*.pdf)|*.pdf|xls (*.xls)|*.xls|doc (*.doc)|*.doc|doc (*.docx)|*.docx"; + public const string FileFilterLibraryImage = "Valid / valides|*.jpg;*.png;*.gif;*.tiff|jpg (*.jpg)|*.jpg|png (*.png)|*.png|gif (*.gif)|*.gif|tiff (*.tiff)|*.tiff"; + public const string FileFilterRDIMSsupportedFiles = "MS Office |*.doc;*.docx;*.xls;*.xlsx;*.ppt;*.pptx*.pptx;*.vsd;*.mpp|PDF |*.pdf|Images files|*.jpg;*.png"; + } + + #endregion + } +} diff --git a/Constants/SolutionConstants.cs b/Constants/SolutionConstants.cs new file mode 100644 index 0000000..d15f6d8 --- /dev/null +++ b/Constants/SolutionConstants.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; + +namespace esdc_sa_appdev_reporting_api.Constants +{ + public sealed class SolutionConstants + { + #region --- Enums ----------------------------------------------------- + /* + public enum ActionType + { + PastDue, + NotStarted, + InProgress, + Completed + } + */ + #endregion + + + #region --- Sets ------------------------------------------------------ + + public static class TrelloLists + { + public const string Backlog = "Backlog"; + public const string Committed = "Committed"; + public const string InProgress = "In Progress"; + public const string OnHold = "On Hold / Blocked"; + public const string Done = "Done"; + } + + + public static class TrelloLabelCategory + { + public const string Client = "green"; + public const string Epic = "red"; + public const string DevX = "blue"; + public const string Administration = "sky"; + public const string Feature = "purple"; + public const string Team = "black"; + } + + #endregion + + + #region --- Complex --------------------------------------------------- + + public static class TrelloListIndexMap + { + // { index, list } + + public static readonly Dictionary Map = new Dictionary() + { + {1, SolutionConstants.TrelloLists.Backlog}, + {2, SolutionConstants.TrelloLists.Committed}, + {3, SolutionConstants.TrelloLists.InProgress}, + {4, SolutionConstants.TrelloLists.OnHold}, + {5, SolutionConstants.TrelloLists.Done} + }; + } + + #endregion + + + #region --- Single Values --------------------------------------------- + + public const string kTrelloAppKey = "534001bbfeb9302f0f62fd6263f80567"; + public const string kTrelloUserSecret = "861101898508498761cd2952b258926b568326994f44ef23fd85ca3685667525"; + public const string kTrelloUserToken = "9a4b5cc8411e7380cf8fc392341f118e2f32869ac50a134fb9bd81f43fb0fe62"; + public const string kTrelloBoardId = "5cdf08913ae8753993cfdd9c"; + + public const string kLabelSeperator = ":"; + + public const string kSelectListKeyEmpty = ""; + public const string kSelectListKeyAll = "ALL"; + public const string kSelectListKeyUnknown = "UNKNOWN"; + + #endregion + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94f4027 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Developer Experience - Expérience des développeurs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Models/GeneralReportResult.cs b/Models/GeneralReportResult.cs new file mode 100644 index 0000000..35be5bf --- /dev/null +++ b/Models/GeneralReportResult.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace esdc_sa_appdev_reporting_api.Models +{ + public class GeneralReportResult + { + public string CardId { get; set; } + public string CardTitle { get; set; } + public string CardCurrentListId { get; set; } + public string CardCurrentListTitle { get; set; } + public List CardLabels { get; set; } + public Dictionary CardMembers { get; set; } + public DateTime? CardDateStarted { get; set; } + public DateTime? CardDateCompleted { get; set; } + public int NbrDaysOnHold { get; set; } + + + public GeneralReportResult() + { + this.CardLabels = new List(); + this.CardMembers = new Dictionary(); + } + } +} \ No newline at end of file diff --git a/Models/LabelResult.cs b/Models/LabelResult.cs new file mode 100644 index 0000000..a364382 --- /dev/null +++ b/Models/LabelResult.cs @@ -0,0 +1,9 @@ +namespace esdc_sa_appdev_reporting_api.Models +{ + public class LabelResult + { + public string Id { get; set; } + public string Name { get; set; } + public string Color { get; set; } + } +} \ No newline at end of file diff --git a/Models/SummaryReportResult.cs b/Models/SummaryReportResult.cs new file mode 100644 index 0000000..f1243d5 --- /dev/null +++ b/Models/SummaryReportResult.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace esdc_sa_appdev_reporting_api.Models +{ + public class SummaryReportResult + { + public List> CompiledResults { get; set; } + public List RawResults { get; set; } + + + public SummaryReportResult() + { + this.CompiledResults = new List>(); + this.RawResults = new List(); + } + } +} \ No newline at end of file diff --git a/Models/TrelloActionDataCardDto.cs b/Models/TrelloActionDataCardDto.cs new file mode 100644 index 0000000..597e61d --- /dev/null +++ b/Models/TrelloActionDataCardDto.cs @@ -0,0 +1,7 @@ +namespace esdc_sa_appdev_reporting_api.Models +{ + public class TrelloActionDataCardDto + { + public string id { get; set; } + } +} \ No newline at end of file diff --git a/Models/TrelloActionDataDto.cs b/Models/TrelloActionDataDto.cs new file mode 100644 index 0000000..600becb --- /dev/null +++ b/Models/TrelloActionDataDto.cs @@ -0,0 +1,17 @@ +namespace esdc_sa_appdev_reporting_api.Models +{ + public class TrelloActionDataDto + { + public TrelloActionDataCardDto card { get; set; } + public TrelloActionDataListDto listAfter { get; set; } + public TrelloActionDataListDto listBefore { get; set; } + + + public TrelloActionDataDto() + { + card = new TrelloActionDataCardDto(); + listAfter = new TrelloActionDataListDto(); + listBefore = new TrelloActionDataListDto(); + } + } +} \ No newline at end of file diff --git a/Models/TrelloActionDataListDto.cs b/Models/TrelloActionDataListDto.cs new file mode 100644 index 0000000..c998c39 --- /dev/null +++ b/Models/TrelloActionDataListDto.cs @@ -0,0 +1,8 @@ +namespace esdc_sa_appdev_reporting_api.Models +{ + public class TrelloActionDataListDto + { + public string id { get; set; } + public string name { get; set; } + } +} \ No newline at end of file diff --git a/Models/TrelloActionDto.cs b/Models/TrelloActionDto.cs new file mode 100644 index 0000000..ddc790f --- /dev/null +++ b/Models/TrelloActionDto.cs @@ -0,0 +1,32 @@ +using System; + +namespace esdc_sa_appdev_reporting_api.Models +{ + public class TrelloActionDto + { + public string id { get; set; } + public string type { get; set; } + public DateTime date { get; set; } + public TrelloActionDataDto data { get; set; } + + + public TrelloActionDto() + { + data = new TrelloActionDataDto(); + } + + + public bool IsListTransfer () + { + if ((string.IsNullOrEmpty(data.listAfter.id) == false) && + (string.IsNullOrEmpty(data.listBefore.id) == false)) + { + return true; + } + else + { + return false; + } + } + } +} \ No newline at end of file diff --git a/Models/TrelloCardDto.cs b/Models/TrelloCardDto.cs new file mode 100644 index 0000000..192f322 --- /dev/null +++ b/Models/TrelloCardDto.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace esdc_sa_appdev_reporting_api.Models +{ + public class TrelloCardDto + { + public string id { get; set; } + public string idBoard { get; set; } + public string idList { get; set; } + public string name { get; set; } + public List idLabels { get; set; } + public List idMembers { get; set; } + + + public TrelloCardDto() + { + idLabels = new List(); + idMembers = new List(); + } + } +} \ No newline at end of file diff --git a/Models/TrelloLabels.cs b/Models/TrelloLabels.cs new file mode 100644 index 0000000..e72e61b --- /dev/null +++ b/Models/TrelloLabels.cs @@ -0,0 +1,10 @@ +namespace esdc_sa_appdev_reporting_api.Models +{ + public class TrelloLabelsDto + { + public string id { get; set; } + public string idBoard { get; set; } + public string name { get; set; } + public string color { get; set; } + } +} \ No newline at end of file diff --git a/Models/TrelloListDto.cs b/Models/TrelloListDto.cs new file mode 100644 index 0000000..e08bd4e --- /dev/null +++ b/Models/TrelloListDto.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +namespace esdc_sa_appdev_reporting_api.Models +{ + public class TrelloListDto + { + public string id { get; set; } + public string idBoard { get; set; } + public string name { get; set; } + } +} \ No newline at end of file diff --git a/Models/TrelloMemberDto.cs b/Models/TrelloMemberDto.cs new file mode 100644 index 0000000..83c5e5b --- /dev/null +++ b/Models/TrelloMemberDto.cs @@ -0,0 +1,8 @@ +namespace esdc_sa_appdev_reporting_api.Models +{ + public class TrelloMemberDto + { + public string id { get; set; } + public string fullName { get; set; } + } +} \ No newline at end of file diff --git a/Services/TrelloService.cs b/Services/TrelloService.cs new file mode 100644 index 0000000..1d78826 --- /dev/null +++ b/Services/TrelloService.cs @@ -0,0 +1,355 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using esdc_sa_appdev_reporting_api.Models; +using esdc_sa_appdev_reporting_api.Constants; +using Newtonsoft.Json; + +namespace esdc_sa_appdev_reporting_api.Services +{ + public class TrelloService + { + public async Task>> GetSummaryReportResults() + { + /* + The resulting result matrix must have the following format: + [ + [' ','Backlog','Committed','In Progress','Blocked / On Hold','Done'], + ['BDM', '6', '12', '5', '2', '6'], + ['OAS-SIS', '7', '4', '1', '1', '3'], + ['DTS', '1', '1', '0', '3', '1'], + ['CCOE', '2', '6', '3', '2', '1'] + ] + + where the first row contains a non-empty string as the first item and the client titles + then, where the first item of each result set is the client and the remaining are + the tallied number of cards per list in order of Backlog to Done. + */ + + var results = new List>(); + + var reportResultItems = await this.GetGeneralReportResults(); + + // Extract the results that have a Client label + var applicableResults = reportResultItems + .Where(x => x.CardLabels.Where(y => y.Color.Equals(SolutionConstants.TrelloLabelCategory.Client)).Any()) + .ToList(); + + // Compile the count of cards per list by sub-category, where sub-category equals color "Client" + // The first-level key is the Client label, the second-level is the Trello list + var compiledResultMatrix = new Dictionary>(); + + foreach (var result in applicableResults) + { + // Get the first green label of the card (should only be only, but just in case, we'll grab the first). + var subCategoryLabel = result.CardLabels.FirstOrDefault(x => x.Color == SolutionConstants.TrelloLabelCategory.Client); + + // Get the label sub-category (meaning, remove the prefix, i.e. "Client: ") + var posLabelSeparator = subCategoryLabel.Name.IndexOf(SolutionConstants.kLabelSeperator); + + var clientKey = (((posLabelSeparator > 0) && (posLabelSeparator < (subCategoryLabel.Name.Length - 1))) + ? subCategoryLabel.Name.Substring(posLabelSeparator + 1) + : subCategoryLabel.Name) + .Trim(); + + // Tally the nbr of cards + if (compiledResultMatrix.ContainsKey(clientKey) == false) + { + compiledResultMatrix.Add(clientKey, new Dictionary(){{result.CardCurrentListTitle, 1}}); + } + else + { + if (compiledResultMatrix[clientKey].ContainsKey(result.CardCurrentListTitle) == false) + { + compiledResultMatrix[clientKey].Add(result.CardCurrentListTitle, 1); + } + else + { + compiledResultMatrix[clientKey][result.CardCurrentListTitle] ++; + } + } + } + + // For each client, fetch the total for each Trello list + foreach (var client in compiledResultMatrix) + { + var trelloClientValues = new List(); + + // Add the first value: client name + trelloClientValues.Add(client.Key); + + // Add the nbr of cards value in explicit list order + foreach (var trelloList in SolutionConstants.TrelloListIndexMap.Map) + { + // Check to see if there's a compiled value for the intended client + trello list, otherwise assign 0. + if (compiledResultMatrix[client.Key].ContainsKey(trelloList.Value) == true) + { + trelloClientValues.Add(compiledResultMatrix[client.Key][trelloList.Value].ToString()); + } + else + { + trelloClientValues.Add("0"); + } + } + + results.Add(trelloClientValues); + } + + // Order the clients alphabetically + results = results.OrderBy(x => x.FirstOrDefault()).ToList(); + + // Add the first result row. + var firstResultSetValues = new List(); + + firstResultSetValues.Add(" "); + firstResultSetValues.Add(SolutionConstants.TrelloLists.Backlog); + firstResultSetValues.Add(SolutionConstants.TrelloLists.Committed); + firstResultSetValues.Add(SolutionConstants.TrelloLists.InProgress); + firstResultSetValues.Add(SolutionConstants.TrelloLists.OnHold); + firstResultSetValues.Add(SolutionConstants.TrelloLists.Done); + + results.Insert(0, firstResultSetValues); + + return results; + } + + + public async Task> GetGeneralReportResults() + { + // 1. Gather all cards + // 2. Compute date started and completed + // 3. Compute number of days on hold + + var results = new List(); + + var trelloLists = await this.GetTrelloLists(); + var trelloLabels = await this.GetTrelloLabels(); + var trelloMembers = await this.GetTrelloMembers(); + var trelloCards = await this.GetTrelloCards(); + var trelloCardMoveActions = await this.GetTrelloCardMoveActions(); + + // Pre-sort + trelloCardMoveActions + .OrderBy(x => x.data.card) + .ThenBy(x => x.date); + + // Step 1 + foreach (var card in trelloCards) + { + var result = new GeneralReportResult(); + + result.CardId = card.id; + result.CardTitle = card.name; + result.CardCurrentListId = card.idList; + result.CardCurrentListTitle = trelloLists.SingleOrDefault(x => x.id == card.idList)?.name; + + foreach (var labelId in card.idLabels) + { + var trelloLabel = trelloLabels.SingleOrDefault(x => x.id == labelId); + + if (trelloLabel != null) + { + var cardLabel = new LabelResult(); + + cardLabel.Id = trelloLabel.id; + cardLabel.Name = trelloLabel.name; + cardLabel.Color = trelloLabel.color; + + result.CardLabels.Add(cardLabel); + } + } + + foreach (var memberId in card.idMembers) + { + var trelloMember = trelloLabels.SingleOrDefault(x => x.id == memberId); + + result.CardMembers.Add + ( + trelloMembers.SingleOrDefault(x => x.id == memberId)?.id, + trelloMembers.SingleOrDefault(x => x.id == memberId)?.fullName + ); + } + + // Card in backlog and committed aren't considered started. + if ((result.CardCurrentListTitle != SolutionConstants.TrelloLists.Backlog) || + (result.CardCurrentListTitle != SolutionConstants.TrelloLists.Committed)) + { + var action = trelloCardMoveActions + .Where + ( + x => + (x.data.card.id == card.id) && + (x.data.listAfter.name == SolutionConstants.TrelloLists.InProgress) + ) + .FirstOrDefault(); + + result.CardDateStarted = action?.date; + } + + // Only cards in done are considered completed. + if (result.CardCurrentListTitle != SolutionConstants.TrelloLists.Done) + { + var action = trelloCardMoveActions + .Where(x => (x.data.card.id == card.id) && x.data.listAfter.name == SolutionConstants.TrelloLists.Done) + .LastOrDefault(); + + result.CardDateCompleted = action?.date; + } + + results.Add(result); + } + + return results; + } + + + public async Task> GetTrelloLists() + { + using (var http = new HttpClient()) + { + try + { + var url = "https://api.trello.com/1/boards/" + SolutionConstants.kTrelloBoardId + "/lists/" + + "?key=" + SolutionConstants.kTrelloAppKey + + "&token=" + SolutionConstants.kTrelloUserToken; + + var response = await http.GetAsync(url); + + response.EnsureSuccessStatusCode(); + + var jsonResult = await response.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject>(jsonResult); + } + catch (HttpRequestException httpRequestException) + { + Console.WriteLine($"Error in GetTrelloLists: {httpRequestException.Message}"); + } + + return null; + } + } + + + public async Task> GetTrelloLabels() + { + using (var http = new HttpClient()) + { + try + { + var url = "https://api.trello.com/1/boards/" + SolutionConstants.kTrelloBoardId + "/labels/" + + "?key=" + SolutionConstants.kTrelloAppKey + + "&token=" + SolutionConstants.kTrelloUserToken; + + var response = await http.GetAsync(url); + + response.EnsureSuccessStatusCode(); + + var jsonResult = await response.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject>(jsonResult); + } + catch (HttpRequestException httpRequestException) + { + Console.WriteLine($"Error in GetTrelloLabels: {httpRequestException.Message}"); + } + + return null; + } + } + + + public async Task> GetTrelloMembers() + { + using (var http = new HttpClient()) + { + try + { + var url = "https://api.trello.com/1/boards/" + SolutionConstants.kTrelloBoardId + "/members/" + + "?key=" + SolutionConstants.kTrelloAppKey + + "&token=" + SolutionConstants.kTrelloUserToken; + + var response = await http.GetAsync(url); + + response.EnsureSuccessStatusCode(); + + var jsonResult = await response.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject>(jsonResult); + } + catch (HttpRequestException httpRequestException) + { + Console.WriteLine($"Error in GetTrelloMembers: {httpRequestException.Message}"); + } + + return null; + } + } + + + public async Task> GetTrelloCards() + { + using (var http = new HttpClient()) + { + try + { + var url = "https://api.trello.com/1/boards/" + SolutionConstants.kTrelloBoardId + "/cards/" + + "?key=" + SolutionConstants.kTrelloAppKey + + "&token=" + SolutionConstants.kTrelloUserToken; + + var response = await http.GetAsync(url); + + response.EnsureSuccessStatusCode(); + + var jsonResult = await response.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject>(jsonResult); + } + catch (HttpRequestException httpRequestException) + { + Console.WriteLine($"Error in GetTrelloCards: {httpRequestException.Message}"); + } + + return null; + } + } + + + // https://stackoverflow.com/questions/51777063/how-can-i-get-all-actions-for-a-board-using-trellos-rest-api + + public async Task> GetTrelloCardMoveActions() + { + using (var http = new HttpClient()) + { + try + { + var url = "https://api.trello.com/1/boards/" + SolutionConstants.kTrelloBoardId + "/actions/" + + "?key=" + SolutionConstants.kTrelloAppKey + + "&token=" + SolutionConstants.kTrelloUserToken + //+ "&before=2019-07-01" + //+ "&since=2019-06-01" + + "&filter=updateCard" + + "&limit=1000"; + + var response = await http.GetAsync(url); + + response.EnsureSuccessStatusCode(); + + var jsonResult = await response.Content.ReadAsStringAsync(); + + var list = JsonConvert.DeserializeObject>(jsonResult); + + return list.Where(x => (x.IsListTransfer() == true)).ToList(); + } + catch (HttpRequestException httpRequestException) + { + Console.WriteLine($"Error in GetTrelloCardMoveActions: {httpRequestException.Message}"); + } + + return null; + } + } + } +} \ No newline at end of file diff --git a/TrelloReportingApi.cs b/TrelloReportingApi.cs new file mode 100644 index 0000000..879dc77 --- /dev/null +++ b/TrelloReportingApi.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using esdc_sa_appdev_reporting_api.Services; + +namespace esdc_sa_appdev_reporting_api +{ + public static class TrelloReportingApi + { + [FunctionName("getGeneralReportResults")] + public static async Task GetGeneralReportResults + ( + [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, + ILogger log + ) + { + var service = new TrelloService(); + + var results = await service.GetGeneralReportResults(); + + return results != null + ? (ActionResult) new OkObjectResult(results) + : new BadRequestObjectResult("No results retrieved..."); + } + + + [FunctionName("getSummaryReportResults")] + public static async Task GetSummaryReportResults + ( + [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, + ILogger log + ) + { + var service = new TrelloService(); + + var results = await service.GetSummaryReportResults(); + + return results != null + ? (ActionResult) new OkObjectResult(results) + : new BadRequestObjectResult("No results retrieved..."); + } + } +} \ No newline at end of file diff --git a/esdc-sa-appdev-reporting-api.csproj b/esdc-sa-appdev-reporting-api.csproj new file mode 100644 index 0000000..1dcbb70 --- /dev/null +++ b/esdc-sa-appdev-reporting-api.csproj @@ -0,0 +1,19 @@ + + + netcoreapp2.1 + v2 + esdc_sa_appdev_reporting_api + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + \ No newline at end of file diff --git a/host.json b/host.json new file mode 100644 index 0000000..b9f92c0 --- /dev/null +++ b/host.json @@ -0,0 +1,3 @@ +{ + "version": "2.0" +} \ No newline at end of file