feature: custom details first-class
custom details (such as those from the UserDetails plugin) can now be more deeply integrated throughtout the system
This commit is contained in:
@@ -322,6 +322,7 @@
|
||||
<Compile Include="Expressions\Extensions\ImageResultImplementations\FileMontageImageExpressionResult.cs" />
|
||||
<Compile Include="Expressions\Extensions\UserExt.cs" />
|
||||
<Compile Include="Expressions\IExpressionPart.cs" />
|
||||
<Compile Include="Expressions\LazyDictionary.cs" />
|
||||
<Compile Include="Expressions\TextExpressionPart.cs" />
|
||||
<Compile Include="Extensions\DateTimeExtensions.cs" />
|
||||
<Compile Include="Extensions\EnumerableExtensions.cs" />
|
||||
@@ -394,6 +395,10 @@
|
||||
<Compile Include="Plugins\Features\CertificateAuthorityProvider\CertificateAuthorityProviderFeature.cs" />
|
||||
<Compile Include="Plugins\Features\CertificateProvider\ProvisionPersonalCertificateResult.cs" />
|
||||
<Compile Include="Plugins\Features\CertificateAuthorityProvider\ProvisionAuthorityCertificatesResult.cs" />
|
||||
<Compile Include="Plugins\Features\DetailsProvider\DetailsProviderExtensions.cs" />
|
||||
<Compile Include="Plugins\Features\DetailsProvider\DetailsProviderFeature.cs" />
|
||||
<Compile Include="Plugins\Features\DetailsProvider\DetailsProviderService.cs" />
|
||||
<Compile Include="Plugins\Features\DocumentHandlerProvider\DocumentHandlerProviderFeature.cs" />
|
||||
<Compile Include="Plugins\Features\WirelessProfileProvider\ProvisionWirelessProfilesResult.cs" />
|
||||
<Compile Include="Plugins\Features\WirelessProfileProvider\WirelessProfile.cs" />
|
||||
<Compile Include="Plugins\Features\WirelessProfileProvider\WirelessProfileTransformation.cs" />
|
||||
|
||||
@@ -76,6 +76,36 @@ namespace Disco.Services
|
||||
return destination;
|
||||
}
|
||||
|
||||
public static Bitmap ResizeImage(this Image Source, int MaxHeight, Brush BackgroundColor = null)
|
||||
{
|
||||
// Determine Width
|
||||
int Height = (Source.Height > MaxHeight) ?
|
||||
MaxHeight :
|
||||
Source.Height;
|
||||
|
||||
int Width = (Source.Height > Height) ?
|
||||
(int)(((float)Height / Source.Height) * Source.Width) :
|
||||
Source.Width;
|
||||
|
||||
Bitmap destination = new Bitmap(Width, Height);
|
||||
destination.SetResolution(72, 72);
|
||||
using (Graphics destinationGraphics = Graphics.FromImage(destination))
|
||||
{
|
||||
destinationGraphics.CompositingQuality = CompositingQuality.HighQuality;
|
||||
destinationGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
destinationGraphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||
|
||||
if (BackgroundColor != null)
|
||||
destinationGraphics.FillRectangle(BackgroundColor, destinationGraphics.VisibleClipBounds);
|
||||
|
||||
float ratio = Math.Min((float)(destination.Width) / (float)(Source.Width), (float)(destination.Height) / (float)(Source.Height));
|
||||
|
||||
destinationGraphics.DrawImageResized(Source, ratio);
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
public static RectangleF CalculateResize(int SourceWidth, int SourceHeight, int TargetWidth, int TargetHeight, out float scaleRatio)
|
||||
{
|
||||
scaleRatio = Math.Min((float)(TargetWidth) / SourceWidth, (float)(TargetHeight) / SourceHeight);
|
||||
@@ -119,6 +149,33 @@ namespace Disco.Services
|
||||
graphics.DrawImage(SourceImage, resizeBounds, new RectangleF(0, 0, SourceImage.Width, SourceImage.Height), GraphicsUnit.Pixel);
|
||||
}
|
||||
|
||||
public static void DrawImageResized(this Graphics graphics, Image SourceImage, float? Scale = null, float LocationX = -1, float LocationY = -1)
|
||||
{
|
||||
RectangleF clipBounds = graphics.VisibleClipBounds;
|
||||
if (Scale == null) // Calculate Scale
|
||||
Scale = Math.Min(clipBounds.Width / SourceImage.Width, clipBounds.Height / SourceImage.Height);
|
||||
float newWidth = SourceImage.Width * Scale.Value;
|
||||
float newHeight = SourceImage.Height * Scale.Value;
|
||||
float newLeft = LocationX;
|
||||
float newTop = LocationY;
|
||||
|
||||
if (newLeft < 0 || newTop < 0)
|
||||
{
|
||||
if (newWidth < clipBounds.Width)
|
||||
newLeft = (clipBounds.Width - newWidth) / 2;
|
||||
else
|
||||
newLeft = 0;
|
||||
if (newHeight < clipBounds.Height)
|
||||
newTop = (clipBounds.Height - newHeight) / 2;
|
||||
else
|
||||
newTop = 0;
|
||||
}
|
||||
newLeft += clipBounds.Left;
|
||||
newTop += clipBounds.Top;
|
||||
|
||||
graphics.DrawImage(SourceImage, new RectangleF(newLeft, newTop, newWidth, newHeight), new RectangleF(0, 0, SourceImage.Width, SourceImage.Height), GraphicsUnit.Pixel);
|
||||
}
|
||||
|
||||
public static void DrawImageResized(this Graphics graphics, Image SourceImage, float Scale, float LocationX, float LocationY)
|
||||
{
|
||||
RectangleF clipBounds = graphics.VisibleClipBounds;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Models.Services.Plugins.Details;
|
||||
using Disco.Models.UI.Device;
|
||||
using Disco.Models.UI.Job;
|
||||
using Disco.Models.UI.User;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Disco.Services.Plugins.Features.DetailsProvider
|
||||
{
|
||||
public static class DetailsProviderExtensions
|
||||
{
|
||||
|
||||
public static void PopulateDetails(this UserShowModel model, DiscoDataContext database)
|
||||
{
|
||||
var service = new DetailsProviderService(database);
|
||||
|
||||
model.UserDetails = service.GetDetails(model.User);
|
||||
model.HasUserPhoto = service.HasUserPhoto(model.User);
|
||||
|
||||
var currentAssignments = model.User.CurrentDeviceUserAssignments();
|
||||
if (currentAssignments.Count > 0)
|
||||
{
|
||||
model.AssignedDevicesDetails = new Dictionary<string, DetailsResult>(currentAssignments.Count);
|
||||
|
||||
foreach (var device in currentAssignments)
|
||||
{
|
||||
model.AssignedDevicesDetails[device.DeviceSerialNumber] = service.GetDetails(device.Device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void PopulateDetails(this DeviceShowModel model, DiscoDataContext database)
|
||||
{
|
||||
var service = new DetailsProviderService(database);
|
||||
|
||||
model.DeviceDetails = service.GetDetails(model.Device);
|
||||
|
||||
if (model.Device.AssignedUser != null)
|
||||
{
|
||||
model.AssignedUserDetails = service.GetDetails(model.Device.AssignedUser);
|
||||
model.HasAssignedUserPhoto = service.HasUserPhoto(model.Device.AssignedUser);
|
||||
}
|
||||
}
|
||||
|
||||
public static void PopulateDetails(this JobShowModel model, DiscoDataContext database)
|
||||
{
|
||||
var service = new DetailsProviderService(database);
|
||||
|
||||
if (model.Job.Device != null)
|
||||
model.DeviceDetails = service.GetDetails(model.Job.Device);
|
||||
|
||||
if (model.Job.User != null)
|
||||
{
|
||||
model.UserDetails = service.GetDetails(model.Job.User);
|
||||
model.HasUserPhoto = service.HasUserPhoto(model.Job.User);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Models.Services.Plugins.Details;
|
||||
using System;
|
||||
|
||||
namespace Disco.Services.Plugins.Features.DetailsProvider
|
||||
{
|
||||
[PluginFeatureCategory(DisplayName = "Detail Providers")]
|
||||
public abstract class DetailsProviderFeature : PluginFeature
|
||||
{
|
||||
public abstract DetailsResult GetDetails(DiscoDataContext database, User user, DateTime? cacheTimestamp);
|
||||
public abstract DetailsResult GetDetails(DiscoDataContext database, Device device, DateTime? cacheTimestamp);
|
||||
public abstract byte[] GetUserPhoto(DiscoDataContext database, User user, DateTime? cacheTimestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using Disco.Data.Repository;
|
||||
using Disco.Models.Repository;
|
||||
using Disco.Models.Services.Plugins.Details;
|
||||
using Disco.Services.Authorization;
|
||||
using Disco.Services.Users;
|
||||
using Exceptionless.Json;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Disco.Services.Plugins.Features.DetailsProvider
|
||||
{
|
||||
public class DetailsProviderService
|
||||
{
|
||||
private const string DetailsScope = "Details";
|
||||
private readonly DiscoDataContext database;
|
||||
|
||||
public DetailsProviderService(DiscoDataContext database)
|
||||
{
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public bool HasUserPhoto(User user)
|
||||
{
|
||||
var cachePath = GetUserPhotoCachePath(user);
|
||||
if (File.Exists(cachePath))
|
||||
return true;
|
||||
|
||||
// slow-path: this should only happen once,
|
||||
// the first time, before we cache
|
||||
var photo = GetUserPhoto(user);
|
||||
|
||||
return photo != null;
|
||||
}
|
||||
|
||||
public byte[] GetUserPhoto(User user)
|
||||
{
|
||||
var cachePath = GetUserPhotoCachePath(user);
|
||||
var cacheAge = default(DateTime?);
|
||||
if (File.Exists(cachePath))
|
||||
cacheAge = File.GetLastWriteTime(cachePath);
|
||||
|
||||
var features = Plugins.GetPluginFeatures(typeof(DetailsProviderFeature));
|
||||
|
||||
foreach (var feature in features)
|
||||
{
|
||||
var instance = feature.CreateInstance<DetailsProviderFeature>();
|
||||
var result = instance.GetUserPhoto(database, user, cacheAge);
|
||||
if (result != null)
|
||||
{
|
||||
// resize image
|
||||
using (var originalStream = new MemoryStream(result))
|
||||
{
|
||||
using (var originalImage = Image.FromStream(originalStream))
|
||||
{
|
||||
using (var resizedImage = originalImage.ResizeImage(192, Brushes.White))
|
||||
{
|
||||
using (var savedResizedImage = (MemoryStream)resizedImage.SaveJpg(85))
|
||||
{
|
||||
result = savedResizedImage.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cachePath));
|
||||
File.WriteAllBytes(cachePath, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// serve from cache
|
||||
if (cacheAge.HasValue)
|
||||
return File.ReadAllBytes(cachePath);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetUserPhotoCachePath(User user)
|
||||
{
|
||||
var hasher = new SHA1Managed();
|
||||
var userHash = BitConverter.ToString(hasher.ComputeHash(Encoding.UTF8.GetBytes(user.UserId))).Replace("-", string.Empty);
|
||||
return Path.Combine(database.DiscoConfiguration.PluginUserPhotosLocation, userHash.Substring(0, 2), $"{userHash}.jpg");
|
||||
}
|
||||
|
||||
public DetailsResult GetDetails(User user)
|
||||
{
|
||||
var result = new DetailsResult();
|
||||
var saveChangesRequired = false;
|
||||
|
||||
if (!UserService.CurrentAuthorization.HasAll(Claims.User.Show, Claims.User.ShowDetails))
|
||||
return result;
|
||||
|
||||
var features = Plugins.GetPluginFeatures(typeof(DetailsProviderFeature));
|
||||
|
||||
if (features.Count == 0)
|
||||
return result;
|
||||
|
||||
var cache = user.UserDetails?.Where(d => d.Scope == DetailsScope).ToDictionary(d => d.Key, d => new { DbDetails = d, Details = JsonConvert.DeserializeObject<DetailsResult>(d.Value) }, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var feature in features)
|
||||
{
|
||||
var featureResult = default(DetailsResult);
|
||||
if (!cache.TryGetValue(feature.Id, out var cacheResult) || cacheResult.Details.ExpiresOn < DateTime.Now || cacheResult.Details.GatheredOn < database.DiscoConfiguration.PluginDetailsCacheExpiration)
|
||||
{
|
||||
var timestamp = cacheResult?.Details.GatheredOn;
|
||||
if (timestamp.HasValue && timestamp.Value < database.DiscoConfiguration.PluginDetailsCacheExpiration)
|
||||
timestamp = null;
|
||||
|
||||
try
|
||||
{
|
||||
var featureInstance = feature.CreateInstance<DetailsProviderFeature>();
|
||||
featureResult = featureInstance.GetDetails(database, user, timestamp);
|
||||
|
||||
if (featureResult != null)
|
||||
{
|
||||
if (featureResult.ExpiresOn > DateTime.Now)
|
||||
{
|
||||
if (cacheResult == null)
|
||||
database.UserDetails.Add(new UserDetail() { UserId = user.UserId, Scope = DetailsScope, Key = feature.Id, Value = JsonConvert.SerializeObject(featureResult) });
|
||||
else
|
||||
cacheResult.DbDetails.Value = JsonConvert.SerializeObject(featureResult);
|
||||
saveChangesRequired = true;
|
||||
}
|
||||
else if (cacheResult != null)
|
||||
{
|
||||
database.UserDetails.Remove(cacheResult.DbDetails);
|
||||
saveChangesRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignore exceptions when plugins behave badly
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
featureResult = cacheResult.Details;
|
||||
}
|
||||
|
||||
// apply feature results
|
||||
if (featureResult != null)
|
||||
{
|
||||
result.SetExpiration(featureResult.ExpiresOn);
|
||||
foreach (var value in featureResult.Details)
|
||||
{
|
||||
result.Details[value.Key] = value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (saveChangesRequired)
|
||||
database.SaveChanges();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public DetailsResult GetDetails(Device device)
|
||||
{
|
||||
var result = new DetailsResult();
|
||||
var saveChangesRequired = false;
|
||||
|
||||
if (!UserService.CurrentAuthorization.HasAll(Claims.Device.Show, Claims.Device.ShowDetails))
|
||||
return result;
|
||||
|
||||
var features = Plugins.GetPluginFeatures(typeof(DetailsProviderFeature));
|
||||
|
||||
if (features.Count == 0)
|
||||
return result;
|
||||
|
||||
var cache = device.DeviceDetails?.Where(d => d.Scope == DetailsScope).ToDictionary(d => d.Key, d => new { DbDetails = d, Details = JsonConvert.DeserializeObject<DetailsResult>(d.Value) }, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var feature in features)
|
||||
{
|
||||
var featureResult = default(DetailsResult);
|
||||
if (!cache.TryGetValue(feature.Id, out var cacheResult) || cacheResult.Details.ExpiresOn < DateTime.Now || cacheResult.Details.GatheredOn < database.DiscoConfiguration.PluginDetailsCacheExpiration)
|
||||
{
|
||||
var timestamp = cacheResult?.Details.GatheredOn;
|
||||
if (timestamp.HasValue && timestamp.Value < database.DiscoConfiguration.PluginDetailsCacheExpiration)
|
||||
timestamp = null;
|
||||
|
||||
try
|
||||
{
|
||||
var featureInstance = feature.CreateInstance<DetailsProviderFeature>();
|
||||
featureResult = featureInstance.GetDetails(database, device, timestamp);
|
||||
|
||||
if (featureResult != null)
|
||||
{
|
||||
if (featureResult.ExpiresOn > DateTime.Now)
|
||||
{
|
||||
if (cacheResult == null)
|
||||
database.DeviceDetails.Add(new DeviceDetail() { DeviceSerialNumber = device.SerialNumber, Scope = DetailsScope, Key = feature.Id, Value = JsonConvert.SerializeObject(featureResult) });
|
||||
else
|
||||
cacheResult.DbDetails.Value = JsonConvert.SerializeObject(featureResult);
|
||||
saveChangesRequired = true;
|
||||
}
|
||||
else if (cacheResult != null)
|
||||
{
|
||||
database.DeviceDetails.Remove(cacheResult.DbDetails);
|
||||
saveChangesRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignore exceptions when plugins behave badly
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
featureResult = cacheResult.Details;
|
||||
}
|
||||
|
||||
// apply feature results
|
||||
if (featureResult != null)
|
||||
{
|
||||
result.SetExpiration(featureResult.ExpiresOn);
|
||||
foreach (var value in featureResult.Details)
|
||||
{
|
||||
result.Details[value.Key] = value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (saveChangesRequired)
|
||||
database.SaveChanges();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user