Changeset 5887289614d0…
by Lou Manglass
Changes to 60 files · Browse files at 5887289614d0 Diff from another changeset...
|
@@ -0,0 +1,15 @@ + syntax: glob
+
+## User-specific files
+
+*.suo
+*.user
+
+## Build results
+
+[Dd]ebug/
+[Rr]elease/
+x64/
+build/
+[Bb]in/
+[Oo]bj/
|
|
@@ -0,0 +1,51 @@ + using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using FogBugzForVisualStudio.Api;
+
+namespace FogBugzForVisualStudio.Test
+{
+ class Locale : IDisposable {
+ private CultureInfo current;
+
+ public Locale(CultureInfo culture) {
+ current = Thread.CurrentThread.CurrentCulture;
+ Thread.CurrentThread.CurrentCulture = culture;
+ }
+
+ public void Dispose()
+ {
+ Thread.CurrentThread.CurrentCulture = current;
+ }
+ }
+
+ [TestClass]
+ public class ApiTests
+ {
+ [TestMethod]
+ public void CreateBug()
+ {
+ using(new Locale(CultureInfo.InvariantCulture))
+ {
+ var bug = new Case(1, Case.Op.resolve, new MockBug().Dict, new MockClient());
+ }
+ }
+
+ [TestMethod]
+ public void CreateBugLocalized()
+ {
+ using (new Locale(new CultureInfo("fr-FR")))
+ {
+ var bug = new Case(1, Case.Op.resolve, new MockBug{
+ {"hrsElapsed", "0.5"}
+ }.Dict, new MockClient());
+ }
+ }
+ }
+}
|
Change 1 of 1
|
||
---|---|---|
|
@@ -0,0 +1,66 @@ + <?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>
+ </ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{91F54069-7C06-4327-9A18-D4DC6BDA3114}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>FogBugzForVisualStudio.Test</RootNamespace>
+ <AssemblyName>FogBugzForVisualStudio.Test</AssemblyName>
+ <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
+ <Reference Include="System" />
+ <Reference Include="System.Core">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
+ <Visible>False</Visible>
+ </CodeAnalysisDependentAssemblyPaths>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Mocks.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="ApiTests.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\FogBugzForVisualStudio\FogBugzForVisualStudio.csproj">
+ <Project>{1261AF02-2638-4D92-AF2C-8112C72E46E7}</Project>
+ <Name>FogBugzForVisualStudio</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project>
\ No newline at end of file |
|
@@ -0,0 +1,66 @@ + using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using FogBugzForVisualStudio.Api;
+
+namespace FogBugzForVisualStudio.Test
+{
+ class MockClient : FogBugzClient
+ {
+ public override Person GetPerson(int ixPerson)
+ {
+ return null;
+ }
+
+ public override Status GetStatus(int ixStatus)
+ {
+ return null;
+ }
+
+ public override Category GetCategory(int ixCategory)
+ {
+ return null;
+ }
+ }
+
+ class MockBug : System.Collections.IEnumerable
+ {
+
+ private Dictionary<string, string> fields = new Dictionary<string, string>();
+ public Dictionary<string, string> Dict { get { return fields; } }
+
+ private void Fill(String s, string[] keys)
+ {
+ foreach (var key in keys)
+ {
+ fields[key] = s;
+ }
+ }
+
+ public MockBug()
+ {
+ Fill("0", new string[]{
+ "ixBugParent", "ixBugChildren", "ixBugEventLatest", "ixBugEventLastView", "ixCategory",
+ "ixPriority", "ixStatus", "ixProject", "ixArea", "ixFixFor", "ixPersonAssignedTo",
+ "ixPersonOpenedBy", "ixPersonResolvedBy", "ixPersonClosedBy", "ixPersonLastEditedBy",
+ Case.BacklogFieldName
+ });
+
+ Fill("", new string[] { "sProject", "sArea", "sFixFor", "sPriority", "sTitle" });
+ Fill("0", new string[] { "hrsOrigEst", "hrsCurrEst", "hrsElapsed" });
+ Fill(null, new string[] { "dtOpened", "dtResolved", "dtClosed", "dtDue", "dtLastUpdated" });
+ }
+
+ public void Add(string key, string value)
+ {
+ fields[key] = value;
+ }
+
+ public System.Collections.IEnumerator GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
|
|
@@ -0,0 +1,35 @@ + using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("FogBugzForVisualStudio.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("FogBugzForVisualStudio.Test")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2010")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("10b12626-e388-4cb3-b7d3-fc8ced5ce83e")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
|
|
@@ -0,0 +1,36 @@ +
+Microsoft Visual Studio Solution File, Format Version 11.00
+# Visual Studio 2010
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FogBugzForVisualStudio", "FogBugzForVisualStudio\FogBugzForVisualStudio.csproj", "{1261AF02-2638-4D92-AF2C-8112C72E46E7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FogBugzForVisualStudio.Test", "FogBugzForVisualStudio.Test\FogBugzForVisualStudio.Test.csproj", "{91F54069-7C06-4327-9A18-D4DC6BDA3114}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{67FAB558-D949-40E1-A588-C9B7912068D8}"
+ ProjectSection(SolutionItems) = preProject
+ FogBugzForVisualStudio.vsmdi = FogBugzForVisualStudio.vsmdi
+ Local.testsettings = Local.testsettings
+ TraceAndTestImpact.testsettings = TraceAndTestImpact.testsettings
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(TestCaseManagementSettings) = postSolution
+ CategoryFile = FogBugzForVisualStudio.vsmdi
+ EndGlobalSection
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {1261AF02-2638-4D92-AF2C-8112C72E46E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1261AF02-2638-4D92-AF2C-8112C72E46E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1261AF02-2638-4D92-AF2C-8112C72E46E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1261AF02-2638-4D92-AF2C-8112C72E46E7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91F54069-7C06-4327-9A18-D4DC6BDA3114}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {91F54069-7C06-4327-9A18-D4DC6BDA3114}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91F54069-7C06-4327-9A18-D4DC6BDA3114}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {91F54069-7C06-4327-9A18-D4DC6BDA3114}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
|
|
@@ -0,0 +1,23 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Area
+ {
+ public readonly int ixArea;
+ public readonly string sArea;
+
+ public Area(int ixArea, string sArea)
+ {
+ this.ixArea = ixArea;
+ this.sArea = sArea;
+ }
+
+ public override string ToString()
+ {
+ return sArea;
+ }
+ }
+}
|
|
|
@@ -0,0 +1,172 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Drawing;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Case
+ {
+ [FlagsAttribute]
+ public enum Op
+ {
+ edit = 0x0001,
+ spam = 0x0002,
+ assign = 0x0004,
+ resolve = 0x0008,
+ move = 0x0010,
+ reactivate = 0x0020,
+ close = 0x0040,
+ reopen = 0x0080,
+ remind = 0x0100,
+ reply = 0x0200,
+ forward = 0x0400,
+ email = 0x0800
+ }
+
+ public int ixBug { get; private set; }
+ public Op ops { get; private set; }
+
+ public int? ixBugParent { get; private set; }
+ public IList<int> ixBugChildren { get; private set; }
+
+ public int ixBugEventLatest { get; private set; }
+ public int ixBugEventLastView { get; private set; }
+
+ public Category category { get; private set; }
+ public Priority priority { get; private set; }
+ public Status status { get; private set; }
+ public Project project { get; private set; }
+ public Area area { get; private set; }
+ public FixFor fixfor { get; private set; }
+
+ public string sTitle { get; private set; }
+
+ public Person assignedTo { get; private set; }
+ public Person openedBy { get; private set; }
+ public Person resolvedBy { get; private set; }
+ public Person closedBy { get; private set; }
+ public Person lastEditedBy { get; private set; }
+
+ public Estimate hrsOrigEst { get; private set; }
+ public Estimate hrsCurrEst { get; private set; }
+ public Estimate hrsElapsed { get; private set; }
+ public Estimate hrsRemaining
+ {
+ get
+ {
+ return new Estimate(Math.Max(this.hrsCurrEst.hrs - this.hrsElapsed.hrs, Decimal.Zero));
+ }
+ }
+
+ public Nullable<DateTime> dtOpened { get; private set; }
+ public Nullable<DateTime> dtResolved { get; private set; }
+ public Nullable<DateTime> dtClosed { get; private set; }
+ public Nullable<DateTime> dtDue { get; private set; }
+ public Nullable<DateTime> dtLastUpdated { get; private set; }
+
+ public int? iBacklog { get; private set; }
+
+ public static string BacklogFieldName = "plugin_projectbacklog_at_fogcreek_com_ibacklog";
+
+ public Case(int ixBug, Op ops, Dictionary<String, String> fields, FogBugzClient parentClient)
+ {
+ this.ixBug = ixBug;
+ this.ops = ops;
+
+ if (!String.IsNullOrEmpty(fields["ixBugParent"]))
+ {
+ this.ixBugParent = Convert.ToInt32(fields["ixBugParent"]);
+ if (this.ixBugParent <= 0) this.ixBugParent = null;
+ }
+ else
+ {
+ this.ixBugParent = null;
+ }
+
+ this.ixBugChildren = new List<int>();
+ if (!String.IsNullOrEmpty(fields["ixBugChildren"]))
+ {
+ foreach (string ix in fields["ixBugChildren"].Split(','))
+ {
+ this.ixBugChildren.Add(Convert.ToInt32(ix));
+ }
+ }
+
+ this.ixBugEventLatest = Convert.ToInt32(fields["ixBugEventLatest"]);
+ this.ixBugEventLastView = Convert.ToInt32(fields["ixBugEventLastView"]);
+
+ this.category = parentClient.GetCategory(Convert.ToInt32(fields["ixCategory"]));
+ this.priority = new Priority(Convert.ToInt32(fields["ixPriority"]), fields["sPriority"]);
+ this.status = parentClient.GetStatus(Convert.ToInt32(fields["ixStatus"]));
+ this.project = new Project(Convert.ToInt32(fields["ixProject"]), fields["sProject"]);
+ this.area = new Area(Convert.ToInt32(fields["ixArea"]), fields["sArea"]);
+ this.fixfor = new FixFor(Convert.ToInt32(fields["ixFixFor"]), fields["sFixFor"]);
+
+ this.sTitle = fields["sTitle"];
+
+ this.assignedTo = parentClient.GetPerson(Convert.ToInt32(fields["ixPersonAssignedTo"]));
+ this.openedBy = parentClient.GetPerson(Convert.ToInt32(fields["ixPersonOpenedBy"]));
+ this.resolvedBy = parentClient.GetPerson(Convert.ToInt32(fields["ixPersonResolvedBy"]));
+ this.closedBy = parentClient.GetPerson(Convert.ToInt32(fields["ixPersonClosedBy"]));
+ this.lastEditedBy = parentClient.GetPerson(Convert.ToInt32(fields["ixPersonLastEditedBy"]));
+
+ this.hrsOrigEst = Estimate.Parse(fields["hrsOrigEst"]);
+ this.hrsCurrEst = Estimate.Parse(fields["hrsCurrEst"]);
+ this.hrsElapsed = Estimate.Parse(fields["hrsElapsed"]);
+
+ this.dtOpened = Util.ParseApiDate(fields["dtOpened"]);
+ this.dtResolved = Util.ParseApiDate(fields["dtResolved"]);
+ this.dtClosed = Util.ParseApiDate(fields["dtClosed"]);
+ this.dtDue = Util.ParseApiDate(fields["dtDue"]);
+ this.dtLastUpdated = Util.ParseApiDate(fields["dtLastUpdated"]);
+
+ if (fields.ContainsKey(BacklogFieldName) && !String.IsNullOrEmpty(fields[BacklogFieldName]))
+ {
+ this.iBacklog = Convert.ToInt32(fields[BacklogFieldName]);
+ }
+ }
+
+ /// <summary>
+ /// DataGridView's binding doesn't support subkeys. So cheat and add a shortcut.
+ /// </summary>
+ public Bitmap categoryImage
+ {
+ get
+ {
+ return category.Image;
+ }
+ }
+
+ public bool Unread
+ {
+ get
+ {
+ return ixBugEventLastView < ixBugEventLatest;
+ }
+ set
+ {
+ // An exception to our usual immutability, since the user might view a case
+ if (value)
+ {
+ ixBugEventLastView = ixBugEventLatest;
+ }
+ else
+ {
+ ixBugEventLastView = 0;
+ }
+ }
+ }
+
+ public void SetNewEstimate(Estimate e)
+ {
+ // A rare exception to Bug's immutability. Thus make a special wrapper method to set this.
+ hrsCurrEst = e;
+ }
+
+ internal void WorkStarted()
+ {
+ hrsElapsed = new Estimate(0.01m);
+ }
+ }
+}
|
|
|
@@ -0,0 +1,112 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Drawing;
+using System.Resources;
+using System.Reflection;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Category
+ {
+ private class IconTypeImageAttribute : Attribute
+ {
+ public readonly string FileName;
+
+ public IconTypeImageAttribute(String fileName)
+ {
+ this.FileName = fileName;
+ }
+ }
+
+ public enum IconType
+ {
+ [IconTypeImageAttribute("icon_none")]
+ None = 0,
+ [IconTypeImageAttribute("icon_bug")]
+ Bug = 1,
+ [IconTypeImageAttribute("icon_feature")]
+ Feature = 2,
+ [IconTypeImageAttribute("icon_inquiry")]
+ Inquiry = 3,
+ [IconTypeImageAttribute("icon_scheduleitem")]
+ ScheduleItem = 4,
+ [IconTypeImageAttribute("icon_bug_error")]
+ BugError = 5,
+ [IconTypeImageAttribute("icon_wrench")]
+ Wrench = 6,
+ [IconTypeImageAttribute("icon_magnifier")]
+ Magnifier = 7,
+ [IconTypeImageAttribute("icon_key")]
+ Key = 8,
+ [IconTypeImageAttribute("icon_error")]
+ Error = 9
+ }
+
+ public int ixCategory { get; private set; }
+ public string sCategory { get; private set; }
+ public bool fDeleted { get; private set; }
+ public int ixStatusDefault { get; private set; }
+ public IconType nIconType { get; private set; }
+
+ public Category(Dictionary<String, String> fields)
+ {
+ this.ixCategory = Convert.ToInt32(fields["ixCategory"]);
+ this.sCategory = fields["sCategory"];
+ this.fDeleted = Convert.ToBoolean(fields["fDeleted"]);
+ this.ixStatusDefault = Convert.ToInt32(fields["ixStatusDefault"]);
+ try
+ {
+ this.nIconType = (IconType)Enum.Parse(typeof(IconType), fields["nIconType"]);
+ }
+ catch
+ {
+ // Ignore in case more icons are added later.
+ }
+ }
+
+ public override string ToString()
+ {
+ return sCategory;
+ }
+
+ private static Dictionary<IconType, Bitmap> imageCache;
+
+ /// <summary>
+ /// Get the Category image. Properly belongs in the view, but .NET's bindings give us grief
+ /// if we try to format an image in the DataGridView.CellFormatting event. Just give in and
+ /// put it in the model.
+ /// </summary>
+ public Bitmap Image
+ {
+ get
+ {
+ if (imageCache == null)
+ {
+ imageCache = new Dictionary<IconType, Bitmap>();
+ ResourceManager manager = new ResourceManager("FogBugzForVisualStudio.Resource1", GetType().Assembly);
+
+ foreach (FieldInfo field in typeof(IconType).GetFields(BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public))
+ {
+ object[] attrs = field.GetCustomAttributes(true);
+ string fileName = null;
+ foreach (object attr in attrs)
+ {
+ if (attr.GetType().Equals(typeof(IconTypeImageAttribute)))
+ {
+ fileName = ((IconTypeImageAttribute)attr).FileName;
+ break;
+ }
+ }
+
+ if (fileName != null)
+ {
+ imageCache[(IconType)field.GetValue(null)] = (Bitmap)manager.GetObject(fileName);
+ }
+ }
+ }
+ return imageCache[nIconType];
+ }
+ }
+ }
+}
|
|
@@ -0,0 +1,44 @@ + using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Estimate
+ {
+ public decimal hrs { get; private set; }
+
+ public Estimate(decimal hrs)
+ {
+ this.hrs = hrs;
+ }
+
+ public static Estimate Parse(String s) {
+ return new Estimate(Convert.ToDecimal(s, CultureInfo.InvariantCulture));
+ }
+
+ public override string ToString()
+ {
+ if (hrs == 0) return "";
+
+ // Based on SFromHours in util.was
+ int nHours = Convert.ToInt32(Decimal.Floor(hrs));
+ int nMinutes = Convert.ToInt32(Decimal.Floor((hrs - nHours) * 60));
+
+ if (nHours == 0 && nMinutes == 0)
+ {
+ return "0 hours";
+ }
+ else if (nHours == 0)
+ {
+ return nMinutes == 1 ? "1 minute" : (nMinutes + " minutes");
+ }
+ else
+ {
+ double h = Math.Round(nHours + nMinutes/60.0, 2);
+ return h == 1 ? "1 hour" : (h + " hours");
+ }
+ }
+ }
+}
|
|
@@ -0,0 +1,21 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Filter
+ {
+ public enum FilterType
+ {
+ builtin = 1,
+ saved = 2,
+ shared = 4,
+ }
+
+ public FilterType type;
+ public string codeFilter; // opaque code
+ public string s; // user-visible name
+ public bool fIsCurrent; // is this the user's current filter?
+ }
+}
|
|
@@ -0,0 +1,23 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class FixFor
+ {
+ public readonly int ixFixFor;
+ public readonly string sFixFor;
+
+ public FixFor(int ixFixFor, string sFixFor)
+ {
+ this.ixFixFor = ixFixFor;
+ this.sFixFor = sFixFor;
+ }
+
+ public override string ToString()
+ {
+ return sFixFor;
+ }
+ }
+}
|
|
|
@@ -0,0 +1,1085 @@ + using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web;
+using System.Xml;
+
+
+namespace FogBugzForVisualStudio.Api
+{
+ /// <summary>
+ /// Event fires when Logon is done.
+ /// </summary>
+ /// <param name="bSuccess">true if successful</param>
+ /// <param name="sError">specific error message, if unsuccessful</param>
+ /// <param name="rgNames">if unsuccessful because sEmail was ambiguous, a list of full names user must choose between</param>
+ public delegate void LogonHandler(bool bSuccess, string sError, StringCollection rgNames);
+
+ /// <summary>
+ /// Event fires when List is done.
+ /// </summary>
+ /// <param name="bSuccess">true if successful</param>
+ /// <param name="sError">specific error message, if unsuccessful</param>
+ /// <param name="sListDescription">e.g. "All open cases assigned to Sophia"</param>
+ /// <param name="rgCases">the cases themselves</param>
+ public delegate void ListCasesHandler(bool bSuccess, string sError, string sListDescription, List<Case> rgCasesInCurrentFilter, List<Case>rgParentAndSubcases);
+
+ /// <summary>
+ /// Event fires if user is suddenly discovered to have logged off
+ /// (logon token no longer works)
+ /// </summary>
+ public delegate void UserLoggedOffHandler();
+
+ /// <summary>
+ /// Event fires when ListFilters is done
+ /// </summary>
+ /// <param name="bSuccess"></param>
+ /// <param name="sError"></param>
+ /// <param name="rgFilters"></param>
+ public delegate void ListFiltersHandler(bool bSuccess, string sError, List<Filter> rgFilters);
+
+ /// <summary>
+ /// Event fires when ListIntervals is done
+ /// </summary>
+ /// <param name="bSuccess"></param>
+ /// <param name="rgIntervals"></param>
+ public delegate void ListIntervalsHandler(bool bSuccess, string sError, List<Interval> rgIntervals);
+
+ /// <summary>
+ /// Event fires *instead of* the event you were expecting
+ /// (LogonHandler, ListCasesHandler, UserLoggedOffHandler, etc)
+ /// to tell you that your Stop() request has been honored
+ /// </summary>
+ public delegate void AsyncOperationCancelled();
+
+ /// <summary>
+ /// A C# class that implements a client to the FogBugz version 1 API.
+ /// </summary>
+ public class FogBugzClient
+ {
+ private string _sEmail;
+ private string _sPassword;
+ private string _sToken;
+ private WebClient _wc = null;
+ private string _lastCertError = "";
+ private bool _retryWithHttp = false;
+
+ public string sToken
+ {
+ get { return _sToken; }
+ }
+
+ /// <summary>
+ /// The URL of FogBugz itself
+ /// </summary>
+ private string _url = "" ;
+ private string _params = "";
+
+ private Dictionary<int, Person> dictPeople;
+ private Dictionary<int, Status> dictStatuses;
+ private Dictionary<int, Category> dictCategories;
+
+ public virtual Person GetPerson(int ixPerson)
+ {
+ return (dictPeople.ContainsKey(ixPerson)) ? dictPeople[ixPerson] : null;
+ }
+
+ public virtual Status GetStatus(int ixStatus)
+ {
+ return (dictStatuses.ContainsKey(ixStatus)) ? dictStatuses[ixStatus] : null;
+ }
+
+ public virtual Category GetCategory(int ixCategory)
+ {
+ return (dictCategories.ContainsKey(ixCategory)) ? dictCategories[ixCategory] : null;
+ }
+
+ /// <summary>
+ /// The complete url of the API, e.g. "http://www.example.com/fogbugz/api.asp?"
+ /// Not set until CheckAPIVersion has succeeded
+ /// </summary>
+ private string _urlApi = "";
+
+ public string sApiUrl
+ {
+ get { return _urlApi; }
+ }
+
+ public bool? fHasBacklogPlugin;
+
+ public event LogonHandler OnLogon;
+ public event ListCasesHandler OnListCases;
+ public event UserLoggedOffHandler OnUserLoggedOff;
+ public event ListFiltersHandler OnListFilters;
+ public event ListIntervalsHandler OnListIntervals;
+ public event AsyncOperationCancelled OnStop;
+
+ public FogBugzClient()
+ {
+ }
+
+ public FogBugzClient(string sApiUrl, string sToken)
+ {
+ _urlApi = sApiUrl;
+ _sToken = sToken;
+
+ StartPostLogonInit();
+ }
+
+ public bool LoggedOn()
+ {
+ return !String.IsNullOrEmpty(_sToken) && !String.IsNullOrEmpty(_urlApi)
+ && dictPeople != null && dictStatuses != null && dictCategories != null;
+ }
+
+ /// <summary>
+ /// Stops any operation in progress
+ /// </summary>
+ public void Stop()
+ {
+ if (_wc != null)
+ {
+ _wc.CancelAsync();
+ }
+ }
+
+ /// <summary>
+ /// Starts the logon process asynchronously. Fires LogonCompleted when done.
+ /// </summary>
+ /// <param name="url">FogBugz URL</param>
+ /// <param name="sEmail">email address or full name if email address is ambiguous</param>
+ /// <param name="sPassword">password</param>
+ public void Logon( string url, string sEmail, string sPassword )
+ {
+ _url = url;
+ _url = _url.Replace("default.asp", "");
+ _url = _url.Replace("default.php", "");
+ if (_url.Contains("?"))
+ {
+ _params = _url.Substring(_url.IndexOf("?") + 1);
+ _url = _url.Substring(0, _url.IndexOf("?"));
+ }
+
+ if (!_url.EndsWith("/")) _url += "/";
+
+ _sEmail = sEmail;
+ _sPassword = sPassword;
+
+ System.Net.ServicePointManager.ServerCertificateValidationCallback = OnRemoteCertificateValidation;
+
+ if (Regex.IsMatch(_url, @"http://[^.]+\.fogbugz\.com/"))
+ {
+ _url = "https://" + _url.Substring("http://".Length);
+ _retryWithHttp = true;
+ }
+
+ CheckAPIVersion();
+ }
+
+ /// <summary>
+ /// Check that the server is really a FogBugz server with the right version of the API
+ /// installed
+ /// </summary>
+ public void CheckAPIVersion()
+ {
+ InitiateDownload("", CheckAPIVersion_DownloadStringCompleted,
+ delegate(string sErrorMessage) { OnLogon(false, sErrorMessage, null); });
+ }
+
+ private void CheckAPIVersion_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error == null)
+ {
+ int iVersion = 0;
+ int iMinVersion = 0;
+ int iClientVersionOnServer = 0;
+ string sURL = "";
+ XmlReader xml;
+
+ try
+ {
+ xml = GetXmlReader(e.Result);
+ }
+ catch
+ {
+ var fFBOD = _url.EndsWith(".fogbugz.com/");
+ var msg = fFBOD ?
+ "FogBugz On Demand is currently undergoing maintenance." :
+ "Malformed api.xml.";
+
+ OnLogon(false, msg, null);
+ return;
+ }
+
+
+ bool fContinue = xml.Read();
+ while (fContinue)
+ {
+ if (xml.NodeType == XmlNodeType.Element)
+ {
+ switch (xml.Name)
+ {
+ case "version":
+ iVersion = xml.ReadElementContentAsInt();
+ break;
+
+ case "minversion":
+ iMinVersion = xml.ReadElementContentAsInt();
+ break;
+
+ case "url":
+ sURL = xml.ReadElementContentAsString();
+ break;
+
+ case "private":
+ if (xml.GetAttribute("id") == "FogBugz for Visual Studio")
+ {
+ iClientVersionOnServer = Convert.ToInt32(xml.GetAttribute("version"));
+ }
+ fContinue = xml.Read();
+ break;
+
+ default:
+ fContinue = xml.Read();
+ break;
+ }
+ }
+ else
+ {
+ fContinue = xml.Read();
+ }
+ }
+
+ if (iVersion == 0 || iMinVersion == 0 || sURL.Length == 0)
+ {
+ OnLogon(false, "Incomplete or missing api.xml", null);
+ }
+ else if (iMinVersion > 1)
+ {
+ OnLogon(false, "That version of FogBugz is too new. Please re-install from the \"Extras\" menu in FogBugz", null);
+ }
+ else if (iClientVersionOnServer > 2)
+ {
+ OnLogon(false, "Newer version of add-in available. Please re-install from the \"Extras\" menu in FogBugz", null);
+ }
+ else
+ {
+ _urlApi = _url + sURL;
+ StartLogon();
+ }
+ }
+ else
+ {
+ if (_retryWithHttp && Regex.IsMatch(_url, @"https://[^.]+\.fogbugz\.com/"))
+ {
+ _url = "http://" + _url.Substring("https://".Length);
+ _retryWithHttp = false;
+ CheckAPIVersion();
+ }
+ else if (e.Error is WebException)
+ {
+ WebException we = (WebException)e.Error;
+ if (we.Status == WebExceptionStatus.TrustFailure)
+ {
+ // we can get better error messages for SSL failures.
+ OnLogon(false, _lastCertError, null);
+ }
+ else if (e.Error.Message.Contains("404"))
+ {
+ OnLogon(false, e.Error.Message + " FogBugz API not installed on server.", null);
+ }
+ else
+ {
+ OnLogon(false, e.Error.Message, null);
+ }
+ }
+ else
+ {
+ OnLogon(false, e.Error.Message, null);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Passed version check; start logging on
+ /// </summary>
+ private void StartLogon()
+ {
+ _sToken = ""; // clear any old tokens - otherwise logon won't happen if we're passing an invalid token
+ InitiateDownload( UrlParams("cmd","logon","email",_sEmail,"password",_sPassword),
+ StartLogon_DownloadStringCompleted,
+ delegate(string sErrorMessage) { OnLogon(false, sErrorMessage, null); } );
+ }
+
+ private void StartLogon_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnLogon(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result.Trim());
+
+ bool fAmbiguousLogon = false;
+ StringCollection rgsFullNames = new StringCollection();
+ string sErrAmbiguous = "";
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element)
+ {
+ xml.Read();
+ continue;
+ }
+
+ switch (xml.Name)
+ {
+ case "token":
+ _sToken = xml.ReadElementContentAsString();
+
+ // We've successfully logged on, so download a list of people.
+ StartPostLogonInit();
+
+ return;
+
+ case "error":
+ switch (xml.GetAttribute("code"))
+ {
+ case "1":
+ OnLogon(false, xml.ReadElementContentAsString(), null);
+ return;
+ case "2":
+ sErrAmbiguous = xml.ReadElementContentAsString();
+ fAmbiguousLogon = true;
+ continue;
+ default:
+ OnLogon(false, "Unknown Error: " + xml.ReadElementContentAsString(), null);
+ return;
+ }
+
+ case "person":
+ rgsFullNames.Add(xml.ReadElementContentAsString());
+ break;
+
+ default:
+ xml.Read();
+ break;
+ }
+ }
+
+ if (fAmbiguousLogon)
+ {
+ OnLogon(false, sErrAmbiguous, rgsFullNames);
+ }
+ else
+ {
+ OnLogon(false, "Unknown error; unrecognized XML response.", null);
+ }
+ }
+
+ private void StartPostLogonInit()
+ {
+ InitiateDownload(UrlParams("cmd", "listPeople", "fIncludeNormal", "1", "fIncludeVirtual", "1"),
+ ListPeople_DownloadStringCompleted,
+ delegate(string s) { OnLogon(false, "Unable to download list of people.", null); });
+ }
+
+ private void ListPeople_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnLogon(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+
+ dictPeople = new Dictionary<int, Person>();
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element || xml.Name != "person")
+ {
+ xml.Read();
+ continue;
+ }
+
+ try
+ {
+ var fields = ReadSubElementsAsDictionary(xml);
+ var person = new Person(fields);
+ dictPeople[person.ixPerson] = person;
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ OnLogon(false, "API returned invalid <person>", null);
+ return;
+ }
+ }
+
+ InitiateDownload(UrlParams("cmd", "listStatuses"),
+ ListStatuses_DownloadStringCompleted,
+ delegate(string s) { OnLogon(false, "Unable to download a list of statuses", null); });
+ }
+
+ private void ListStatuses_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnLogon(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+
+ dictStatuses = new Dictionary<int, Status>();
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element || xml.Name != "status")
+ {
+ xml.Read();
+ continue;
+ }
+
+ try
+ {
+ var fields = ReadSubElementsAsDictionary(xml);
+ var status = new Status(fields);
+ dictStatuses[status.ixStatus] = status;
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ OnLogon(false, "API returned invalid <status>", null);
+ return;
+ }
+ }
+
+ InitiateDownload(UrlParams("cmd", "listCategories"),
+ ListCategories_DownloadStringCompleted,
+ delegate(string s) { OnLogon(false, "Unable to download a list of categories", null); });
+ }
+
+ private void ListCategories_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnLogon(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+
+ dictCategories = new Dictionary<int, Category>();
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element || xml.Name != "category")
+ {
+ xml.Read();
+ continue;
+ }
+
+ try
+ {
+ var fields = ReadSubElementsAsDictionary(xml);
+ var category = new Category(fields);
+ dictCategories[category.ixCategory] = category;
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ OnLogon(false, "API returned invalid <category>", null);
+ return;
+ }
+ }
+
+ OnLogon(true, "", null);
+ }
+
+ public void LogOff()
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ InitiateDownload(UrlParams("cmd", "logoff"),
+ null,
+ null);
+ _sToken = "";
+ }
+
+ public void SetCaseEstimate(Case c, string estimate)
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+
+ var response = SynchronousDownload(UrlParams("cmd", "edit", "ixBug", c.ixBug.ToString(), "hrsCurrEst", estimate, "cols", "hrsCurrEst"));
+
+ XmlReader xml = GetXmlReader(response);
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType == XmlNodeType.Element && xml.Name == "hrsCurrEst")
+ {
+ c.SetNewEstimate(new Estimate(xml.ReadElementContentAsDecimal()));
+ return;
+ }
+ else if (xml.NodeType == XmlNodeType.Element && xml.Name == "error")
+ {
+ throw new FogBugzErrorException(xml.ReadElementContentAsString(), Convert.ToInt32(xml.GetAttribute("code")));
+ }
+ else xml.Read();
+ }
+ }
+
+ private void CheckForErrors(string response)
+ {
+ var xml = GetXmlReader(response);
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType == XmlNodeType.Element && xml.Name == "error")
+ {
+ throw new FogBugzErrorException(xml.ReadElementContentAsString(), Convert.ToInt32(xml.GetAttribute("code")));
+ }
+ else xml.Read();
+ }
+ }
+
+ public void StartWork(Case c)
+ {
+ StartWork(c.ixBug);
+ c.WorkStarted();
+ }
+
+ public void StartWork(int ixBug)
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ CheckForErrors(SynchronousDownload(UrlParams("cmd", "startWork", "ixBug", ixBug.ToString())));
+ }
+
+ public void StopWork()
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ CheckForErrors(SynchronousDownload(UrlParams("cmd", "stopWork")));
+ }
+
+ public void ListIntervals(DateTime? dtStart, DateTime? dtEnd)
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ InitiateDownload(UrlParams("cmd", "listIntervals", "cols", sListCols, "dtStart", dtStart.HasValue ? Util.FormatApiDate(dtStart.Value) : "", "dtEnd", dtEnd.HasValue ? Util.FormatApiDate(dtEnd.Value) : ""),
+ delegate(Object sender, DownloadStringCompletedEventArgs e) { ListIntervals_DownloadStringCompleted(sender, e); },
+ delegate(string s) { OnListIntervals(false, s, null); });
+ }
+
+ private void ListIntervals_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnListIntervals(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+
+ var rgIntervals = new List<Interval>();
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element)
+ {
+ xml.Read();
+ continue;
+ }
+ switch (xml.Name)
+ {
+ case "error":
+ if (xml.GetAttribute("code") == "3")
+ {
+ OnListIntervals(false, xml.ReadElementContentAsString(), null);
+ OnUserLoggedOff();
+ }
+ else
+ {
+ OnListIntervals(false, xml.ReadElementContentAsString(), null);
+ }
+ return;
+
+ case "interval":
+ try
+ {
+ rgIntervals.Add(new Interval(ReadSubElementsAsDictionary(xml), this));
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ OnListIntervals(false, "API returned invalid <interval>", null);
+ return;
+ }
+ break;
+
+ default:
+ xml.Read();
+ break;
+ }
+ }
+
+ OnListIntervals(true, "", rgIntervals);
+ }
+
+ private static string sListCols = "ixBug,ixBugParent,ixBugChildren,sTitle,ixProject,sProject,ixArea,sArea,ixPersonAssignedTo,ixPersonOpenedBy,ixPersonResolvedBy,ixPersonClosedBy,ixPersonLastEditedBy,ixStatus,ixPriority,sPriority,ixFixFor,sFixFor,hrsOrigEst,hrsCurrEst,hrsElapsed,ixCategory,dtOpened,dtResolved,dtClosed,dtLastUpdated,dtDue,ixBugEventLatest,ixBugEventLastView,plugin";
+
+ /// <summary>
+ /// Starts downloading a list of cases in the user's current filter.
+ /// </summary>
+ public void ListCases()
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ InitiateDownload( UrlParams("cmd", "search", "cols", sListCols),
+ delegate(Object sender, DownloadStringCompletedEventArgs e) { ListCases_DownloadStringCompleted(sender, e, null, null, null); },
+ delegate(string s) { OnListCases(false, s, null, null, null); });
+ }
+
+ /// <summary>
+ /// Responds to a payload from a list cases command. This may be the main call from ListCases, listing the cases in the
+ /// user's current filter, or from a followup call that is resolving subcases and parent cases. The two cases can be
+ /// differentiated by rgCasesInCurrentFilter.
+ /// </summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
+ /// <param name="rgCasesInCurrentFilter">If null, the payload contains list of cases in current filter. Otherwise, payload contains assorted parent cases and subcases for the cases in this array.</param>
+ /// <param name="sDescription">If null, fill in from payload. Otherwise this is a supplementary call loading parent and subcases, and the value is already loaded.</param>
+ /// <param name="rgParentAndSubcases">It may take multiple passes to get a complete tree of cases (resolve all parent and subcases). If so, this list holds an intermediate array.</param>
+ private void ListCases_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e, List<Case> rgCasesInCurrentFilter, string sDescription, List<Case> rgParentAndSubcases)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnListCases(false, e.Error.Message, null, null, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+
+ if (rgParentAndSubcases == null)
+ {
+ rgParentAndSubcases = new List<Case>();
+ }
+
+ List<Case> rgCaseDestination;
+ if (rgCasesInCurrentFilter == null)
+ {
+ rgCasesInCurrentFilter = new List<Case>();
+ rgCaseDestination = rgCasesInCurrentFilter;
+ }
+ else
+ {
+ rgCaseDestination = rgParentAndSubcases;
+ }
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element)
+ {
+ xml.Read();
+ continue;
+ }
+ switch (xml.Name)
+ {
+ case "error":
+ if (xml.GetAttribute("code") == "3")
+ {
+ OnListCases(false, xml.ReadElementContentAsString(), null, null, null);
+ OnUserLoggedOff();
+ }
+ else
+ {
+ OnListCases(false, xml.ReadElementContentAsString(), null, null, null);
+ }
+ return;
+
+ case "description":
+ sDescription = xml.ReadElementContentAsString();
+ break;
+
+ case "case":
+ try
+ {
+ var ixBug = Convert.ToInt32(xml.GetAttribute("ixBug"));
+ var ops = (Case.Op)Enum.Parse(typeof(Case.Op), xml.GetAttribute("operations"));
+ var els = ReadSubElementsAsDictionary(xml);
+ fHasBacklogPlugin = els.ContainsKey(Case.BacklogFieldName);
+ rgCaseDestination.Add(new Case(ixBug, ops, els, this));
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ OnListCases(false, "API returned invalid <case>", null, null, null);
+ return;
+ }
+ break;
+
+ default:
+ xml.Read();
+ break;
+ }
+ }
+
+ var rgIxBugUnresolved = ComputeUnresolvedCases(rgCasesInCurrentFilter, rgParentAndSubcases);
+ if (rgIxBugUnresolved.Count > 0)
+ {
+ string sIxBugQuery = "";
+ foreach (int ixBug in rgIxBugUnresolved)
+ {
+ sIxBugQuery += "," + Convert.ToString(ixBug);
+ }
+ sIxBugQuery = sIxBugQuery.Substring(1);
+
+ Debug.WriteLine("Requesting bugs " + sIxBugQuery);
+
+ InitiateDownload( UrlParams("cmd", "search", "cols", sListCols, "q", sIxBugQuery),
+ delegate(Object sender2, DownloadStringCompletedEventArgs e2) { ListCases_DownloadStringCompleted(sender2, e2, rgCasesInCurrentFilter, sDescription, rgParentAndSubcases); },
+ delegate(string s) { OnListCases(false, s, null, null, null); });
+ }
+ else
+ {
+ OnListCases(true, "", sDescription, rgCasesInCurrentFilter, rgParentAndSubcases);
+ }
+ }
+
+ private List<int> ComputeUnresolvedCases(List<Case> rgCasesInCurrentFilter, List<Case> rgParentAndSubcases)
+ {
+ var dictAllCases = new Dictionary<int, Case>();
+
+ var queue = new Queue<Case>();
+ foreach (var c in rgCasesInCurrentFilter)
+ {
+ queue.Enqueue(c);
+ dictAllCases[c.ixBug] = c;
+ }
+ foreach (var c in rgParentAndSubcases)
+ {
+ queue.Enqueue(c);
+ dictAllCases[c.ixBug] = c;
+ }
+
+ // Dictionary with a dummy value: the poor man's Set class.
+ var dictUnresolved = new Dictionary<int, int>();
+ while (queue.Count > 0)
+ {
+ var c = queue.Dequeue();
+ if (c.ixBugParent.HasValue && !dictAllCases.ContainsKey((int)c.ixBugParent)) dictUnresolved[(int)c.ixBugParent] = 0;
+ foreach (var ixBugChild in c.ixBugChildren)
+ {
+ if (!dictAllCases.ContainsKey(ixBugChild)) dictUnresolved[ixBugChild] = 0;
+ }
+ }
+
+ var rgUnresolved = new List<int>();
+ foreach (var ixBug in dictUnresolved.Keys)
+ {
+ rgUnresolved.Add(ixBug);
+ }
+ return rgUnresolved;
+ }
+
+ private Dictionary<String,String> ReadSubElementsAsDictionary(XmlReader xml)
+ {
+ var fields = new Dictionary<String, String>();
+
+ if (xml.NodeType != XmlNodeType.Element)
+ {
+ throw new InvalidOperationException("ReadSubElementsAsDictionary was called when not positioned on an element");
+ }
+
+ String elementName = xml.Name;
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType == XmlNodeType.EndElement && xml.Name == elementName)
+ {
+ xml.Read(); // Advance past end element, just like ReadElementContentAsString
+ return fields;
+ }
+
+ if (xml.NodeType == XmlNodeType.Element)
+ fields[xml.Name] = xml.ReadElementContentAsString();
+ else
+ xml.Read();
+ }
+
+ throw new XmlException("ReadSubElementsAsDictionary reached EOF without reaching end of element.");
+ }
+
+ public void ListFilters()
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ InitiateDownload(UrlParams("cmd", "listFilters"),
+ ListFilters_DownloadStringCompleted,
+ delegate(string s) { OnListFilters(false, s, null); });
+
+ }
+
+ private void ListFilters_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnListFilters(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+ List<Filter> rgFilters = new List<Filter>();
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType == XmlNodeType.Element && xml.Name == "filter")
+ {
+ Filter f = new Filter();
+ f.codeFilter = xml.GetAttribute("sFilter");
+ f.type = (Filter.FilterType)Enum.Parse(typeof(Filter.FilterType), xml.GetAttribute("type"));
+ f.fIsCurrent = (xml.GetAttribute("status") == "current");
+ f.s = xml.ReadElementContentAsString().Trim();
+ rgFilters.Add(f);
+ }
+ else
+ {
+ xml.Read();
+ }
+ }
+ OnListFilters(true, "", rgFilters);
+ }
+
+ public void SaveFilterAndList(string codeFilter)
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ InitiateDownload(UrlParams("cmd", "setCurrentFilter", "sFilter", codeFilter),
+ SaveFilter_DownloadStringCompleted,
+ delegate(string s) { OnListCases(false, s, null, null, null); });
+
+ }
+
+ private void SaveFilter_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error == null)
+ {
+ ListCases();
+ }
+ }
+
+ public string GetDefaultUrl()
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ return _urlApi.Replace("api.", "default.");
+ }
+
+ //
+ // UTILITY FUNCTIONS
+ //
+
+ private delegate void InitiateDownloadFailureHandler( string sErrorMessage );
+
+ /// <summary>
+ /// Initiates a download.
+ /// </summary>
+ /// <param name="sParams">GET params, for example, "cmd=logoff"; use UrlParams to generate. If blank, downloads api.xml.</param>
+ /// <param name="h">handler to call when string is received</param>
+ /// <param name="hFail">handler to call if exception is raised even before download begins</param>
+ private void InitiateDownload(string sParams,
+ DownloadStringCompletedEventHandler h,
+ InitiateDownloadFailureHandler hFail
+ )
+ {
+ _wc = new WebClient();
+ _wc.UseDefaultCredentials = true;
+ _wc.Proxy.Credentials = CredentialCache.DefaultCredentials;
+ _wc.Encoding = Encoding.UTF8;
+
+ if (h != null)
+ {
+ _wc.DownloadStringCompleted += BugScout.CrashProof(h);
+ }
+
+ try
+ {
+ string surl = "";
+ if (sParams.Length == 0)
+ {
+ surl = _url + "api.xml?" + _params;
+ }
+ else
+ {
+ surl = _urlApi;
+ if (_params.Length > 0)
+ {
+ surl += _params + "&";
+ }
+ surl += sParams;
+ }
+
+ _wc.DownloadStringAsync(new Uri(surl));
+ }
+ catch(Exception e)
+ {
+ BugScout.ReportException(e);
+ if (hFail != null)
+ {
+ BugScout.CatchAndReport(() => {
+ hFail(e.Message);
+ });
+ }
+ }
+ }
+
+ private string SynchronousDownload(string sParams)
+ {
+ _wc = new WebClient();
+ _wc.UseDefaultCredentials = true;
+ _wc.Proxy.Credentials = CredentialCache.DefaultCredentials;
+ _wc.Encoding = Encoding.UTF8;
+
+ string surl = _urlApi;
+ if (_params.Length > 0)
+ {
+ surl += _params + "&";
+ }
+ surl += sParams;
+
+ return _wc.DownloadString(new Uri(surl));
+ }
+
+ private string UrlParams(params string[] rgs)
+ {
+ Debug.Assert(rgs.Length % 2 == 0);
+
+ string s = "token=" + HttpUtility.UrlEncode(_sToken);
+ for (int i = 0; i < rgs.Length; i += 2)
+ {
+ s += "&" + HttpUtility.UrlEncode(rgs[i]) + "=" + HttpUtility.UrlEncode(rgs[i + 1]);
+ }
+ return s;
+ }
+
+ private XmlReader GetXmlReader(string sxml)
+ {
+ XmlReaderSettings st = new XmlReaderSettings();
+ st.IgnoreComments = true;
+ st.IgnoreWhitespace = true;
+ st.ConformanceLevel = ConformanceLevel.Fragment;
+ st.ValidationType = ValidationType.None;
+
+ XmlReader xml = XmlReader.Create(new StringReader(sxml.Trim()), st); // Trim() removes BOM so <?xml is at the beginning
+
+ xml.MoveToContent();
+
+ return xml;
+ }
+
+ public bool OnRemoteCertificateValidation(Object sender,
+ X509Certificate certificate,
+ X509Chain chain,
+ SslPolicyErrors sslPolicyErrors)
+ {
+ switch (sslPolicyErrors)
+ {
+ case SslPolicyErrors.None:
+ return true;
+
+ case SslPolicyErrors.RemoteCertificateChainErrors:
+ _lastCertError = "SSL Error: " + chain.ChainElements[0].ChainElementStatus[0].StatusInformation.Trim();
+ break;
+
+ case SslPolicyErrors.RemoteCertificateNameMismatch:
+ _lastCertError = "SSL Error: The certificate name does not match the name in the URL.";
+ break;
+
+ case SslPolicyErrors.RemoteCertificateNotAvailable:
+ _lastCertError = "SSL Error: The remote certificate is not available.";
+ break;
+ }
+
+ return false;
+ }
+ }
+
+ class FogBugzNotLoggedOnException : Exception { }
+
+ class FogBugzErrorException : Exception
+ {
+ public int Code { get; private set; }
+
+ public FogBugzErrorException(string msg, int code) : base(msg)
+ {
+ Code = code;
+ }
+ }
+}
|
|
@@ -0,0 +1,36 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Interval
+ {
+ public int ixInterval { get; private set; }
+
+ public Person Person { get; private set; }
+
+ public int ixBug { get; private set; }
+ public string sTitle { get; private set; }
+
+ public DateTime? dtStart { get; private set; }
+ public DateTime? dtEnd { get; private set; }
+
+ public bool fDeleted { get; private set; }
+
+ public Interval(Dictionary<String, String> fields, FogBugzClient parentClient)
+ {
+ this.ixInterval = Convert.ToInt32(fields["ixInterval"]);
+
+ this.Person = parentClient.GetPerson(Convert.ToInt32(fields["ixPerson"]));
+
+ this.ixBug = Convert.ToInt32(fields["ixBug"]);
+ this.sTitle = fields["sTitle"];
+
+ this.dtStart = Util.ParseApiDate(fields["dtStart"]);
+ this.dtEnd = Util.ParseApiDate(fields["dtEnd"]);
+
+ this.fDeleted = Convert.ToBoolean(fields["fDeleted"]);
+ }
+ }
+}
|
|
@@ -0,0 +1,25 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Person
+ {
+ public readonly int ixPerson;
+ public readonly string sFullName;
+ public readonly string sEmail;
+
+ public Person(Dictionary<String, String> fields)
+ {
+ this.ixPerson = Convert.ToInt32(fields["ixPerson"]);
+ this.sFullName = fields["sFullName"];
+ this.sEmail = fields["sEmail"];
+ }
+
+ public override string ToString()
+ {
+ return sFullName;
+ }
+ }
+}
|
|
@@ -0,0 +1,23 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Priority
+ {
+ public readonly int ixPriority;
+ public readonly string sPriority;
+
+ public Priority(int ixPriority, string sPriority)
+ {
+ this.ixPriority = ixPriority;
+ this.sPriority = sPriority;
+ }
+
+ public override string ToString()
+ {
+ return ixPriority.ToString() + " - " + sPriority;
+ }
+ }
+}
|
|
@@ -0,0 +1,23 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Project
+ {
+ public readonly int ixProject;
+ public readonly string sProject;
+
+ public Project(int ixProject, string sProject)
+ {
+ this.ixProject = ixProject;
+ this.sProject = sProject;
+ }
+
+ public override string ToString()
+ {
+ return sProject;
+ }
+ }
+}
|
|
@@ -0,0 +1,31 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Status
+ {
+ public readonly int ixStatus;
+ public readonly string sStatus;
+ public readonly bool fDeleted;
+ public readonly int ixCategory;
+ public readonly bool fResolved;
+ public readonly int iOrder;
+
+ public Status(Dictionary<String, String> fields)
+ {
+ this.ixStatus = Convert.ToInt32(fields["ixStatus"]);
+ this.sStatus = fields["sStatus"];
+ this.fDeleted = Convert.ToBoolean(fields["fDeleted"]);
+ this.ixCategory = Convert.ToInt32(fields["ixCategory"]);
+ this.fResolved = Convert.ToBoolean(fields["fResolved"]);
+ this.iOrder = Convert.ToInt32(fields["iOrder"]);
+ }
+
+ public override string ToString()
+ {
+ return sStatus;
+ }
+ }
+}
|
|
@@ -0,0 +1,30 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Globalization;
+
+namespace FogBugzForVisualStudio.Api
+{
+ static class Util
+ {
+ public static Nullable<DateTime> ParseApiDate(string s)
+ {
+ // Shortcut for empty strings
+ if (String.IsNullOrEmpty(s)) return null;
+
+ try
+ {
+ return DateTime.ParseExact(s, "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToLocalTime();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public static string FormatApiDate(DateTime dt)
+ {
+ return dt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
+ }
+ }
+}
|
|
@@ -0,0 +1,51 @@ + using System.Reflection;
+using System.Runtime.CompilerServices;
+
+//
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+//
+[assembly: AssemblyTitle("Fog Creek Software, Inc.")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+//
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Revision
+// Build Number
+//
+// You can specify all the value or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("3.2.*")] // mhp - bump for working with vs2012
+
+//
+// In order to sign your assembly you must specify a key to use. Refer to the
+// Microsoft .NET Framework documentation for more information on assembly signing.
+//
+// Use the attributes below to control which key is used for signing.
+//
+// Notes:
+// (*) If no key is specified - the assembly cannot be signed.
+// (*) KeyName refers to a key that has been installed in the Crypto Service
+// Provider (CSP) on your machine.
+// (*) If the key file and a key name attributes are both specified, the
+// following processing occurs:
+// (1) If the KeyName can be found in the CSP - that key is used.
+// (2) If the KeyName does not exist and the KeyFile does exist, the key
+// in the file is installed into the CSP and used.
+// (*) Delay Signing is an advanced option - see the Microsoft .NET Framework
+// documentation for more information on this.
+//
+[assembly: AssemblyDelaySign(false)]
+[assembly: AssemblyKeyFile("")]
+[assembly: AssemblyKeyName("")]
|
|
@@ -0,0 +1,101 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Net;
+using System.Web;
+
+namespace FogBugzForVisualStudio
+{
+ class UrlParams : System.Collections.IEnumerable {
+ private List<KeyValuePair<string, string>> parms = new List<KeyValuePair<string, string>>();
+
+ public void Add(string name, string value) {
+ parms.Add(new KeyValuePair<string, string>(name, value));
+ }
+
+ public override String ToString() {
+ return String.Join("&", System.Array.ConvertAll(parms.ToArray(), (pair) => {
+ return String.Format("{0}={1}", Encode(pair.Key), Encode(pair.Value));
+ }));
+ }
+
+ private String Encode(String s) {
+ return HttpUtility.UrlEncode(s);
+ }
+
+ public System.Collections.IEnumerator GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ class BugScout
+ {
+ public static void ReportException(Exception ex)
+ {
+ ReportException(ex, "");
+ }
+
+ public static void ReportException(Exception ex, String extra)
+ {
+ ReportError(ex.Message, String.Format(
+ "[code]\n{0}\n[/code]\n\n{1}",
+ ex.ToString(), extra
+ ));
+ }
+
+ public static void ReportError(String title)
+ {
+ ReportError(title, "");
+ }
+
+ public static void ReportError(String title, String extra){
+ #if !DEBUG
+ if (!Connect.ReportErrors)
+ {
+ return;
+ }
+ // This is where errors can be reported to any system you prefer.
+ // We use BugzScout, and have included some sample code to get
+ // you started capturing error reports in FogBugz. You'll
+ // need to update the URL and parameter values below. For more
+ // information, please visit:
+ // http://help.fogcreek.com/7566/bugzscout-for-automatic-crash-reporting
+ //
+ //var url = "https://your-site.fogbugz.com/scoutSubmit.asp";
+ //var client = new WebClient();
+ //client.DownloadStringAsync(new Uri(url + "?" + new UrlParams{
+ // {"ScoutUserName", "Your BugzScout User},
+ // {"ScoutProject", "Your BugzScout Project"},
+ // {"ScoutArea", "Your BugzScout Area"},
+ // {"description", "VS Add-In: " title},
+ // {"extra", extra}
+ //}.ToString()));
+
+ #endif
+ }
+
+ public delegate void Action();
+
+ public static DownloadStringCompletedEventHandler CrashProof(DownloadStringCompletedEventHandler action)
+ {
+ return (a, b) =>
+ {
+ CatchAndReport(() => {
+ action(a, b);
+ });
+ };
+ }
+
+ public static void CatchAndReport(Action action){
+ try
+ {
+ action();
+ }
+ catch (Exception ex)
+ {
+ ReportException(ex);
+ }
+ }
+ }
+}
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
@@ -0,0 +1,63 @@ + //------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.1
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace FogBugzForVisualStudio {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class CommandBar {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal CommandBar() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FogBugzForVisualStudio.CommandBar", typeof(CommandBar).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
|
|
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
@@ -0,0 +1,36 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+using Microsoft.Win32;
+
+namespace FogBugzForVisualStudio
+{
+ static class RegistryHelper
+ {
+ public static RegistryKey FogBugzVSKey
+ {
+ get
+ {
+ return Registry.CurrentUser.CreateSubKey(@"Software\Fog Creek Software\FogBugz For Visual Studio");
+ }
+ }
+
+ public static RegistryKey FogBugzVSMachineKey
+ {
+ get
+ {
+ return Registry.LocalMachine.CreateSubKey(@"Software\Fog Creek Software\FogBugz For Visual Studio");
+ }
+ }
+
+ public static bool? GetBool(RegistryKey obj, String key) {
+ var result = obj.GetValue(key, null);
+ if (result == null) {
+ return null;
+ }
+
+ return Convert.ToBoolean(result);
+ }
+ }
+}
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
@@ -0,0 +1,18 @@ + using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Text;
+using System.Windows.Forms;
+
+namespace FogBugzForVisualStudio
+{
+ public partial class frmSetEstimate : Form
+ {
+ public frmSetEstimate()
+ {
+ InitializeComponent();
+ }
+ }
+}
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
Loading...