Feature: Job Queues

Also UI style, theme and element changes
This commit is contained in:
Gary Sharp
2014-02-03 14:50:08 +11:00
parent bdb3e1e6b4
commit 3f63281dc4
212 changed files with 17334 additions and 5441 deletions
@@ -0,0 +1,250 @@
using Disco.Data.Repository;
using Disco.Models.Repository;
using Disco.Models.Services.Jobs.JobLists;
using Disco.Services.Authorization;
using Disco.Services.Users;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Disco.Services
{
public static class JobTableExtensions
{
private static JobTableModel CloneEmptyJobTableModel(JobTableModel Model)
{
return new JobTableModel()
{
ShowId = Model.ShowId,
ShowDeviceAddress = Model.ShowDeviceAddress,
ShowDates = Model.ShowDates,
ShowType = Model.ShowType,
ShowDevice = Model.ShowDevice,
ShowUser = Model.ShowUser,
ShowTechnician = Model.ShowTechnician,
ShowLocation = Model.ShowLocation,
ShowStatus = Model.ShowStatus,
IsSmallTable = Model.IsSmallTable,
HideClosedJobs = Model.HideClosedJobs,
EnablePaging = Model.EnablePaging,
EnableFilter = Model.EnableFilter
};
}
public static IDictionary<string, JobTableModel> MultiCampusModels(this JobTableModel Model)
{
var items = Model.Items;
if (items == null || items.Count() > 0)
{
return items.OrderBy(i => i.DeviceAddress).GroupBy(i => i.DeviceAddress).ToDictionary(
ig => ig.Key ?? string.Empty,
ig =>
{
var jtm = CloneEmptyJobTableModel(Model);
jtm.Items = ig.ToList();
return jtm;
}
);
}
else
{
return null;
}
}
private static List<string> FilterAllowedTypes(AuthorizationToken Authorization)
{
if (!Authorization.HasAll(Claims.Job.Types.ShowHMisc, Claims.Job.Types.ShowHNWar, Claims.Job.Types.ShowHWar, Claims.Job.Types.ShowSApp, Claims.Job.Types.ShowSImg, Claims.Job.Types.ShowSOS, Claims.Job.Types.ShowUMgmt))
{
// Must Filter
List<string> allowedTypes = new List<string>(6);
if (Authorization.Has(Claims.Job.Types.ShowHMisc))
allowedTypes.Add(JobType.JobTypeIds.HMisc);
if (Authorization.Has(Claims.Job.Types.ShowHNWar))
allowedTypes.Add(JobType.JobTypeIds.HNWar);
if (Authorization.Has(Claims.Job.Types.ShowHWar))
allowedTypes.Add(JobType.JobTypeIds.HWar);
if (Authorization.Has(Claims.Job.Types.ShowSApp))
allowedTypes.Add(JobType.JobTypeIds.SApp);
if (Authorization.Has(Claims.Job.Types.ShowSImg))
allowedTypes.Add(JobType.JobTypeIds.SImg);
if (Authorization.Has(Claims.Job.Types.ShowSOS))
allowedTypes.Add(JobType.JobTypeIds.SOS);
if (Authorization.Has(Claims.Job.Types.ShowUMgmt))
allowedTypes.Add(JobType.JobTypeIds.UMgmt);
return allowedTypes;
}
return null;
}
public static IQueryable<Job> FilterPermissions(this JobTableModel model, IQueryable<Job> Jobs, AuthorizationToken Authorization)
{
var allowedTypes = FilterAllowedTypes(Authorization);
if (allowedTypes != null)
{
return Jobs.Where(j => allowedTypes.Contains(j.JobTypeId));
}
return Jobs;
}
public static IEnumerable<JobTableItemModel> PermissionsFiltered(this IEnumerable<JobTableItemModel> Items, AuthorizationToken Authorization)
{
if (Items != null && Items.Count() > 0)
{
var allowedTypes = FilterAllowedTypes(Authorization);
if (allowedTypes != null)
{
return Items.Where(j => allowedTypes.Contains(j.JobTypeId)).ToList();
}
}
return Items;
}
public static IEnumerable<JobTableItemModel> DetermineItems(this JobTableModel model, DiscoDataContext Database, IQueryable<Job> Jobs)
{
List<JobTableItemModel> items;
// Permissions
var auth = UserService.CurrentAuthorization;
if (!auth.HasAll(Claims.Job.Types.ShowHMisc, Claims.Job.Types.ShowHNWar, Claims.Job.Types.ShowHWar, Claims.Job.Types.ShowSApp, Claims.Job.Types.ShowSImg, Claims.Job.Types.ShowSOS, Claims.Job.Types.ShowUMgmt))
{
// Must Filter
List<string> allowedTypes = new List<string>(6);
if (auth.Has(Claims.Job.Types.ShowHMisc))
allowedTypes.Add(JobType.JobTypeIds.HMisc);
if (auth.Has(Claims.Job.Types.ShowHNWar))
allowedTypes.Add(JobType.JobTypeIds.HNWar);
if (auth.Has(Claims.Job.Types.ShowHWar))
allowedTypes.Add(JobType.JobTypeIds.HWar);
if (auth.Has(Claims.Job.Types.ShowSApp))
allowedTypes.Add(JobType.JobTypeIds.SApp);
if (auth.Has(Claims.Job.Types.ShowSImg))
allowedTypes.Add(JobType.JobTypeIds.SImg);
if (auth.Has(Claims.Job.Types.ShowSOS))
allowedTypes.Add(JobType.JobTypeIds.SOS);
if (auth.Has(Claims.Job.Types.ShowUMgmt))
allowedTypes.Add(JobType.JobTypeIds.UMgmt);
Jobs = Jobs.Where(j => allowedTypes.Contains(j.JobTypeId));
}
if (model.ShowStatus)
{
var jobItems = Jobs.Select(j => new JobTableStatusItemModel()
{
Id = j.Id,
OpenedDate = j.OpenedDate,
ClosedDate = j.ClosedDate,
JobTypeId = j.JobTypeId,
JobTypeDescription = j.JobType.Description,
DeviceSerialNumber = j.Device.SerialNumber,
DeviceProfileId = j.Device.DeviceProfileId,
DeviceModelId = j.Device.DeviceModelId,
DeviceModelDescription = j.Device.DeviceModel.Description,
DeviceAddressId = j.Device.DeviceProfile.DefaultOrganisationAddress,
UserId = j.UserId,
UserDisplayName = j.User.DisplayName,
OpenedTechUserId = j.OpenedTechUserId,
OpenedTechUserDisplayName = j.OpenedTechUser.DisplayName,
DeviceHeldLocation = j.DeviceHeldLocation,
Flags = j.Flags,
JobMetaWarranty_ExternalReference = j.JobMetaWarranty.ExternalReference,
JobMetaWarranty_ExternalLoggedDate = j.JobMetaWarranty.ExternalLoggedDate,
JobMetaWarranty_ExternalCompletedDate = j.JobMetaWarranty.ExternalCompletedDate,
JobMetaNonWarranty_RepairerLoggedDate = j.JobMetaNonWarranty.RepairerLoggedDate,
JobMetaNonWarranty_RepairerCompletedDate = j.JobMetaNonWarranty.RepairerCompletedDate,
JobMetaNonWarranty_AccountingChargeAddedDate = j.JobMetaNonWarranty.AccountingChargeAddedDate,
JobMetaNonWarranty_AccountingChargePaidDate = j.JobMetaNonWarranty.AccountingChargePaidDate,
JobMetaNonWarranty_AccountingChargeRequiredDate = j.JobMetaNonWarranty.AccountingChargeRequiredDate,
JobMetaNonWarranty_IsInsuranceClaim = j.JobMetaNonWarranty.IsInsuranceClaim,
JobMetaInsurance_ClaimFormSentDate = j.JobMetaInsurance.ClaimFormSentDate,
WaitingForUserAction = j.WaitingForUserAction,
DeviceReadyForReturn = j.DeviceReadyForReturn,
DeviceHeld = j.DeviceHeld,
DeviceReturnedDate = j.DeviceReturnedDate,
JobMetaWarranty_ExternalName = j.JobMetaWarranty.ExternalName,
JobMetaNonWarranty_RepairerName = j.JobMetaNonWarranty.RepairerName,
ActiveJobQueues = j.JobQueues.Where(jq => !jq.RemovedDate.HasValue).Select(jq => new JobTableStatusQueueItemModel()
{
Id = jq.Id,
QueueId = jq.JobQueueId,
AddedDate = jq.AddedDate,
SLAExpiresDate = jq.SLAExpiresDate,
Priority = jq.Priority
})
});
items = new List<JobTableItemModel>();
foreach (var j in jobItems)
{
j.StatusId = j.CalculateStatusId();
j.StatusDescription = JobExtensions.JobStatusDescription(j.StatusId, j);
items.Add(j);
}
}
else
{
items = Jobs.Select(j => new JobTableItemModel()
{
Id = j.Id,
OpenedDate = j.OpenedDate,
ClosedDate = j.ClosedDate,
JobTypeId = j.JobTypeId,
JobTypeDescription = j.JobType.Description,
DeviceSerialNumber = j.Device.SerialNumber,
DeviceProfileId = j.Device.DeviceProfileId,
DeviceModelId = j.Device.DeviceModelId,
DeviceModelDescription = j.Device.DeviceModel.Description,
DeviceAddressId = j.Device.DeviceProfile.DefaultOrganisationAddress,
UserId = j.UserId,
UserDisplayName = j.User.DisplayName,
OpenedTechUserId = j.OpenedTechUserId,
OpenedTechUserDisplayName = j.OpenedTechUser.DisplayName,
DeviceHeldLocation = j.DeviceHeldLocation,
Flags = j.Flags
}).ToList();
}
if (!model.ShowDeviceAddress.HasValue)
model.ShowDeviceAddress = Database.DiscoConfiguration.MultiSiteMode;
foreach (var j in items)
if (j.DeviceAddressId.HasValue)
j.DeviceAddress = Database.DiscoConfiguration.OrganisationAddresses.GetAddress(j.DeviceAddressId.Value).Name;
return items;
}
public static void Fill(this JobTableModel model, DiscoDataContext Database, IQueryable<Job> Jobs)
{
model.Items = model.DetermineItems(Database, Jobs);
}
public static double? SlaPrecentageRemaining(this IEnumerable<JobTableStatusQueueItemModel> queueItems)
{
return queueItems.Where(i => i.SLAExpiresDate.HasValue).Min<JobTableStatusQueueItemModel, double?>(i =>
{
var total = (i.SLAExpiresDate.Value - i.AddedDate).Ticks;
var remaining = (i.SLAExpiresDate.Value - DateTime.Now).Ticks;
return ((double)remaining / total);
});
}
public static IEnumerable<JobTableStatusQueueItemModel> UsersQueueItems(this IEnumerable<JobTableStatusQueueItemModel> queueItems, AuthorizationToken Authorization)
{
var usersQueues = Jobs.JobQueues.JobQueueService.UsersQueues(Authorization).ToDictionary(q => q.JobQueue.Id);
return queueItems.Where(qi => usersQueues.ContainsKey(qi.QueueId));
}
}
}
@@ -0,0 +1,265 @@
using Disco.Data.Repository;
using Disco.Data.Repository.Monitor;
using Disco.Models.Repository;
using Disco.Models.Services.Jobs.JobLists;
using Disco.Services.Logging;
using Disco.Services.Users;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
namespace Disco.Services.Jobs.JobLists
{
using FilterFunc = Func<IQueryable<Job>, IQueryable<Job>>;
using SortFunc = Func<IEnumerable<JobTableItemModel>, IEnumerable<JobTableItemModel>>;
using OpenFilterFunc = Func<IEnumerable<JobTableStatusItemModel>, IEnumerable<JobTableStatusItemModel>>;
using Disco.Services.Authorization;
using Disco.Services.Jobs.JobQueues;
public class ManagedJobList : JobTableModel, IDisposable
{
private static ManagedJobList openJobs;
private IDisposable unsubscribeToken;
private object updateLock = new object();
public string Name { get; private set; }
public FilterFunc FilterFunction { get; private set; }
public SortFunc SortFunction { get; private set; }
public static OpenFilterFunc AwaitingTechnicianActionFilter = q => q.Where(j => j.ClosedDate == null && !j.WaitingForUserAction.HasValue
&& !(j.JobTypeId == JobType.JobTypeIds.HNWar && (j.JobMetaNonWarranty_RepairerLoggedDate.HasValue && j.JobMetaNonWarranty_IsInsuranceClaim.Value && !j.JobMetaInsurance_ClaimFormSentDate.HasValue))
&& !(j.JobTypeId == JobType.JobTypeIds.HNWar && (j.JobMetaNonWarranty_RepairerLoggedDate.HasValue && !j.JobMetaNonWarranty_RepairerCompletedDate.HasValue))
&& !(j.JobTypeId == JobType.JobTypeIds.HNWar && (j.JobMetaNonWarranty_RepairerLoggedDate.HasValue && j.JobMetaNonWarranty_AccountingChargeAddedDate.HasValue && !j.JobMetaNonWarranty_AccountingChargePaidDate.HasValue))
&& !(j.JobTypeId == JobType.JobTypeIds.HWar && (j.JobMetaWarranty_ExternalLoggedDate.HasValue && !j.JobMetaWarranty_ExternalCompletedDate.HasValue))
&& (j.DeviceHeld == null || j.DeviceReturnedDate != null || j.DeviceReadyForReturn == null));
static ManagedJobList()
{
using (var Database = new DiscoDataContext())
{
openJobs = new ManagedJobList(
Name: "Open Jobs",
FilterFunction: q => q.Where(j => !j.ClosedDate.HasValue),
SortFunction: q => q)
{
ShowDeviceAddress = true,
ShowLocation = true,
ShowStatus = true
}.Initialize(Database);
}
}
public static JobTableModel OpenJobsTable(OpenFilterFunc FilterFunction)
{
return new JobTableModel()
{
ShowStatus = true,
Items = FilterFunction(openJobs.Items.Cast<JobTableStatusItemModel>())
};
}
public static JobTableModel MyJobsTable(AuthorizationToken AuthToken)
{
IEnumerable<Tuple<JobTableStatusItemModel, byte, byte, DateTime?>> allJobs = null;
if (AuthToken.Has(Claims.Job.Lists.MyJobsOrphaned))
{
allJobs = AwaitingTechnicianActionFilter(
openJobs.Items.Cast<JobTableStatusItemModel>()
.Where(i => i.ActiveJobQueues == null || i.ActiveJobQueues.Count() == 0)
).Select(i => new Tuple<JobTableStatusItemModel, byte, byte, DateTime?>(i, (byte)JobQueuePriority.Normal, (byte)JobQueuePriority.Normal, null));
}
var usersQueues = JobQueueService.UsersQueues(AuthToken).ToDictionary(q => q.JobQueue.Id);
var queueJobs = openJobs.Items.Cast<JobTableStatusItemModel>()
.Where(i => i.ActiveJobQueues != null && i.ActiveJobQueues.Any(jqj => usersQueues.ContainsKey(jqj.QueueId)))
.Select(i => new Tuple<JobTableStatusItemModel, byte, byte, DateTime?>(
i,
(byte)i.ActiveJobQueues.Where(q => usersQueues.ContainsKey(q.QueueId)).Max(q => (int)usersQueues[q.QueueId].JobQueue.Priority),
(byte)i.ActiveJobQueues.Where(q => usersQueues.ContainsKey(q.QueueId)).Max(q => (int)q.Priority),
i.ActiveJobQueues.Where(q => usersQueues.ContainsKey(q.QueueId) && q.SLAExpiresDate.HasValue).Min(q => q.SLAExpiresDate)
));
if (allJobs != null)
allJobs = allJobs.Concat(queueJobs).ToList();
else
allJobs = queueJobs.ToList();
var allJobsSorted = allJobs
.OrderByDescending(i => i.Item2).ThenByDescending(i => i.Item3).ThenBy(i => i.Item4).ThenBy(i => i.Item1.OpenedDate).Select(q => q.Item1);
return new JobTableModel()
{
ShowStatus = true,
Items = allJobsSorted.ToList()
};
}
public override IEnumerable<JobTableItemModel> Items
{
get
{
return base.Items.PermissionsFiltered(UserService.CurrentAuthorization);
}
set
{
throw new InvalidOperationException("Items cannot be manually set in a Managed Job List");
}
}
public ManagedJobList(string Name, FilterFunc FilterFunction, SortFunc SortFunction)
{
this.Name = Name;
this.FilterFunction = FilterFunction;
this.SortFunction = SortFunction;
}
public ManagedJobList Initialize(DiscoDataContext Database)
{
// Can only Initialize once
if (base.Items != null)
return ReInitialize(Database);
lock (updateLock)
{
// Subscribe for Changes
// - Job (or Job Meta) Changes
// - Device Profile Address Changes (for multi-campus Schools)
// - Device Model Description Changes
// - Device's Profile or Model Changes
unsubscribeToken = RepositoryMonitor.StreamAfterCommit
.Where(n => n.EntityType == typeof(Job) ||
n.EntityType == typeof(JobQueueJob) ||
n.EntityType == typeof(JobMetaWarranty) ||
n.EntityType == typeof(JobMetaNonWarranty) ||
n.EntityType == typeof(JobMetaInsurance) ||
(n.EventType == RepositoryMonitorEventType.Modified && (
(n.EntityType == typeof(DeviceProfile) && n.ModifiedProperties.Contains("DefaultOrganisationAddress")) ||
(n.EntityType == typeof(DeviceModel) && n.ModifiedProperties.Contains("Description")) ||
(n.EntityType == typeof(Device) && n.ModifiedProperties.Contains("DeviceProfileId") || n.ModifiedProperties.Contains("DeviceModelId"))
)))
.Subscribe(JobNotification, NotificationError);
// Initially fill table
base.Items = this.SortFunction(this.DetermineItems(Database, this.FilterFunction(Database.Jobs))).ToList();
}
return this;
}
public ManagedJobList ReInitialize(DiscoDataContext Database)
{
return ReInitialize(Database, null, null);
}
public ManagedJobList ReInitialize(DiscoDataContext Database, FilterFunc FilterFunction)
{
return ReInitialize(Database, FilterFunction, null);
}
public ManagedJobList ReInitialize(DiscoDataContext Database, FilterFunc FilterFunction, SortFunc SortFunction)
{
if (Database == null)
throw new ArgumentNullException("Database");
lock (updateLock)
{
if (FilterFunction != null)
this.FilterFunction = FilterFunction;
if (SortFunction != null)
this.SortFunction = SortFunction;
base.Items = this.SortFunction(this.DetermineItems(Database, this.FilterFunction(Database.Jobs))).ToList();
}
return this;
}
private void NotificationError(Exception ex)
{
SystemLog.LogException(string.Format("Disco.Services.Jobs.JobLists.ManagedJobList: [{0}]", this.Name), ex);
}
private void JobNotification(RepositoryMonitorEvent e)
{
List<int> jobIds = null;
JobTableItemModel[] existingItems = null;
if (e.EntityType == typeof(Job))
jobIds = new List<int>() { ((Job)e.Entity).Id };
else if (e.EntityType == typeof(JobQueueJob))
jobIds = new List<int>() { ((JobQueueJob)e.Entity).JobId };
else if (e.EntityType == typeof(JobMetaWarranty))
jobIds = new List<int>() { ((JobMetaWarranty)e.Entity).JobId };
else if (e.EntityType == typeof(JobMetaNonWarranty))
jobIds = new List<int>() { ((JobMetaNonWarranty)e.Entity).JobId };
else if (e.EntityType == typeof(JobMetaInsurance))
jobIds = new List<int>() { ((JobMetaInsurance)e.Entity).JobId };
else if (e.EntityType == typeof(DeviceProfile))
{
int deviceProfileId = ((DeviceProfile)e.Entity).Id;
existingItems = base.Items.Where(i => i.DeviceProfileId == deviceProfileId).ToArray();
}
else if (e.EntityType == typeof(DeviceModel))
{
int deviceModelId = ((DeviceModel)e.Entity).Id;
existingItems = base.Items.Where(i => i.DeviceModelId == deviceModelId).ToArray();
}
else if (e.EntityType == typeof(Device))
{
string deviceSerialNumber = ((Device)e.Entity).SerialNumber;
existingItems = base.Items.Where(i => i.DeviceSerialNumber == deviceSerialNumber).ToArray();
}
else
return; // Subscription should never reach
if (jobIds == null)
{
if (existingItems == null)
throw new InvalidOperationException("Notification algorithm didn't indicate any Jobs for update");
else
jobIds = existingItems.Select(i => i.Id).ToList();
}
if (jobIds.Count == 0)
return;
else
UpdateJobs(e.Database, jobIds, existingItems);
}
private void UpdateJobs(DiscoDataContext Database, List<int> jobIds, JobTableItemModel[] existingItems = null)
{
lock (updateLock)
{
// Check for existing items, if not handed them
if (existingItems == null)
existingItems = base.Items.Where(i => jobIds.Contains(i.Id)).ToArray();
var updatedItems = this.DetermineItems(Database, this.FilterFunction(Database.Jobs.Where(j => jobIds.Contains(j.Id))));
var refreshedList = base.Items.ToList();
// Remove Existing
if (existingItems.Length > 0)
foreach (var existingItem in existingItems)
refreshedList.Remove(existingItem);
// Add Updated Items
if (updatedItems.Count() > 0)
foreach (var updatedItem in updatedItems)
refreshedList.Add(updatedItem);
// Reorder
base.Items = this.SortFunction(refreshedList).ToList();
}
}
public void Dispose()
{
if (unsubscribeToken != null)
{
unsubscribeToken.Dispose();
unsubscribeToken = null;
}
}
}
}