diff --git a/Disco.Services/Devices/DeviceFlags/Cache.cs b/Disco.Services/Devices/DeviceFlags/Cache.cs new file mode 100644 index 00000000..c680148e --- /dev/null +++ b/Disco.Services/Devices/DeviceFlags/Cache.cs @@ -0,0 +1,61 @@ +using Disco.Data.Repository; +using Disco.Models.Repository; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Disco.Services.Devices.DeviceFlags +{ + internal class Cache + { + private ConcurrentDictionary _Cache; + + public Cache(DiscoDataContext Database) + { + Initialize(Database); + } + + public void ReInitialize(DiscoDataContext Database) + { + Initialize(Database); + } + + private void Initialize(DiscoDataContext Database) + { + // Queues from Database + var flags = Database.DeviceFlags.ToList(); + + // Add Queues to In-Memory Cache + _Cache = new ConcurrentDictionary(flags.Select(f => new KeyValuePair(f.Id, f))); + } + + public DeviceFlag GetDeviceFlag(int deviceFlagId) + { + if (_Cache.TryGetValue(deviceFlagId, out var item)) + return item; + else + return null; + } + public List GetDeviceFlags() + { + return _Cache.Values.ToList(); + } + + public void AddOrUpdate(DeviceFlag flag) + { + _Cache.AddOrUpdate(flag.Id, flag, (key, existingItem) => flag); + } + + public DeviceFlag Remove(int deviceFlagId) + { + if (_Cache.TryRemove(deviceFlagId, out var item)) + return item; + else + return null; + } + public DeviceFlag Remove(DeviceFlag deviceFlag) + { + return Remove(deviceFlag.Id); + } + } +} diff --git a/Disco.Services/Devices/DeviceFlags/DeviceFlagDeviceAssignedUsersManagedGroup.cs b/Disco.Services/Devices/DeviceFlags/DeviceFlagDeviceAssignedUsersManagedGroup.cs new file mode 100644 index 00000000..4333e336 --- /dev/null +++ b/Disco.Services/Devices/DeviceFlags/DeviceFlagDeviceAssignedUsersManagedGroup.cs @@ -0,0 +1,184 @@ +using Disco.Data.Repository; +using Disco.Data.Repository.Monitor; +using Disco.Models.Repository; +using Disco.Models.Services.Interop.ActiveDirectory; +using Disco.Services.Interop.ActiveDirectory; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; + +namespace Disco.Services.Devices.DeviceFlags +{ + public class DeviceFlagDeviceAssignedUsersManagedGroup : ADManagedGroup + { + private const string KeyFormat = "DeviceFlag_{0}_DeviceAssignedUsers"; + private const string DescriptionFormat = "User associated with devices which have the {0} Flag will be added to this Active Directory group."; + private const string CategoryDescriptionFormat = "Device Assigned Users Linked Group"; + private const string GroupDescriptionFormat = "{0} [Device Flag Device Assigned Users]"; + + private IDisposable repositorySubscription; + private int deviceFlagId; + private string deviceFlagName; + + public override string Description { get { return string.Format(DescriptionFormat, deviceFlagName); } } + public override string CategoryDescription { get { return CategoryDescriptionFormat; } } + public override string GroupDescription { get { return string.Format(GroupDescriptionFormat, deviceFlagName); } } + public override bool IncludeFilterBeginDate { get { return true; } } + + private DeviceFlagDeviceAssignedUsersManagedGroup(string Key, ADManagedGroupConfiguration Configuration, DeviceFlag deviceFlag) + : base(Key, Configuration) + { + deviceFlagId = deviceFlag.Id; + deviceFlagName = deviceFlag.Name; + } + + public override void Initialize() + { + // Subscribe to changes + repositorySubscription = DeviceFlagService.DeviceFlagAssignmentRepositoryEvents.Value + .Where(e => + (((DeviceFlagAssignment)e.Entity).DeviceFlagId == deviceFlagId)) + .Subscribe(ProcessRepositoryEvent); + } + + public static string GetKey(DeviceFlag deviceFlag) + { + return string.Format(KeyFormat, deviceFlag.Id); + } + public static string GetDescription(DeviceFlag deviceFlag) + { + return string.Format(DescriptionFormat, deviceFlag.Name); + } + public static string GetCategoryDescription(DeviceFlag deviceFlag) + { + return CategoryDescriptionFormat; + } + + public static bool TryGetManagedGroup(DeviceFlag deviceFlag, out DeviceFlagDeviceAssignedUsersManagedGroup managedGroup) + { + return ActiveDirectory.Context.ManagedGroups.TryGetValue(GetKey(deviceFlag), out managedGroup); + } + + public static DeviceFlagDeviceAssignedUsersManagedGroup Initialize(DeviceFlag deviceFlag) + { + if (deviceFlag.Id > 0) + { + var key = GetKey(deviceFlag); + + if (!string.IsNullOrEmpty(deviceFlag.DeviceUsersLinkedGroup)) + { + var config = ConfigurationFromJson(deviceFlag.DeviceUsersLinkedGroup); + + if (config != null && !string.IsNullOrWhiteSpace(config.GroupId)) + { + var group = new DeviceFlagDeviceAssignedUsersManagedGroup( + key, + config, + deviceFlag); + + // Add to AD Context + ActiveDirectory.Context.ManagedGroups.AddOrUpdate(group); + + return group; + } + } + + // Remove from AD Context + ActiveDirectory.Context.ManagedGroups.Remove(key); + } + + return null; + } + + public override IEnumerable DetermineMembers(DiscoDataContext Database) + { + var query = (IQueryable)Database.Users; + + if (Configuration.FilterBeginDate.HasValue) + { + query = query + .Where(u => u.DeviceUserAssignments.Any(a => + a.UnassignedDate == null && + a.Device.DeviceFlagAssignments.Any(fa => + fa.DeviceFlagId == deviceFlagId && + !fa.RemovedDate.HasValue && + fa.AddedDate >= Configuration.FilterBeginDate))); + } + else + { + query = query + .Where(u => u.DeviceUserAssignments.Any(a => + a.UnassignedDate == null && + a.Device.DeviceFlagAssignments.Any(fa => + fa.DeviceFlagId == deviceFlagId && + !fa.RemovedDate.HasValue))); + } + + return query.Select(u => u.UserId) + .Distinct() + .ToList() + .Where(ActiveDirectory.IsValidDomainAccountId) + .ToList(); + } + + private void ProcessRepositoryEvent(RepositoryMonitorEvent Event) + { + var assignment = (DeviceFlagAssignment)Event.Entity; + + string userId = assignment.Device?.AssignedUserId; + if (!ActiveDirectory.IsValidDomainAccountId(userId)) + return; + + switch (Event.EventType) + { + case RepositoryMonitorEventType.Added: + if (Configuration.FilterBeginDate.HasValue) + { + if (!assignment.RemovedDate.HasValue && assignment.AddedDate >= Configuration.FilterBeginDate) + { + AddMember(userId); + } + } + else + { + if (!assignment.RemovedDate.HasValue) + { + AddMember(userId); + } + } + break; + case RepositoryMonitorEventType.Modified: + if (!Configuration.FilterBeginDate.HasValue || assignment.AddedDate >= Configuration.FilterBeginDate) + { + if (assignment.RemovedDate.HasValue) + RemoveMember(userId, (database) => + { + if (database.Users.Any(u => u.DeviceUserAssignments.Any(a => + a.UnassignedDate == null && + a.Device.DeviceFlagAssignments.Any(fa => + fa.DeviceFlagId == deviceFlagId && + !fa.RemovedDate.HasValue)))) + { + return null; + } + else + { + return new[] { userId }; + } + } + ); + else + AddMember(userId); + } + break; + } + } + + public override void Dispose() + { + if (repositorySubscription != null) + repositorySubscription.Dispose(); + } + } +} diff --git a/Disco.Services/Devices/DeviceFlags/DeviceFlagDevicesManagedGroup.cs b/Disco.Services/Devices/DeviceFlags/DeviceFlagDevicesManagedGroup.cs new file mode 100644 index 00000000..e728d229 --- /dev/null +++ b/Disco.Services/Devices/DeviceFlags/DeviceFlagDevicesManagedGroup.cs @@ -0,0 +1,202 @@ +using Disco.Data.Repository; +using Disco.Data.Repository.Monitor; +using Disco.Models.Repository; +using Disco.Models.Services.Interop.ActiveDirectory; +using Disco.Services.Interop.ActiveDirectory; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; + +namespace Disco.Services.Devices.DeviceFlags +{ + public class DeviceFlagDevicesManagedGroup : ADManagedGroup + { + private const string KeyFormat = "DeviceFlag_{0}_Devices"; + private const string DescriptionFormat = "Devices associated with the {0} Flag will be added to this Active Directory group."; + private const string CategoryDescriptionFormat = "Assigned Devices Linked Group"; + private const string GroupDescriptionFormat = "{0} [Device Flag Devices]"; + + private IDisposable repositorySubscription; + private int deviceFlagId; + private string deviceFlagName; + + public override string Description { get { return string.Format(DescriptionFormat, deviceFlagName); } } + public override string CategoryDescription { get { return CategoryDescriptionFormat; } } + public override string GroupDescription { get { return string.Format(GroupDescriptionFormat, deviceFlagName); } } + public override bool IncludeFilterBeginDate { get { return true; } } + + private DeviceFlagDevicesManagedGroup(string Key, ADManagedGroupConfiguration Configuration, DeviceFlag DeviceFlag) + : base(Key, Configuration) + { + deviceFlagId = DeviceFlag.Id; + deviceFlagName = DeviceFlag.Name; + } + + public override void Initialize() + { + // Subscribe to changes + repositorySubscription = DeviceFlagService.DeviceFlagAssignmentRepositoryEvents.Value + .Where(e => + ((DeviceFlagAssignment)e.Entity).DeviceFlagId == deviceFlagId) + .Subscribe(ProcessRepositoryEvent); + } + + public static string GetKey(DeviceFlag deviceFlag) + { + return string.Format(KeyFormat, deviceFlag.Id); + } + public static string GetDescription(DeviceFlag deviceFlag) + { + return string.Format(DescriptionFormat, deviceFlag.Name); + } + public static string GetCategoryDescription(DeviceFlag deviceFlag) + { + return CategoryDescriptionFormat; + } + + public static bool TryGetManagedGroup(DeviceFlag deviceFlag, out DeviceFlagDevicesManagedGroup managedGroup) + { + return ActiveDirectory.Context.ManagedGroups.TryGetValue(GetKey(deviceFlag), out managedGroup); + } + + public static DeviceFlagDevicesManagedGroup Initialize(DeviceFlag deviceFlag) + { + if (deviceFlag.Id > 0) + { + var key = GetKey(deviceFlag); + + if (!string.IsNullOrEmpty(deviceFlag.DevicesLinkedGroup)) + { + var config = ConfigurationFromJson(deviceFlag.DevicesLinkedGroup); + + if (config != null && !string.IsNullOrWhiteSpace(config.GroupId)) + { + var group = new DeviceFlagDevicesManagedGroup( + key, + config, + deviceFlag); + + // Add to AD Context + ActiveDirectory.Context.ManagedGroups.AddOrUpdate(group); + + return group; + } + } + + // Remove from AD Context + ActiveDirectory.Context.ManagedGroups.Remove(key); + } + + return null; + } + + public override IEnumerable DetermineMembers(DiscoDataContext Database) + { + var query = Database.DeviceFlagAssignments + .Where(a => a.DeviceFlagId == deviceFlagId && !a.RemovedDate.HasValue && a.Device.DeviceDomainId != null); + + if (Configuration.FilterBeginDate.HasValue) + query = query.Where(a => a.AddedDate >= Configuration.FilterBeginDate); + + return query + .Select(a => a.Device.DeviceDomainId) + .ToList() + .Where(ActiveDirectory.IsValidDomainAccountId) + .Select(id => id + "$"); + } + + private void ProcessRepositoryEvent(RepositoryMonitorEvent Event) + { + var assignment = (DeviceFlagAssignment)Event.Entity; + + var domainId = assignment.Device?.DeviceDomainId; + + if (!ActiveDirectory.IsValidDomainAccountId(domainId)) + return; + domainId += "$"; + + switch (Event.EventType) + { + case RepositoryMonitorEventType.Added: + if (Configuration.FilterBeginDate.HasValue) + { + if (!assignment.RemovedDate.HasValue && assignment.AddedDate >= Configuration.FilterBeginDate) + { + AddMember(domainId); + } + } + else + { + if (!assignment.RemovedDate.HasValue) + { + AddMember(domainId); + } + } + break; + case RepositoryMonitorEventType.Modified: + if (Configuration.FilterBeginDate.HasValue) + { + if (assignment.AddedDate >= Configuration.FilterBeginDate) + { + if (assignment.RemovedDate.HasValue) + { + RemoveMember(domainId); + } + else + { + AddMember(domainId); + } + } + } + else + { + if (assignment.RemovedDate.HasValue) + { + RemoveMember(domainId); + } + else + { + AddMember(domainId); + } + } + break; + case RepositoryMonitorEventType.Deleted: + // Remove the device if no other (non-removed) assignments exist. + var serialNumber = assignment.DeviceSerialNumber; + RemoveMember(domainId, (database) => + { + if (Configuration.FilterBeginDate.HasValue) + { + if (database.DeviceFlagAssignments.Any(a => a.DeviceFlagId == deviceFlagId && a.DeviceSerialNumber == serialNumber && !a.RemovedDate.HasValue && a.AddedDate >= Configuration.FilterBeginDate)) + { + return null; + } + else + { + return new string[] { domainId }; + } + } + else + { + if (database.DeviceFlagAssignments.Any(a => a.DeviceFlagId == deviceFlagId && a.DeviceSerialNumber == serialNumber && !a.RemovedDate.HasValue)) + { + return null; + } + else + { + return new string[] { domainId }; + } + } + }); + break; + } + } + + public override void Dispose() + { + if (repositorySubscription != null) + repositorySubscription.Dispose(); + } + } +} diff --git a/Disco.Services/Devices/DeviceFlags/DeviceFlagExport.cs b/Disco.Services/Devices/DeviceFlags/DeviceFlagExport.cs new file mode 100644 index 00000000..0370b973 --- /dev/null +++ b/Disco.Services/Devices/DeviceFlags/DeviceFlagExport.cs @@ -0,0 +1,229 @@ +using Disco.Data.Repository; +using Disco.Models.Exporting; +using Disco.Models.Services.Devices.DeviceFlag; +using Disco.Models.Services.Exporting; +using Disco.Services.Plugins.Features.DetailsProvider; +using Disco.Services.Tasks; +using Disco.Services.Users; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Data.Entity; +using System.Linq; + +namespace Disco.Services.Devices.DeviceFlags +{ + using Metadata = ExportFieldMetadata; + + public class DeviceFlagExport + { + private readonly DiscoDataContext database; + private readonly DeviceFlagExportOptions options; + + public DeviceFlagExport(DiscoDataContext database, DeviceFlagExportOptions options) + { + this.database = database; + this.options = options; + } + + public ExportResult Generate(IScheduledTaskStatus status) + { + var records = BuildRecords(status); + + var metadata = BuildMetadata(records, status); + + if (metadata.Count == 0) + throw new ArgumentException("At least one export field must be specified", nameof(options)); + + status.UpdateStatus(90, $"Formatting {records.Count} records for export"); + return ExportHelpers.WriteExport(options, status, metadata, records); + } + + private List BuildRecords(IScheduledTaskStatus status) + { + var query = database.DeviceFlagAssignments + .Include(a => a.DeviceFlag); + + if (options.HasDeviceOptions()) + query = query.Include(a => a.Device); + if (options.HasDeviceModelOptions()) + query = query.Include(a => a.Device.DeviceModel); + if (options.HasDeviceBatchOptions()) + query = query.Include(a => a.Device.DeviceBatch); + if (options.HasDeviceProfileOptions()) + query = query.Include(a => a.Device.DeviceProfile); + if (options.HasAssignedUserOptions()) + query = query.Include(a => a.Device.AssignedUser); + if (options.AssignedUserDetailCustom) + query = query.Include(a => a.Device.AssignedUser.UserDetails); + + query = query.Where(a => options.DeviceFlagIds.Contains(a.DeviceFlagId)); + + if (options.CurrentOnly) + { + query = query.Where(a => !a.RemovedDate.HasValue); + } + + // Update Users + if (options.HasAssignedUserOptions()) + { + status.UpdateStatus(5, "Refreshing user details from Active Directory"); + var userIds = query.Where(d => d.Device.AssignedUserId != null).Select(d => d.Device.AssignedUserId).Distinct().ToList(); + foreach (var userId in userIds) + { + try + { + UserService.GetUser(userId, database); + } + catch (Exception) { } // Ignore Errors + } + } + + status.UpdateStatus(15, "Extracting records from the database"); + + var records = query.Select(a => new DeviceFlagExportRecord() + { + Assignment = a + }).ToList(); + + if (options.AssignedUserDetailCustom) + { + status.UpdateStatus(50, "Extracting custom user detail records"); + + var detailsService = new DetailsProviderService(database); + var cache = new Dictionary>(StringComparer.Ordinal); + foreach (var record in records) + { + var userId = record.Assignment.Device.AssignedUserId; + + if (userId == null) + continue; + + if (!cache.TryGetValue(userId, out var details)) + details = detailsService.GetDetails(record.Assignment.Device.AssignedUser); + record.AssignedUserCustomDetails = details; + } + } + + return records; + } + + private List BuildMetadata(List records, IScheduledTaskStatus status) + { + status.UpdateStatus(80, "Building metadata"); + + IEnumerable userDetailCustomKeys = null; + if (options.AssignedUserDetailCustom) + userDetailCustomKeys = records.Where(r => r.AssignedUserCustomDetails != null).SelectMany(r => r.AssignedUserCustomDetails.Keys).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + + var accessors = BuildAccessors(userDetailCustomKeys); + + return typeof(DeviceFlagExportOptions).GetProperties() + .Where(p => p.PropertyType == typeof(bool)) + .Select(p => new + { + property = p, + details = (DisplayAttribute)p.GetCustomAttributes(typeof(DisplayAttribute), false).FirstOrDefault() + }) + .Where(p => p.details != null && p.property.Name != nameof(options.CurrentOnly) && (bool)p.property.GetValue(options)) + .SelectMany(p => + { + var fieldMetadata = accessors[p.property.Name]; + fieldMetadata.ForEach(f => + { + if (f.ColumnName == null) + f.ColumnName = (p.details.ShortName == "Device Flag") ? p.details.Name : $"{p.details.ShortName} {p.details.Name}"; + }); + return fieldMetadata; + }).ToList(); + } + + private static Dictionary> BuildAccessors(IEnumerable userDetailsCustomKeys) + { + const string DateFormat = "yyyy-MM-dd"; + const string DateTimeFormat = DateFormat + " HH:mm:ss"; + + Func csvStringEncoded = (o) => o == null ? null : $"\"{((string)o).Replace("\"", "\"\"")}\""; + Func csvToStringEncoded = (o) => o == null ? null : o.ToString(); + Func csvCurrencyEncoded = (o) => ((decimal?)o).HasValue ? ((decimal?)o).Value.ToString("C") : null; + Func csvDateEncoded = (o) => ((DateTime)o).ToString(DateFormat); + Func csvDateTimeEncoded = (o) => ((DateTime)o).ToString(DateTimeFormat); + Func csvNullableDateEncoded = (o) => ((DateTime?)o).HasValue ? csvDateEncoded(o) : null; + Func csvNullableDateTimeEncoded = (o) => ((DateTime?)o).HasValue ? csvDateTimeEncoded(o) : null; + + var metadata = new Dictionary>(); + + // Device Flag + metadata.Add(nameof(DeviceFlagExportOptions.Id), new List() { new Metadata(nameof(DeviceFlagExportOptions.Id), typeof(string), r => r.Assignment.DeviceFlagId, csvToStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.Name), new List() { new Metadata(nameof(DeviceFlagExportOptions.Name), typeof(string), r => r.Assignment.DeviceFlag.Name, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.Description), new List() { new Metadata(nameof(DeviceFlagExportOptions.Description), typeof(string), r => r.Assignment.DeviceFlag.Description, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.Icon), new List() { new Metadata(nameof(DeviceFlagExportOptions.Icon), typeof(string), r => r.Assignment.DeviceFlag.Icon, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.IconColour), new List() { new Metadata(nameof(DeviceFlagExportOptions.IconColour), typeof(string), r => r.Assignment.DeviceFlag.IconColour, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.AssignmentId), new List() { new Metadata(nameof(DeviceFlagExportOptions.AssignmentId), typeof(string), r => r.Assignment.Id, csvToStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.AddedDate), new List() { new Metadata(nameof(DeviceFlagExportOptions.AddedDate), typeof(string), r => r.Assignment.AddedDate, csvDateTimeEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.AddedUserId), new List() { new Metadata(nameof(DeviceFlagExportOptions.AddedUserId), typeof(string), r => r.Assignment.AddedUserId, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.RemovedUserId), new List() { new Metadata(nameof(DeviceFlagExportOptions.RemovedUserId), typeof(string), r => r.Assignment.RemovedUserId, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.RemovedDate), new List() { new Metadata(nameof(DeviceFlagExportOptions.RemovedDate), typeof(string), r => r.Assignment.RemovedDate, csvNullableDateTimeEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.Comments), new List() { new Metadata(nameof(DeviceFlagExportOptions.Comments), typeof(string), r => r.Assignment.Comments, csvStringEncoded) }); + + + // Device + metadata.Add(nameof(DeviceFlagExportOptions.DeviceSerialNumber), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceSerialNumber), typeof(string), r => r.Assignment.Device.SerialNumber, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.DeviceAssetNumber), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceAssetNumber), typeof(string), r => r.Assignment.Device.AssetNumber, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.DeviceLocation), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceLocation), typeof(string), r => r.Assignment.Device.Location, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.DeviceComputerName), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceComputerName), typeof(string), r => r.Assignment.Device.DeviceDomainId, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.DeviceLastNetworkLogon), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceLastNetworkLogon), typeof(DateTime), r => r.Assignment.Device.LastNetworkLogonDate, csvNullableDateTimeEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.DeviceCreatedDate), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceCreatedDate), typeof(DateTime), r => r.Assignment.Device.CreatedDate, csvDateTimeEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.DeviceFirstEnrolledDate), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceFirstEnrolledDate), typeof(DateTime), r => r.Assignment.Device.EnrolledDate, csvNullableDateTimeEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.DeviceLastEnrolledDate), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceLastEnrolledDate), typeof(DateTime), r => r.Assignment.Device.LastEnrolDate, csvNullableDateTimeEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.DeviceAllowUnauthenticatedEnrol), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceAllowUnauthenticatedEnrol), typeof(bool), r => r.Assignment.Device.AllowUnauthenticatedEnrol, csvToStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.DeviceDecommissionedDate), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceDecommissionedDate), typeof(DateTime), r => r.Assignment.Device.DecommissionedDate, csvNullableDateTimeEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.DeviceDecommissionedReason), new List() { new Metadata(nameof(DeviceFlagExportOptions.DeviceDecommissionedReason), typeof(string), r => r.Assignment.Device.DecommissionReason, csvToStringEncoded) }); + + // Model + metadata.Add(nameof(DeviceFlagExportOptions.ModelId), new List() { new Metadata(nameof(DeviceFlagExportOptions.ModelId), typeof(int), r => r.Assignment.Device.DeviceModel.Id, csvToStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.ModelDescription), new List() { new Metadata(nameof(DeviceFlagExportOptions.ModelDescription), typeof(string), r => r.Assignment.Device.DeviceModel.Description, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.ModelManufacturer), new List() { new Metadata(nameof(DeviceFlagExportOptions.ModelManufacturer), typeof(string), r => r.Assignment.Device.DeviceModel.Manufacturer, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.ModelModel), new List() { new Metadata(nameof(DeviceFlagExportOptions.ModelModel), typeof(string), r => r.Assignment.Device.DeviceModel.Model, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.ModelType), new List() { new Metadata(nameof(DeviceFlagExportOptions.ModelType), typeof(string), r => r.Assignment.Device.DeviceModel.ModelType, csvStringEncoded) }); + + // Batch + metadata.Add(nameof(DeviceFlagExportOptions.BatchId), new List() { new Metadata(nameof(DeviceFlagExportOptions.BatchId), typeof(int), r => r.Assignment.Device.DeviceBatch?.Id, csvToStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.BatchName), new List() { new Metadata(nameof(DeviceFlagExportOptions.BatchName), typeof(string), r => r.Assignment.Device.DeviceBatch?.Name, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.BatchPurchaseDate), new List() { new Metadata(nameof(DeviceFlagExportOptions.BatchPurchaseDate), typeof(DateTime), r => r.Assignment.Device.DeviceBatch?.PurchaseDate, csvNullableDateEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.BatchSupplier), new List() { new Metadata(nameof(DeviceFlagExportOptions.BatchSupplier), typeof(string), r => r.Assignment.Device.DeviceBatch?.Supplier, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.BatchUnitCost), new List() { new Metadata(nameof(DeviceFlagExportOptions.BatchUnitCost), typeof(decimal), r => r.Assignment.Device.DeviceBatch?.UnitCost, csvCurrencyEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.BatchWarrantyValidUntilDate), new List() { new Metadata(nameof(DeviceFlagExportOptions.BatchWarrantyValidUntilDate), typeof(DateTime), r => r.Assignment.Device.DeviceBatch?.WarrantyValidUntil, csvNullableDateEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.BatchInsuredDate), new List() { new Metadata(nameof(DeviceFlagExportOptions.BatchInsuredDate), typeof(DateTime), r => r.Assignment.Device.DeviceBatch?.InsuredDate, csvNullableDateEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.BatchInsuranceSupplier), new List() { new Metadata(nameof(DeviceFlagExportOptions.BatchInsuranceSupplier), typeof(string), r => r.Assignment.Device.DeviceBatch?.InsuranceSupplier, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.BatchInsuredUntilDate), new List() { new Metadata(nameof(DeviceFlagExportOptions.BatchInsuredUntilDate), typeof(DateTime), r => r.Assignment.Device.DeviceBatch?.InsuredUntil, csvNullableDateEncoded) }); + + // Profile + metadata.Add(nameof(DeviceFlagExportOptions.ProfileId), new List() { new Metadata(nameof(DeviceFlagExportOptions.ProfileId), typeof(int), r => r.Assignment.Device.DeviceProfile?.Id, csvToStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.ProfileName), new List() { new Metadata(nameof(DeviceFlagExportOptions.ProfileName), typeof(string), r => r.Assignment.Device.DeviceProfile?.Name, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.ProfileShortName), new List() { new Metadata(nameof(DeviceFlagExportOptions.ProfileShortName), typeof(string), r => r.Assignment.Device.DeviceProfile?.ShortName, csvStringEncoded) }); + + + // User + metadata.Add(nameof(DeviceFlagExportOptions.AssignedUserId), new List() { new Metadata(nameof(DeviceFlagExportOptions.AssignedUserId), typeof(string), r => r.Assignment.Device?.AssignedUser?.UserId, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.AssignedUserDisplayName), new List() { new Metadata(nameof(DeviceFlagExportOptions.AssignedUserDisplayName), typeof(string), r => r.Assignment.Device?.AssignedUser?.DisplayName, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.AssignedUserSurname), new List() { new Metadata(nameof(DeviceFlagExportOptions.AssignedUserSurname), typeof(string), r => r.Assignment.Device?.AssignedUser?.Surname, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.AssignedUserGivenName), new List() { new Metadata(nameof(DeviceFlagExportOptions.AssignedUserGivenName), typeof(string), r => r.Assignment.Device?.AssignedUser?.GivenName, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.AssignedUserPhoneNumber), new List() { new Metadata(nameof(DeviceFlagExportOptions.AssignedUserPhoneNumber), typeof(string), r => r.Assignment.Device?.AssignedUser?.PhoneNumber, csvStringEncoded) }); + metadata.Add(nameof(DeviceFlagExportOptions.AssignedUserEmailAddress), new List() { new Metadata(nameof(DeviceFlagExportOptions.AssignedUserEmailAddress), typeof(string), r => r.Assignment.Device?.AssignedUser?.EmailAddress, csvStringEncoded) }); + if (userDetailsCustomKeys != null) + { + var userDetailCustomFields = new List(); + foreach (var detailKey in userDetailsCustomKeys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)) + { + var key = detailKey; + userDetailCustomFields.Add(new Metadata(detailKey, detailKey, typeof(string), r => r.AssignedUserCustomDetails != null && r.AssignedUserCustomDetails.TryGetValue(key, out var value) ? value : null, csvStringEncoded)); + } + metadata.Add(nameof(DeviceFlagExportOptions.AssignedUserDetailCustom), userDetailCustomFields); + } + + return metadata; + } + + } +} diff --git a/Disco.Services/Devices/DeviceFlags/DeviceFlagExportTask.cs b/Disco.Services/Devices/DeviceFlags/DeviceFlagExportTask.cs new file mode 100644 index 00000000..f328f688 --- /dev/null +++ b/Disco.Services/Devices/DeviceFlags/DeviceFlagExportTask.cs @@ -0,0 +1,46 @@ +using Disco.Data.Repository; +using Disco.Models.Services.Devices.DeviceFlag; +using Disco.Services.Exporting; +using Disco.Services.Tasks; +using Quartz; + +namespace Disco.Services.Devices.DeviceFlags +{ + public class DeviceFlagExportTask : ScheduledTask + { + private const string JobDataMapContext = "Context"; + + public override string TaskName { get; } = "Export Device Flags"; + public override bool SingleInstanceTask { get { return false; } } + public override bool CancelInitiallySupported { get { return false; } } + + public static ExportTaskContext ScheduleNow(DeviceFlagExportOptions options) + { + // Build Context + var context = new ExportTaskContext(options); + + // Build Data Map + var task = new DeviceFlagExportTask(); + JobDataMap taskData = new JobDataMap() { { JobDataMapContext, context } }; + + // Schedule Task + context.TaskStatus = task.ScheduleTask(taskData); + + return context; + } + + protected override void ExecuteTask() + { + var context = (ExportTaskContext)ExecutionContext.JobDetail.JobDataMap[JobDataMapContext]; + + Status.UpdateStatus(10, "Exporting Device Flag Records", "Starting..."); + + using (DiscoDataContext Database = new DiscoDataContext()) + { + var export = new DeviceFlagExport(Database, context.Options); + + context.Result = export.Generate(Status); + } + } + } +} diff --git a/Disco.Services/Devices/DeviceFlags/DeviceFlagExtensions.cs b/Disco.Services/Devices/DeviceFlags/DeviceFlagExtensions.cs new file mode 100644 index 00000000..3c3dcb97 --- /dev/null +++ b/Disco.Services/Devices/DeviceFlags/DeviceFlagExtensions.cs @@ -0,0 +1,196 @@ +using Disco.Data.Repository; +using Disco.Models.Repository; +using Disco.Services.Authorization; +using Disco.Services.Expressions; +using Disco.Services.Logging; +using Disco.Services.Users; +using System; +using System.Collections; +using System.Data.Entity; +using System.Linq; + +namespace Disco.Services +{ + public static class DeviceFlagExtensions + { + + #region Edit Comments + public static bool CanEditComments(this DeviceFlagAssignment fa) + { + return UserService.CurrentAuthorization.Has(Claims.Device.Actions.EditFlags); + } + public static void OnEditComments(this DeviceFlagAssignment fa, string Comments) + { + if (!fa.CanEditComments()) + throw new InvalidOperationException("Editing comments for device flags is denied"); + + fa.Comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim(); + } + #endregion + + #region Remove + public static bool CanRemove(this DeviceFlagAssignment fa) + { + if (fa.RemovedDate.HasValue) + return false; + + return UserService.CurrentAuthorization.Has(Claims.Device.Actions.RemoveFlags); + } + public static void OnRemove(this DeviceFlagAssignment fa, DiscoDataContext Database, User RemovingUser) + { + if (!fa.CanRemove()) + throw new InvalidOperationException("Removing device flags is denied"); + + fa.OnRemoveUnsafe(Database, RemovingUser); + } + + public static void OnRemoveUnsafe(this DeviceFlagAssignment fa, DiscoDataContext Database, User RemovingUser) + { + fa = Database.DeviceFlagAssignments + .Include(a => a.DeviceFlag) + .First(a => a.Id == fa.Id); + RemovingUser = Database.Users.First(u => u.UserId == RemovingUser.UserId); + + fa.RemovedDate = DateTime.Now; + fa.RemovedUserId = RemovingUser.UserId; + + if (!string.IsNullOrWhiteSpace(fa.DeviceFlag.OnUnassignmentExpression)) + { + try + { + Database.SaveChanges(); + var expressionResult = fa.EvaluateOnUnassignmentExpression(Database, RemovingUser, fa.AddedDate); + if (!string.IsNullOrWhiteSpace(expressionResult)) + { + fa.OnUnassignmentExpressionResult = expressionResult; + Database.SaveChanges(); + } + } + catch (Exception ex) + { + SystemLog.LogException("Device Flag Expression - OnUnassignmentExpression", ex); + } + } + } + #endregion + + #region Add + public static bool CanAddDeviceFlags(this Device d) + { + return UserService.CurrentAuthorization.Has(Claims.Device.Actions.AddFlags); + } + public static bool CanAddDeviceFlag(this Device d, DeviceFlag flag) + { + // Shortcut + if (!d.CanAddDeviceFlags()) + return false; + + // Already has Device Flag? + if (d.DeviceFlagAssignments.Any(fa => !fa.RemovedDate.HasValue && fa.DeviceFlagId == flag.Id)) + return false; + + return true; + } + public static DeviceFlagAssignment OnAddDeviceFlag(this Device d, DiscoDataContext Database, DeviceFlag flag, User AddingUser, string Comments) + { + if (!d.CanAddDeviceFlag(flag)) + throw new InvalidOperationException("Adding device flag is denied"); + + return d.OnAddDeviceFlagUnsafe(Database, flag, AddingUser, Comments); + } + + public static DeviceFlagAssignment OnAddDeviceFlagUnsafe(this Device d, DiscoDataContext Database, DeviceFlag flag, User AddingUser, string Comments) + { + flag = Database.DeviceFlags.First(f => f.Id == flag.Id); + d = Database.Devices.First(de => de.SerialNumber == d.SerialNumber); + AddingUser = Database.Users.First(user => user.UserId == AddingUser.UserId); + + var fa = new DeviceFlagAssignment() + { + DeviceFlag = flag, + Device = d, + AddedDate = DateTime.Now, + AddedUser = AddingUser, + AddedUserId = AddingUser.UserId, + Comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim() + }; + + Database.DeviceFlagAssignments.Add(fa); + + if (!string.IsNullOrWhiteSpace(flag.OnAssignmentExpression)) + { + try + { + Database.SaveChanges(); + var expressionResult = fa.EvaluateOnAssignmentExpression(Database, AddingUser, fa.AddedDate); + if (!string.IsNullOrWhiteSpace(expressionResult)) + { + fa.OnAssignmentExpressionResult = expressionResult; + Database.SaveChanges(); + } + } + catch (Exception ex) + { + SystemLog.LogException("Device Flag Expression - OnAssignmentExpression", ex); + } + } + + return fa; + } + #endregion + + #region Expressions + + public static Expression OnAssignmentExpressionFromCache(this DeviceFlag uf) + { + return ExpressionCache.GetOrCreateSingleExpressions($"DeviceFlag_OnAssignmentExpression_{uf.Id}", () => Expression.TokenizeSingleDynamic(null, uf.OnAssignmentExpression, 0)); + } + + public static void OnAssignmentExpressionInvalidateCache(this DeviceFlag uf) + { + ExpressionCache.InvalidateSingleCache($"DeviceFlag_OnAssignmentExpression_{uf.Id}"); + } + + public static string EvaluateOnAssignmentExpression(this DeviceFlagAssignment dfa, DiscoDataContext Database, User AddingUser, DateTime TimeStamp) + { + if (!string.IsNullOrEmpty(dfa.DeviceFlag.OnAssignmentExpression)) + { + Expression compiledExpression = dfa.DeviceFlag.OnAssignmentExpressionFromCache(); + IDictionary evaluatorVariables = Expression.StandardVariables(null, Database, AddingUser, TimeStamp, null, dfa.Device); + object result = compiledExpression.EvaluateFirst(dfa, evaluatorVariables); + if (result == null) + return null; + else + return result.ToString(); + } + return null; + } + + public static Expression OnUnassignmentExpressionFromCache(this DeviceFlag df) + { + return ExpressionCache.GetOrCreateSingleExpressions($"DeviceFlag_OnUnassignmentExpression_{df.Id}", () => Expression.TokenizeSingleDynamic(null, df.OnUnassignmentExpression, 0)); + } + + public static void OnUnassignmentExpressionInvalidateCache(this DeviceFlag df) + { + ExpressionCache.InvalidateSingleCache($"DeviceFlag_OnUnassignmentExpression_{df.Id}"); + } + + public static string EvaluateOnUnassignmentExpression(this DeviceFlagAssignment dfa, DiscoDataContext Database, User RemovingUser, DateTime TimeStamp) + { + if (!string.IsNullOrEmpty(dfa.DeviceFlag.OnUnassignmentExpression)) + { + Expression compiledExpression = dfa.DeviceFlag.OnUnassignmentExpressionFromCache(); + IDictionary evaluatorVariables = Expression.StandardVariables(null, Database, RemovingUser, TimeStamp, null, dfa.Device); + object result = compiledExpression.EvaluateFirst(dfa, evaluatorVariables); + if (result == null) + return null; + else + return result.ToString(); + } + return null; + } + + #endregion + } +} diff --git a/Disco.Services/Devices/DeviceFlags/DeviceFlagService.cs b/Disco.Services/Devices/DeviceFlags/DeviceFlagService.cs new file mode 100644 index 00000000..adb35f1c --- /dev/null +++ b/Disco.Services/Devices/DeviceFlags/DeviceFlagService.cs @@ -0,0 +1,241 @@ +using Disco.Data.Repository; +using Disco.Data.Repository.Monitor; +using Disco.Models.Repository; +using Disco.Services.Extensions; +using Disco.Services.Tasks; +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Reactive.Linq; + +namespace Disco.Services.Devices.DeviceFlags +{ + public static class DeviceFlagService + { + private static Cache _cache; + internal static Lazy> DeviceFlagAssignmentRepositoryEvents; + + static DeviceFlagService() + { + // Statically defined (lazy) Assignment Repository Definition + DeviceFlagAssignmentRepositoryEvents = + new Lazy>(() => + RepositoryMonitor.StreamAfterCommit.Where(e => + e.EntityType == typeof(DeviceFlagAssignment) && + (e.EventType != RepositoryMonitorEventType.Modified || + e.ModifiedProperties.Contains(nameof(DeviceFlagAssignment.RemovedDate))) + ) + ); + } + + public static void Initialize(DiscoDataContext database) + { + _cache = new Cache(database); + + // Initialize Managed Groups (if configured) + _cache.GetDeviceFlags().ForEach(uf => + { + DeviceFlagDevicesManagedGroup.Initialize(uf); + DeviceFlagDeviceAssignedUsersManagedGroup.Initialize(uf); + }); + } + + public static List GetDeviceFlags() { return _cache.GetDeviceFlags(); } + public static DeviceFlag GetDeviceFlag(int deviceFlagId) { return _cache.GetDeviceFlag(deviceFlagId); } + + #region Device Flag Maintenance + public static DeviceFlag CreateDeviceFlag(DiscoDataContext database, DeviceFlag deviceFlag) + { + // Verify + if (string.IsNullOrWhiteSpace(deviceFlag.Name)) + throw new ArgumentException("The Device Flag Name is required", nameof(deviceFlag)); + + // Name Unique + if (_cache.GetDeviceFlags().Any(f => f.Name == deviceFlag.Name)) + throw new ArgumentException("Another Device Flag already exists with that name", nameof(deviceFlag)); + + // Clone to break reference + var flag = new DeviceFlag() + { + Name = deviceFlag.Name, + Description = deviceFlag.Description, + Icon = deviceFlag.Icon, + IconColour = deviceFlag.IconColour, + DevicesLinkedGroup = deviceFlag.DevicesLinkedGroup, + DeviceUsersLinkedGroup = deviceFlag.DeviceUsersLinkedGroup, + }; + + database.DeviceFlags.Add(flag); + database.SaveChanges(); + + _cache.AddOrUpdate(flag); + + return flag; + } + public static DeviceFlag Update(DiscoDataContext database, DeviceFlag deviceFlag) + { + // Verify + if (string.IsNullOrWhiteSpace(deviceFlag.Name)) + throw new ArgumentException("The Device Flag Name is required", nameof(deviceFlag)); + + // Name Unique + if (_cache.GetDeviceFlags().Any(f => f.Id != deviceFlag.Id && f.Name == deviceFlag.Name)) + throw new ArgumentException("Another Device Flag already exists with that name", nameof(deviceFlag)); + + database.SaveChanges(); + + _cache.AddOrUpdate(deviceFlag); + DeviceFlagDevicesManagedGroup.Initialize(deviceFlag); + DeviceFlagDeviceAssignedUsersManagedGroup.Initialize(deviceFlag); + + return deviceFlag; + } + public static void DeleteDeviceFlag(DiscoDataContext database, int deviceFlagId, IScheduledTaskStatus status) + { + var flag = database.DeviceFlags.Find(deviceFlagId); + + // Dispose of AD Managed Groups + Interop.ActiveDirectory.ActiveDirectory.Context.ManagedGroups.Remove(DeviceFlagDeviceAssignedUsersManagedGroup.GetKey(flag)); + Interop.ActiveDirectory.ActiveDirectory.Context.ManagedGroups.Remove(DeviceFlagDevicesManagedGroup.GetKey(flag)); + + // Delete Assignments + status.UpdateStatus(0, $"Removing '{flag.Name}' [{flag.Id}] Device Flag", "Starting"); + var flagAssignments = database.DeviceFlagAssignments.Where(fa => fa.DeviceFlagId == flag.Id).ToList(); + if (flagAssignments.Count > 0) + { + status.UpdateStatus(20, "Removing flag from devices"); + flagAssignments.ForEach(flagAssignment => database.DeviceFlagAssignments.Remove(flagAssignment)); + database.SaveChanges(); + } + + // Delete Flag + status.UpdateStatus(90, "Deleting Device Flag"); + database.DeviceFlags.Remove(flag); + database.SaveChanges(); + + // Remove from Cache + _cache.Remove(deviceFlagId); + + status.Finished($"Successfully Deleted Device Flag: '{flag.Name}' [{flag.Id}]"); + } + #endregion + + #region Bulk Assignment + public static IEnumerable BulkAssignAddDevices(DiscoDataContext database, DeviceFlag deviceFlag, User technician, string comments, List devices, IScheduledTaskStatus status) + { + if (devices.Count > 0) + { + double progressInterval; + const int databaseChunkSize = 100; + comments = string.IsNullOrWhiteSpace(comments) ? null : comments.Trim(); + + var addDevices = devices.Where(d => !d.DeviceFlagAssignments.Any(a => a.DeviceFlagId == deviceFlag.Id && !a.RemovedDate.HasValue)).ToList(); + + progressInterval = (double)100 / addDevices.Count; + + var addedDeviceAssignments = addDevices.Chunk(databaseChunkSize).SelectMany((chunk, chunkIndex) => + { + var chunkIndexOffset = databaseChunkSize * chunkIndex; + + var chunkResults = chunk.Select((device, index) => + { + status.UpdateStatus((chunkIndexOffset + index) * progressInterval, $"Assigning Flag: {device}"); + + return device.OnAddDeviceFlag(database, deviceFlag, technician, comments); + }).ToList(); + + // Save Chunk Items to Database + database.SaveChanges(); + + return chunkResults; + }).Where(fa => fa != null).ToList(); + + status.SetFinishedMessage($"{addDevices.Count} Devices/s Added; {(devices.Count - addDevices.Count)} Devices/s Skipped"); + + return addedDeviceAssignments; + } + else + { + status.SetFinishedMessage("No changes found"); + return Enumerable.Empty(); + } + } + + public static IEnumerable BulkAssignOverrideDevices(DiscoDataContext database, DeviceFlag deviceFlag, User technician, string comments, List devices, IScheduledTaskStatus status) + { + double progressInterval; + const int databaseChunkSize = 100; + comments = string.IsNullOrWhiteSpace(comments) ? null : comments.Trim(); + + status.UpdateStatus(0, "Calculating assignment changes"); + + var currentAssignments = database.DeviceFlagAssignments.Include(fa => fa.Device).Where(a => a.DeviceFlagId == deviceFlag.Id && !a.RemovedDate.HasValue).ToList(); + var removeAssignments = currentAssignments.Where(ca => !devices.Any(d => d.SerialNumber.Equals(ca.DeviceSerialNumber, StringComparison.OrdinalIgnoreCase))).ToList(); + var addAssignments = devices.Where(d => !currentAssignments.Any(ca => ca.DeviceSerialNumber.Equals(d.SerialNumber, StringComparison.OrdinalIgnoreCase))).ToList(); + + if (removeAssignments.Count > 0 || addAssignments.Count > 0) + { + progressInterval = (double)100 / (removeAssignments.Count + addAssignments.Count); + var removedDateTime = DateTime.Now; + + // Remove Assignments + removeAssignments.Chunk(databaseChunkSize).SelectMany((chunk, chunkIndex) => + { + var chunkIndexOffset = (chunkIndex * databaseChunkSize) + removeAssignments.Count; + + var chunkResults = chunk.Select((flagAssignment, index) => + { + status.UpdateStatus((chunkIndexOffset + index) * progressInterval, $"Removing Flag: {flagAssignment.Device}"); + + flagAssignment.OnRemoveUnsafe(database, technician); + + return flagAssignment; + }).ToList(); + + // Save Chunk Items to Database + database.SaveChanges(); + + return chunkResults; + }).ToList(); + + // Add Assignments + var addedAssignments = addAssignments.Chunk(databaseChunkSize).SelectMany((chunk, chunkIndex) => + { + var chunkIndexOffset = (chunkIndex * databaseChunkSize) + removeAssignments.Count; + + var chunkResults = chunk.Select((device, index) => + { + status.UpdateStatus((chunkIndexOffset + index) * progressInterval, $"Assigning Flag: {device}"); + + return device.OnAddDeviceFlag(database, deviceFlag, technician, comments); + }).ToList(); + + // Save Chunk Items to Database + database.SaveChanges(); + + return chunkResults; + }).ToList(); + + status.SetFinishedMessage($"{addAssignments.Count} Devices/s Added; {removeAssignments.Count} Devices/s Removed; {(devices.Count - addAssignments.Count)} Devices/s Skipped"); + + return addedAssignments; + } + else + { + status.SetFinishedMessage("No changes found"); + return Enumerable.Empty(); + } + } + #endregion + + public static string RandomUnusedIcon() + { + return UIHelpers.RandomIcon(_cache.GetDeviceFlags().Select(f => f.Icon)); + } + public static string RandomUnusedThemeColour() + { + return UIHelpers.RandomThemeColour(_cache.GetDeviceFlags().Select(f => f.IconColour)); + } + } +} diff --git a/Disco.Services/Devices/DeviceFlags/DeviceFlagsBulkAssignTask.cs b/Disco.Services/Devices/DeviceFlags/DeviceFlagsBulkAssignTask.cs new file mode 100644 index 00000000..ab6a54c5 --- /dev/null +++ b/Disco.Services/Devices/DeviceFlags/DeviceFlagsBulkAssignTask.cs @@ -0,0 +1,84 @@ +using Disco.Data.Repository; +using Disco.Models.Repository; +using Disco.Services.Tasks; +using Quartz; +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; + +namespace Disco.Services.Devices.DeviceFlags +{ + public class DeviceFlagBulkAssignTask : ScheduledTask + { + public override string TaskName { get { return "Device Flags - Bulk Assign Devices"; } } + + public override bool SingleInstanceTask { get { return false; } } + public override bool CancelInitiallySupported { get { return false; } } + public override bool LogExceptionsOnly { get { return true; } } + + protected override void ExecuteTask() + { + int deviceFlagId = (int)ExecutionContext.JobDetail.JobDataMap["DeviceFlagId"]; + string technicianUserId = (string)ExecutionContext.JobDetail.JobDataMap["TechnicianUserId"]; + string comments = (string)ExecutionContext.JobDetail.JobDataMap["Comments"]; + List deviceSerialNumbers = (List)ExecutionContext.JobDetail.JobDataMap["DeviceSerialNumbers"]; + bool @override = (bool)ExecutionContext.JobDetail.JobDataMap["Override"]; + + using (DiscoDataContext Database = new DiscoDataContext()) + { + // Load Flag + var flag = Database.DeviceFlags.FirstOrDefault(uf => uf.Id == deviceFlagId); + if (flag == null) + throw new Exception("Invalid Device Flag Id"); + + Status.UpdateStatus(0, string.Format("Bulk Assigning Devices to Device Flag: {0}", flag.Name), "Preparing to start"); + + // Load Technician + var technician = Database.Users.FirstOrDefault(user => user.UserId == technicianUserId); + if (technician == null) + throw new Exception("Invalid Technician User Id"); + + // Parse Devices + Status.UpdateStatus(10, "Loading devices from the database"); + var devices = Database.Devices + .Include(d => d.DeviceFlagAssignments) + .Where(d => deviceSerialNumbers.Contains(d.SerialNumber)).ToList(); + + var missingDevices = deviceSerialNumbers.Where(sn => !devices.Any(u => string.Equals(sn, u.SerialNumber, StringComparison.OrdinalIgnoreCase))).ToList(); + if (missingDevices.Count > 0) + { + throw new InvalidOperationException(string.Format("Bulk assignment aborted, invalid Serial Numbers: {0}", string.Join(", ", missingDevices))); + } + devices = devices.OrderBy(d => d.SerialNumber).ToList(); + + Status.ProgressOffset = 50; + Status.ProgressMultiplier = 0.5; + + if (@override) + { + DeviceFlagService.BulkAssignOverrideDevices(Database, flag, technician, comments, devices, Status); + } + else + { + DeviceFlagService.BulkAssignAddDevices(Database, flag, technician, comments, devices, Status); + } + } + } + + public static ScheduledTaskStatus ScheduleBulkAssignDevices(DeviceFlag deviceFlag, User technician, string comments, List deviceSerialNumbers, bool @override) + { + JobDataMap taskData = new JobDataMap() { + {"DeviceFlagId", deviceFlag.Id }, + {"TechnicianUserId", technician.UserId }, + {"Comments", comments }, + {"DeviceSerialNumbers", deviceSerialNumbers }, + {"Override", @override } + }; + + var instance = new DeviceFlagBulkAssignTask(); + + return instance.ScheduleTask(taskData); + } + } +} \ No newline at end of file diff --git a/Disco.Services/Devices/DeviceFlags/DeviceFlagsDeleteTask.cs b/Disco.Services/Devices/DeviceFlags/DeviceFlagsDeleteTask.cs new file mode 100644 index 00000000..72029483 --- /dev/null +++ b/Disco.Services/Devices/DeviceFlags/DeviceFlagsDeleteTask.cs @@ -0,0 +1,34 @@ +using Disco.Data.Repository; +using Disco.Services.Tasks; +using Quartz; + +namespace Disco.Services.Devices.DeviceFlags +{ + public class DeviceFlagDeleteTask : ScheduledTask + { + public override string TaskName { get { return "Device Flags - Delete Flag"; } } + + public override bool SingleInstanceTask { get { return false; }} + public override bool CancelInitiallySupported { get { return false; } } + public override bool LogExceptionsOnly { get { return true; } } + + protected override void ExecuteTask() + { + int deviceFlagId = (int)ExecutionContext.JobDetail.JobDataMap["DeviceFlagId"]; + + using (DiscoDataContext Database = new DiscoDataContext()) + { + DeviceFlagService.DeleteDeviceFlag(Database, deviceFlagId, Status); + } + } + + public static ScheduledTaskStatus ScheduleNow(int deviceFlagId) + { + var taskData = new JobDataMap() { { "DeviceFlagId", deviceFlagId } }; + + var instance = new DeviceFlagDeleteTask(); + + return instance.ScheduleTask(taskData); + } + } +} \ No newline at end of file diff --git a/Disco.Services/Interop/ActiveDirectory/ActiveDirectoryManagedGroups.cs b/Disco.Services/Interop/ActiveDirectory/ActiveDirectoryManagedGroups.cs index 0b0c0a85..37168053 100644 --- a/Disco.Services/Interop/ActiveDirectory/ActiveDirectoryManagedGroups.cs +++ b/Disco.Services/Interop/ActiveDirectory/ActiveDirectoryManagedGroups.cs @@ -61,10 +61,25 @@ namespace Disco.Services.Interop.ActiveDirectory return false; } + public bool TryGetValue(string Key, out ADManagedGroup ManagedGroup) { return managedGroups.TryGetValue(Key, out ManagedGroup); } + public bool TryGetValue(string key, out T managedGroup) where T : ADManagedGroup + { + if (managedGroups.TryGetValue(key, out var item) && item is T typedItem) + { + managedGroup = typedItem; + return true; + } + else + { + managedGroup = null; + return false; + } + } + public List Values { get