Feature: Quick Search
Device/Job/User Search refactoring. Quick-Search implemented.
This commit is contained in:
@@ -173,6 +173,7 @@
|
||||
<Compile Include="Authorization\Roles\RoleClaims.cs" />
|
||||
<Compile Include="Authorization\Roles\RoleToken.cs" />
|
||||
<Compile Include="Extensions\DateTimeExtensions.cs" />
|
||||
<Compile Include="Extensions\StringExtensions.cs" />
|
||||
<Compile Include="Jobs\JobExtensions.cs" />
|
||||
<Compile Include="Jobs\JobLists\JobTableExtensions.cs" />
|
||||
<Compile Include="Jobs\JobQueues\Cache.cs" />
|
||||
@@ -230,6 +231,7 @@
|
||||
<Compile Include="Plugins\PluginWebViewPage.cs" />
|
||||
<Compile Include="Plugins\WebPageHelper.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Searching\Search.cs" />
|
||||
<Compile Include="Tasks\ScheduledTask.cs" />
|
||||
<Compile Include="Tasks\ScheduledTasks.cs" />
|
||||
<Compile Include="Tasks\ScheduledTasksLog.cs" />
|
||||
@@ -278,7 +280,7 @@
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<UserProperties BuildVersion_UpdateFileVersion="True" BuildVersion_UpdateAssemblyVersion="True" BuildVersion_BuildVersioningStyle="None.DeltaBaseYear.MonthAndDayStamp.TimeStamp" BuildVersion_StartDate="2011/7/1" BuildVersion_BuildAction="Both" BuildVersion_DetectChanges="False" BuildVersion_UseGlobalSettings="False" />
|
||||
<UserProperties BuildVersion_UseGlobalSettings="False" BuildVersion_DetectChanges="False" BuildVersion_BuildAction="Both" BuildVersion_StartDate="2011/7/1" BuildVersion_BuildVersioningStyle="None.DeltaBaseYear.MonthAndDayStamp.TimeStamp" BuildVersion_UpdateAssemblyVersion="True" BuildVersion_UpdateFileVersion="True" />
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A fuzzy string search algorithm.
|
||||
///
|
||||
/// Based on: ScoreSharp (https://github.com/bltavares/scoresharp)
|
||||
/// Based on: string_score from Joshaven Potter (https://github.com/joshaven/string_score)
|
||||
///
|
||||
/// MIT License
|
||||
/// </summary>
|
||||
public static double Score(this string Source, string Test, double Fuzziness = 0)
|
||||
{
|
||||
double total_char_score = 0, abbrv_size = Test.Length,
|
||||
fuzzies = 1, final_score, abbrv_score;
|
||||
int word_size = Source.Length;
|
||||
bool start_of_word_bonus = false;
|
||||
|
||||
//If strings are equal, return 1.0
|
||||
if (Source == Test) return 1.0;
|
||||
|
||||
int index_in_string,
|
||||
index_char_lowercase,
|
||||
index_char_uppercase,
|
||||
min_index;
|
||||
double char_score;
|
||||
string c;
|
||||
for (int i = 0; i < abbrv_size; i++)
|
||||
{
|
||||
c = Test[i].ToString();
|
||||
index_char_uppercase = Source.IndexOf(c.ToUpper());
|
||||
index_char_lowercase = Source.IndexOf(c.ToLower());
|
||||
min_index = Math.Min(index_char_lowercase, index_char_uppercase);
|
||||
|
||||
//Finds first valid occurrence
|
||||
//In upper or lowercase
|
||||
index_in_string = min_index > -1 ?
|
||||
min_index : Math.Max(index_char_lowercase, index_char_uppercase);
|
||||
|
||||
//If no value is found
|
||||
//Check if fuzziness is allowed
|
||||
if (index_in_string == -1)
|
||||
{
|
||||
if (Fuzziness > 0)
|
||||
{
|
||||
fuzzies += 1 - Fuzziness;
|
||||
continue;
|
||||
}
|
||||
else return 0;
|
||||
}
|
||||
else
|
||||
char_score = 0.1;
|
||||
|
||||
//Check if current char is the same case
|
||||
//Then add bonus
|
||||
if (Source[index_in_string].ToString() == c) char_score += 0.1;
|
||||
|
||||
//Check if char matches the first letter
|
||||
//And add bonus for consecutive letters
|
||||
if (index_in_string == 0)
|
||||
{
|
||||
char_score += 0.6;
|
||||
|
||||
//Check if the abbreviation
|
||||
//is in the start of the word
|
||||
start_of_word_bonus = i == 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Acronym Bonus
|
||||
// Weighing Logic: Typing the first character of an acronym is as if you
|
||||
// preceded it with two perfect character matches.
|
||||
if (Source.ElementAtOrDefault(index_in_string - 1).ToString() == " ") char_score += 0.8;
|
||||
}
|
||||
|
||||
|
||||
//Remove the start of string, so we don't reprocess it
|
||||
Source = Source.Substring(index_in_string + 1);
|
||||
|
||||
//sum chars scores
|
||||
total_char_score += char_score;
|
||||
}
|
||||
|
||||
abbrv_score = total_char_score / abbrv_size;
|
||||
|
||||
//Reduce penalty for longer words
|
||||
final_score = ((abbrv_score * (abbrv_size / word_size)) + abbrv_score) / 2;
|
||||
|
||||
//Reduce using fuzzies;
|
||||
final_score = final_score / fuzzies;
|
||||
|
||||
//Process start of string bonus
|
||||
if (start_of_word_bonus && final_score <= 0.85)
|
||||
final_score += 0.15;
|
||||
|
||||
return final_score;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace Disco.Services
|
||||
{
|
||||
var i = new JobTableStatusItemModel()
|
||||
{
|
||||
Id = j.Id,
|
||||
JobId = j.Id,
|
||||
OpenedDate = j.OpenedDate,
|
||||
ClosedDate = j.ClosedDate,
|
||||
JobTypeId = j.JobTypeId,
|
||||
|
||||
@@ -120,7 +120,7 @@ namespace Disco.Services
|
||||
|
||||
var jobItems = Jobs.Select(j => new JobTableStatusItemModel()
|
||||
{
|
||||
Id = j.Id,
|
||||
JobId = j.Id,
|
||||
OpenedDate = j.OpenedDate,
|
||||
ClosedDate = j.ClosedDate,
|
||||
JobTypeId = j.JobTypeId,
|
||||
@@ -177,7 +177,7 @@ namespace Disco.Services
|
||||
{
|
||||
items = Jobs.Select(j => new JobTableItemModel()
|
||||
{
|
||||
Id = j.Id,
|
||||
JobId = j.Id,
|
||||
OpenedDate = j.OpenedDate,
|
||||
ClosedDate = j.ClosedDate,
|
||||
JobTypeId = j.JobTypeId,
|
||||
|
||||
@@ -217,7 +217,7 @@ using Disco.Services.Authorization;
|
||||
if (existingItems == null)
|
||||
throw new InvalidOperationException("Notification algorithm didn't indicate any Jobs for update");
|
||||
else
|
||||
jobIds = existingItems.Select(i => i.Id).ToList();
|
||||
jobIds = existingItems.Select(i => i.JobId).ToList();
|
||||
}
|
||||
|
||||
if (jobIds.Count == 0)
|
||||
@@ -232,7 +232,7 @@ using Disco.Services.Authorization;
|
||||
{
|
||||
// Check for existing items, if not handed them
|
||||
if (existingItems == null)
|
||||
existingItems = base.Items.Where(i => jobIds.Contains(i.Id)).ToArray();
|
||||
existingItems = base.Items.Where(i => jobIds.Contains(i.JobId)).ToArray();
|
||||
|
||||
var updatedItems = this.DetermineItems(Database, this.FilterFunction(Database.Jobs.Where(j => jobIds.Contains(j.Id))), false);
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Models.Interop.ActiveDirectory;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Models.Services.Jobs.JobLists;
|
||||
using Disco.Models.Services.Searching;
|
||||
using Disco.Services.Users;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Disco.Services.Searching
|
||||
{
|
||||
public static class Search
|
||||
{
|
||||
|
||||
#region Jobs
|
||||
public static List<JobSearchResultItem> SearchJobs(DiscoDataContext Database, string Term, int? LimitCount = null)
|
||||
{
|
||||
int termInt = default(int);
|
||||
|
||||
IQueryable<Job> query = default(IQueryable<Job>);
|
||||
|
||||
if (int.TryParse(Term, out termInt))
|
||||
{
|
||||
// Term is a Number (int)
|
||||
query = Database.Jobs.Where(j =>
|
||||
j.Id == termInt ||
|
||||
j.Device.SerialNumber.Contains(Term) ||
|
||||
j.Device.AssetNumber.Contains(Term) ||
|
||||
j.User.Id == Term ||
|
||||
j.User.DisplayName.Contains(Term));
|
||||
}
|
||||
else
|
||||
{
|
||||
query = Database.Jobs.Where(j =>
|
||||
j.Device.SerialNumber.Contains(Term) ||
|
||||
j.Device.AssetNumber.Contains(Term) ||
|
||||
j.User.Id == Term ||
|
||||
j.User.DisplayName.Contains(Term));
|
||||
}
|
||||
|
||||
if (LimitCount.HasValue)
|
||||
query = query.Take(LimitCount.Value);
|
||||
|
||||
return query.Select(j => new
|
||||
{
|
||||
Id = j.Id,
|
||||
DeviceSerialNumber = j.DeviceSerialNumber,
|
||||
UserId = j.UserId,
|
||||
UserDisplayName = j.User.DisplayName
|
||||
}).ToArray().Select(i => new JobSearchResultItem()
|
||||
{
|
||||
Id = i.Id.ToString(),
|
||||
DeviceSerialNumber = i.DeviceSerialNumber,
|
||||
UserId = i.UserId,
|
||||
UserDisplayName = i.UserDisplayName
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static JobTableModel SearchJobsTable(DiscoDataContext Database, string Term, int? LimitCount = null, bool IncludeJobStatus = true, bool SearchDetails = false)
|
||||
{
|
||||
int termInt = default(int);
|
||||
|
||||
IQueryable<Job> query = default(IQueryable<Job>);
|
||||
|
||||
if (int.TryParse(Term, out termInt))
|
||||
{
|
||||
// Term is a Number (int)
|
||||
if (SearchDetails)
|
||||
{
|
||||
query = BuildJobTableModel(Database).Where(j =>
|
||||
j.Id == termInt ||
|
||||
j.DeviceHeldLocation.Contains(Term) ||
|
||||
j.Device.SerialNumber.Contains(Term) ||
|
||||
j.Device.AssetNumber.Contains(Term) ||
|
||||
j.User.Id == Term ||
|
||||
j.User.Surname.Contains(Term) ||
|
||||
j.User.GivenName.Contains(Term) ||
|
||||
j.User.DisplayName.Contains(Term) ||
|
||||
j.JobLogs.Any(jl => jl.Comments.Contains(Term)) ||
|
||||
j.JobAttachments.Any(ja => ja.Comments.Contains(Term)));
|
||||
}
|
||||
else
|
||||
{
|
||||
query = BuildJobTableModel(Database).Where(j =>
|
||||
j.Id == termInt ||
|
||||
j.DeviceHeldLocation.Contains(Term) ||
|
||||
j.Device.SerialNumber.Contains(Term) ||
|
||||
j.Device.AssetNumber.Contains(Term) ||
|
||||
j.User.Id == Term ||
|
||||
j.User.Surname.Contains(Term) ||
|
||||
j.User.GivenName.Contains(Term) ||
|
||||
j.User.DisplayName.Contains(Term));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SearchDetails)
|
||||
{
|
||||
query = BuildJobTableModel(Database).Where(j =>
|
||||
j.DeviceHeldLocation.Contains(Term) ||
|
||||
j.Device.SerialNumber.Contains(Term) ||
|
||||
j.Device.AssetNumber.Contains(Term) ||
|
||||
j.User.Id == Term ||
|
||||
j.User.Surname.Contains(Term) ||
|
||||
j.User.GivenName.Contains(Term) ||
|
||||
j.User.DisplayName.Contains(Term) ||
|
||||
j.JobLogs.Any(jl => jl.Comments.Contains(Term)) ||
|
||||
j.JobAttachments.Any(ja => ja.Comments.Contains(Term)));
|
||||
}
|
||||
else
|
||||
{
|
||||
query = BuildJobTableModel(Database).Where(j =>
|
||||
j.DeviceHeldLocation.Contains(Term) ||
|
||||
j.Device.SerialNumber.Contains(Term) ||
|
||||
j.Device.AssetNumber.Contains(Term) ||
|
||||
j.User.Id == Term ||
|
||||
j.User.Surname.Contains(Term) ||
|
||||
j.User.GivenName.Contains(Term) ||
|
||||
j.User.DisplayName.Contains(Term));
|
||||
}
|
||||
}
|
||||
|
||||
if (LimitCount.HasValue)
|
||||
query = query.Take(LimitCount.Value);
|
||||
|
||||
JobTableModel model = new JobTableModel() { ShowStatus = IncludeJobStatus };
|
||||
model.Fill(Database, query, true);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public static IQueryable<Job> BuildJobTableModel(DiscoDataContext Database)
|
||||
{
|
||||
return Database.Jobs.Include("JobType").Include("Device").Include("User").Include("OpenedTechUser");
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Users
|
||||
public static List<UserSearchResultItem> SearchUsers(DiscoDataContext Database, string Term, int? LimitCount = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Term) || Term.Length < 2)
|
||||
throw new ArgumentException("Search Term must contain at least two characters", "Term");
|
||||
|
||||
// Search Active Directory & Import Relevant Users
|
||||
UserService.SearchUsers(Database, Term);
|
||||
|
||||
var matches = Database.Users.Where(u =>
|
||||
u.Id.Contains(Term) ||
|
||||
u.Surname.Contains(Term) ||
|
||||
u.GivenName.Contains(Term) ||
|
||||
u.DisplayName.Contains(Term)
|
||||
);
|
||||
|
||||
if (LimitCount.HasValue)
|
||||
matches = matches.Take(LimitCount.Value);
|
||||
|
||||
return matches.Select(u => new UserSearchResultItem()
|
||||
{
|
||||
Id = u.Id,
|
||||
Surname = u.Surname,
|
||||
GivenName = u.GivenName,
|
||||
DisplayName = u.DisplayName,
|
||||
AssignedDevicesCount = u.DeviceUserAssignments.Where(dua => !dua.UnassignedDate.HasValue).Count(),
|
||||
JobCount = u.Jobs.Count()
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static List<User> SearchUsersUpstream(string Term, int? LimitCount = null)
|
||||
{
|
||||
IEnumerable<ActiveDirectoryUserAccount> matches = UserService.SearchUsers(Term);
|
||||
|
||||
if (LimitCount.HasValue)
|
||||
matches = matches.Take(LimitCount.Value);
|
||||
|
||||
return matches.Select(m => m.ToRepositoryUser()).ToList();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Devices
|
||||
public static List<DeviceSearchResultItem> SearchDevices(DiscoDataContext Database, string Term, int? LimitCount = null, bool SearchDetails = false)
|
||||
{
|
||||
IQueryable<Device> query;
|
||||
|
||||
query = null;
|
||||
|
||||
if (SearchDetails)
|
||||
{
|
||||
query = Database.Devices.Where(d =>
|
||||
d.AssetNumber.Contains(Term) ||
|
||||
d.ComputerName.Contains(Term) ||
|
||||
d.SerialNumber.Contains(Term) ||
|
||||
d.Location.Contains(Term) ||
|
||||
Term.Contains(d.SerialNumber) ||
|
||||
d.DeviceDetails.Any(dd => dd.Value.Contains(Term))
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = Database.Devices.Where(d =>
|
||||
d.AssetNumber.Contains(Term) ||
|
||||
d.ComputerName.Contains(Term) ||
|
||||
d.SerialNumber.Contains(Term) ||
|
||||
d.Location.Contains(Term) ||
|
||||
Term.Contains(d.SerialNumber));
|
||||
}
|
||||
|
||||
return query.ToDeviceSearchResultItems(LimitCount);
|
||||
}
|
||||
|
||||
public static List<DeviceSearchResultItem> SearchDeviceModel(DiscoDataContext Database, int DeviceModelId, int? LimitCount = null)
|
||||
{
|
||||
return Database.Devices.Where(d => d.DeviceModelId == DeviceModelId).ToDeviceSearchResultItems(LimitCount);
|
||||
}
|
||||
public static List<DeviceSearchResultItem> SearchDeviceProfile(DiscoDataContext Database, int DeviceProfileId, int? LimitCount = null)
|
||||
{
|
||||
return Database.Devices.Where(d => d.DeviceProfileId == DeviceProfileId).ToDeviceSearchResultItems(LimitCount);
|
||||
}
|
||||
public static List<DeviceSearchResultItem> SearchDeviceBatch(DiscoDataContext Database, int DeviceBatchId, int? LimitCount = null)
|
||||
{
|
||||
return Database.Devices.Where(d => d.DeviceBatchId == DeviceBatchId).ToDeviceSearchResultItems(LimitCount);
|
||||
}
|
||||
|
||||
private static List<DeviceSearchResultItem> ToDeviceSearchResultItems(this IQueryable<Device> Query, int? LimitCount = null)
|
||||
{
|
||||
if (LimitCount.HasValue)
|
||||
Query = Query.Take(LimitCount.Value);
|
||||
|
||||
return Query.Select(d => new DeviceSearchResultItem()
|
||||
{
|
||||
Id = d.SerialNumber,
|
||||
AssetNumber = d.AssetNumber,
|
||||
ComputerName = d.ComputerName,
|
||||
DeviceModelDescription = d.DeviceModel.Description,
|
||||
DeviceProfileDescription = d.DeviceProfile.Description,
|
||||
DecommissionedDate = d.DecommissionedDate,
|
||||
AssignedUserId = d.AssignedUserId,
|
||||
AssignedUserDisplayName = d.AssignedUser.DisplayName,
|
||||
JobCount = d.Jobs.Count()
|
||||
}).ToList();
|
||||
}
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -22,13 +22,16 @@ namespace Disco.Services.Users
|
||||
private const string _cacheHttpRequestKey = "Disco_CurrentUserToken";
|
||||
private static Func<string, string[], ActiveDirectoryUserAccount> _GetActiveDirectoryUserAccount;
|
||||
private static Func<string, string[], ActiveDirectoryMachineAccount> _GetActiveDirectoryMachineAccount;
|
||||
private static Func<string, List<ActiveDirectoryUserAccount>> _SearchActiveDirectoryUsers;
|
||||
|
||||
public static void Initialize(DiscoDataContext Database,
|
||||
Func<string, string[], ActiveDirectoryUserAccount> GetActiveDirectoryUserAccount,
|
||||
Func<string, string[], ActiveDirectoryMachineAccount> GetActiveDirectoryMachineAccount)
|
||||
Func<string, string[], ActiveDirectoryMachineAccount> GetActiveDirectoryMachineAccount,
|
||||
Func<string, List<ActiveDirectoryUserAccount>> SearchActiveDirectoryUsers)
|
||||
{
|
||||
_GetActiveDirectoryUserAccount = GetActiveDirectoryUserAccount;
|
||||
_GetActiveDirectoryMachineAccount = GetActiveDirectoryMachineAccount;
|
||||
_SearchActiveDirectoryUsers = SearchActiveDirectoryUsers;
|
||||
|
||||
Authorization.Roles.RoleCache.Initialize(Database);
|
||||
}
|
||||
@@ -201,6 +204,27 @@ namespace Disco.Services.Users
|
||||
Cache.FlushCache();
|
||||
}
|
||||
|
||||
internal static List<ActiveDirectoryUserAccount> SearchUsers(string Term)
|
||||
{
|
||||
return _SearchActiveDirectoryUsers(Term);
|
||||
}
|
||||
|
||||
internal static List<ActiveDirectoryUserAccount> SearchUsers(DiscoDataContext Database, string Term)
|
||||
{
|
||||
var adImportedUsers = SearchUsers(Term);
|
||||
foreach (var adU in adImportedUsers.Select(adU => adU.ToRepositoryUser()))
|
||||
{
|
||||
var existingUser = Database.Users.Find(adU.Id);
|
||||
if (existingUser != null)
|
||||
existingUser.UpdateSelf(adU);
|
||||
else
|
||||
Database.Users.Add(adU);
|
||||
Database.SaveChanges();
|
||||
UserService.InvalidateCachedUser(adU.Id);
|
||||
}
|
||||
return adImportedUsers;
|
||||
}
|
||||
|
||||
internal static Tuple<User, AuthorizationToken> ImportUser(DiscoDataContext Database, string UserId)
|
||||
{
|
||||
if (_GetActiveDirectoryUserAccount == null)
|
||||
|
||||
Reference in New Issue
Block a user