feature: user flag assignment exporting

This commit is contained in:
Gary Sharp
2023-11-11 20:04:52 +11:00
parent 143dc1b3e6
commit 53e57d4017
25 changed files with 1790 additions and 183 deletions
+8
View File
@@ -77,6 +77,7 @@ namespace Disco.Services.Authorization
{ "Config.UserFlag.Configure", new Tuple<Func<RoleClaims, bool>, Action<RoleClaims, bool>, string, string, bool>(c => c.Config.UserFlag.Configure, (c, v) => c.Config.UserFlag.Configure = v, "Configure User Flags", "Can configure user flags", false) },
{ "Config.UserFlag.Create", new Tuple<Func<RoleClaims, bool>, Action<RoleClaims, bool>, string, string, bool>(c => c.Config.UserFlag.Create, (c, v) => c.Config.UserFlag.Create = v, "Create User Flags", "Can create user flags", false) },
{ "Config.UserFlag.Delete", new Tuple<Func<RoleClaims, bool>, Action<RoleClaims, bool>, string, string, bool>(c => c.Config.UserFlag.Delete, (c, v) => c.Config.UserFlag.Delete = v, "Delete User Flags", "Can delete user flags", false) },
{ "Config.UserFlag.Export", new Tuple<Func<RoleClaims, bool>, Action<RoleClaims, bool>, string, string, bool>(c => c.Config.UserFlag.Export, (c, v) => c.Config.UserFlag.Export = v, "Export User Flag Assignments", "Can export user flag assignments", false) },
{ "Config.UserFlag.Show", new Tuple<Func<RoleClaims, bool>, Action<RoleClaims, bool>, string, string, bool>(c => c.Config.UserFlag.Show, (c, v) => c.Config.UserFlag.Show = v, "Show User Flags", "Can show user flags", false) },
{ "Config.Show", new Tuple<Func<RoleClaims, bool>, Action<RoleClaims, bool>, string, string, bool>(c => c.Config.Show, (c, v) => c.Config.Show = v, "Show Configuration", "Can show the configuration menu", false) },
{ "Job.Lists.AllOpen", new Tuple<Func<RoleClaims, bool>, Action<RoleClaims, bool>, string, string, bool>(c => c.Job.Lists.AllOpen, (c, v) => c.Job.Lists.AllOpen = v, "All Open List", "Can show list", false) },
@@ -306,6 +307,7 @@ namespace Disco.Services.Authorization
new ClaimNavigatorItem("Config.UserFlag.Configure", false),
new ClaimNavigatorItem("Config.UserFlag.Create", false),
new ClaimNavigatorItem("Config.UserFlag.Delete", false),
new ClaimNavigatorItem("Config.UserFlag.Export", false),
new ClaimNavigatorItem("Config.UserFlag.Show", false)
}),
new ClaimNavigatorItem("Config.Show", false)
@@ -593,6 +595,7 @@ namespace Disco.Services.Authorization
c.Config.UserFlag.Configure = true;
c.Config.UserFlag.Create = true;
c.Config.UserFlag.Delete = true;
c.Config.UserFlag.Export = true;
c.Config.UserFlag.Show = true;
c.Config.Show = true;
c.Job.Lists.AllOpen = true;
@@ -1109,6 +1112,11 @@ namespace Disco.Services.Authorization
/// </summary>
public const string Delete = "Config.UserFlag.Delete";
/// <summary>Export User Flag Assignments
/// <para>Can export user flag assignments</para>
/// </summary>
public const string Export = "Config.UserFlag.Export";
/// <summary>Show User Flags
/// <para>Can show user flags</para>
/// </summary>
@@ -11,6 +11,8 @@
[ClaimDetails("Delete User Flags", "Can delete user flags")]
public bool Delete { get; set; }
[ClaimDetails("Export User Flag Assignments", "Can export user flag assignments")]
public bool Export { get; set; }
[ClaimDetails("Show User Flags", "Can show user flags")]
public bool Show { get; set; }
+2
View File
@@ -461,6 +461,8 @@
<Compile Include="Users\Contact\UserContactService.cs" />
<Compile Include="Users\UserExtensions.cs" />
<Compile Include="Users\UserFlags\Cache.cs" />
<Compile Include="Users\UserFlags\UserFlagExport.cs" />
<Compile Include="Users\UserFlags\UserFlagExportTask.cs" />
<Compile Include="Users\UserFlags\UserFlagExtensions.cs" />
<Compile Include="Users\UserFlags\UserFlagUserDevicesManagedGroup.cs" />
<Compile Include="Users\UserFlags\UserFlagUsersManagedGroup.cs" />
@@ -0,0 +1,176 @@
using Disco.Data.Repository;
using Disco.Models.Exporting;
using Disco.Models.Services.Exporting;
using Disco.Models.Services.Users.UserFlags;
using Disco.Services.Plugins.Features.DetailsProvider;
using Disco.Services.Tasks;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Linq;
namespace Disco.Services.Users.UserFlags
{
using Metadata = ExportFieldMetadata<UserFlagExportRecord>;
public class UserFlagExport
{
private readonly DiscoDataContext database;
private readonly UserFlagExportOptions options;
public UserFlagExport(DiscoDataContext database, UserFlagExportOptions 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<UserFlagExportRecord> BuildRecords(IScheduledTaskStatus status)
{
var query = database.UserFlagAssignments
.Include(a => a.User.UserDetails)
.Include(a => a.UserFlag)
.Where(a => options.UserFlagIds.Contains(a.UserFlagId));
if (options.CurrentOnly)
{
query = query.Where(a => !a.RemovedDate.HasValue);
}
// Update Users
if (options.UserDisplayName ||
options.UserSurname ||
options.UserGivenName ||
options.UserPhoneNumber ||
options.UserEmailAddress)
{
status.UpdateStatus(5, "Refreshing user details from Active Directory");
var userIds = query.Select(d => d.UserId).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 UserFlagExportRecord()
{
Assignment = a
}).ToList();
if (options.UserDetailCustom)
{
status.UpdateStatus(50, "Extracting custom user detail records");
var detailsService = new DetailsProviderService(database);
var cache = new Dictionary<string, Dictionary<string, string>>(StringComparer.Ordinal);
foreach (var record in records)
{
if (!cache.TryGetValue(record.Assignment.UserId, out var details))
details = detailsService.GetDetails(record.Assignment.User);
record.UserCustomDetails = details;
}
}
return records;
}
private List<Metadata> BuildMetadata(List<UserFlagExportRecord> records, IScheduledTaskStatus status)
{
status.UpdateStatus(80, "Building metadata");
IEnumerable<string> userDetailCustomKeys = null;
if (options.UserDetailCustom)
userDetailCustomKeys = records.Where(r => r.UserCustomDetails != null).SelectMany(r => r.UserCustomDetails.Keys).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
var accessors = BuildAccessors(userDetailCustomKeys);
return typeof(UserFlagExportOptions).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 == "User Flag") ? p.details.Name : $"{p.details.ShortName} {p.details.Name}";
});
return fieldMetadata;
}).ToList();
}
private static Dictionary<string, List<Metadata>> BuildAccessors(IEnumerable<string> userDetailsCustomKeys)
{
const string DateFormat = "yyyy-MM-dd";
const string DateTimeFormat = DateFormat + " HH:mm:ss";
Func<object, string> csvStringEncoded = (o) => o == null ? null : $"\"{((string)o).Replace("\"", "\"\"")}\"";
Func<object, string> csvToStringEncoded = (o) => o == null ? null : o.ToString();
Func<object, string> csvCurrencyEncoded = (o) => ((decimal?)o).HasValue ? ((decimal?)o).Value.ToString("C") : null;
Func<object, string> csvDateEncoded = (o) => ((DateTime)o).ToString(DateFormat);
Func<object, string> csvDateTimeEncoded = (o) => ((DateTime)o).ToString(DateTimeFormat);
Func<object, string> csvNullableDateEncoded = (o) => ((DateTime?)o).HasValue ? csvDateEncoded(o) : null;
Func<object, string> csvNullableDateTimeEncoded = (o) => ((DateTime?)o).HasValue ? csvDateTimeEncoded(o) : null;
var metadata = new Dictionary<string, List<Metadata>>();
// User Flag
metadata.Add(nameof(UserFlagExportOptions.Id), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.Id), typeof(string), r => r.Assignment.UserFlagId, csvToStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.Name), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.Name), typeof(string), r => r.Assignment.UserFlag.Name, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.Description), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.Description), typeof(string), r => r.Assignment.UserFlag.Description, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.Icon), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.Icon), typeof(string), r => r.Assignment.UserFlag.Icon, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.IconColour), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.IconColour), typeof(string), r => r.Assignment.UserFlag.IconColour, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.AssignmentId), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.AssignmentId), typeof(string), r => r.Assignment.Id, csvToStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.AddedDate), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.AddedDate), typeof(string), r => r.Assignment.AddedDate, csvDateTimeEncoded) });
metadata.Add(nameof(UserFlagExportOptions.AddedUserId), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.AddedUserId), typeof(string), r => r.Assignment.AddedUserId, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.RemovedUserId), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.RemovedUserId), typeof(string), r => r.Assignment.RemovedUserId, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.RemovedDate), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.RemovedDate), typeof(string), r => r.Assignment.RemovedDate, csvNullableDateTimeEncoded) });
metadata.Add(nameof(UserFlagExportOptions.Comments), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.Comments), typeof(string), r => r.Assignment.Comments, csvStringEncoded) });
// User
metadata.Add(nameof(UserFlagExportOptions.UserId), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.UserId), typeof(string), r => r.Assignment.User?.UserId, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.UserDisplayName), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.UserDisplayName), typeof(string), r => r.Assignment.User?.DisplayName, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.UserSurname), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.UserSurname), typeof(string), r => r.Assignment.User?.Surname, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.UserGivenName), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.UserGivenName), typeof(string), r => r.Assignment.User?.GivenName, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.UserPhoneNumber), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.UserPhoneNumber), typeof(string), r => r.Assignment.User?.PhoneNumber, csvStringEncoded) });
metadata.Add(nameof(UserFlagExportOptions.UserEmailAddress), new List<Metadata>() { new Metadata(nameof(UserFlagExportOptions.UserEmailAddress), typeof(string), r => r.Assignment.User?.EmailAddress, csvStringEncoded) });
if (userDetailsCustomKeys != null)
{
var userDetailCustomFields = new List<Metadata>();
foreach (var detailKey in userDetailsCustomKeys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))
{
var key = detailKey;
userDetailCustomFields.Add(new Metadata(detailKey, detailKey, typeof(string), r => r.UserCustomDetails != null && r.UserCustomDetails.TryGetValue(key, out var value) ? value : null, csvStringEncoded));
}
metadata.Add(nameof(UserFlagExportOptions.UserDetailCustom), userDetailCustomFields);
}
return metadata;
}
}
}
@@ -0,0 +1,46 @@
using Disco.Data.Repository;
using Disco.Models.Services.Users.UserFlags;
using Disco.Services.Exporting;
using Disco.Services.Tasks;
using Quartz;
namespace Disco.Services.Users.UserFlags
{
public class UserFlagExportTask : ScheduledTask
{
private const string JobDataMapContext = "Context";
public override string TaskName { get; } = "Export User Flags";
public override bool SingleInstanceTask { get { return false; } }
public override bool CancelInitiallySupported { get { return false; } }
public static ExportTaskContext<UserFlagExportOptions> ScheduleNow(UserFlagExportOptions options)
{
// Build Context
var context = new ExportTaskContext<UserFlagExportOptions>(options);
// Build Data Map
var task = new UserFlagExportTask();
JobDataMap taskData = new JobDataMap() { { JobDataMapContext, context } };
// Schedule Task
context.TaskStatus = task.ScheduleTask(taskData);
return context;
}
protected override void ExecuteTask()
{
var context = (ExportTaskContext<UserFlagExportOptions>)ExecutionContext.JobDetail.JobDataMap[JobDataMapContext];
Status.UpdateStatus(10, "Exporting User Flag Records", "Starting...");
using (DiscoDataContext Database = new DiscoDataContext())
{
var export = new UserFlagExport(Database, context.Options);
context.Result = export.Generate(Status);
}
}
}
}