Pārlūkot izejas kodu

Changed postable interface to use DataModels

Kenric Nugteren 1 gadu atpakaļ
vecāks
revīzija
0b8ff96d67

+ 1 - 1
InABox.Client.RPC/Transports/RPCClientTransport.cs

@@ -92,7 +92,7 @@ namespace InABox.Rpc
             var response = GetResponse(request.Id, ev, DefaultRequestTimeout)
                 ?? throw new Exception($"{typeof(TCommand).Name}({request.Id}) returned NULL");
 
-            if (response.Error != RpcError.NONE)throw new RpcException($"Server error in {typeof(TCommand).Name}({request.Id})", response.Error);
+            if (response.Error != RpcError.NONE) throw new RpcException($"Server error in {typeof(TCommand).Name}({request.Id}): {response.Error}", response.Error);
             
             var result = Serialization.ReadBinary<TResult>(response.Payload, BinarySerializationSettings.Latest)
                 ?? throw new Exception($"Cannot Deserialize {typeof(TCommand).Name}({request.Id})");

+ 21 - 4
InABox.Core/DataModel/DataModel.cs

@@ -83,6 +83,7 @@ namespace InABox.Core
         void AddTable(string alias, CoreTable table, bool isdefault = false);
 
         void AddTable(Type type, CoreTable table, bool isdefault = false, string? alias = null);
+
         /// <summary>
         /// Adds a table to the datamodel.
         /// </summary>
@@ -156,12 +157,22 @@ namespace InABox.Core
         bool HasTable(Type type, string? alias = null);
         bool HasTable<TType>(string? alias = null);
 
-        void LoadModel(IEnumerable<string> requiredTables, Dictionary<string, IQueryDef>? requiredQueries = null);
-        void LoadModel(IEnumerable<string> requiredTables, params IDataModelQueryDef[] requiredQueries);
+        void LoadModel(IEnumerable<string>? requiredTables, Dictionary<string, IQueryDef>? requiredQueries = null);
+        void LoadModel(IEnumerable<string>? requiredTables, params IDataModelQueryDef[] requiredQueries);
+
+        /// <summary>
+        /// Load the model, loading all tables that are set to be default. (See <see cref="SetIsDefault{TType}(bool, string?)"/>).
+        /// </summary>
+        void LoadModel();
 
         TType[] ExtractValues<TSource, TType>(Expression<Func<TSource, TType>> column, bool distinct = true, string? alias = null);
     }
 
+    public interface IDataModel<T> : IDataModel
+        where T : Entity, IRemotable, IPersistent, new()
+    {
+    }
+
     public abstract class DataModel : IDataModel
     {
         private readonly List<IDataModelRelationship> _relationships = new List<IDataModelRelationship>();
@@ -465,11 +476,16 @@ namespace InABox.Core
             if (!args.Cancel) AfterLoad(requiredTablesList);
         }
 
-        public void LoadModel(IEnumerable<string> requiredTables, params IDataModelQueryDef[] requiredQueries)
+        public void LoadModel(IEnumerable<string>? requiredTables, params IDataModelQueryDef[] requiredQueries)
         {
             LoadModel(requiredTables, requiredQueries.ToDictionary(x => x.TableName, x => x as IQueryDef));
         }
 
+        public void LoadModel()
+        {
+            LoadModel(DefaultTableNames);
+        }
+
         #endregion
 
         #region Non-Generic Stuff
@@ -731,7 +747,8 @@ namespace InABox.Core
         #endregion
     }
 
-    public abstract class DataModel<T> : DataModel where T : Entity, IRemotable, IPersistent, new()
+    public abstract class DataModel<T> : DataModel, IDataModel<T>
+        where T : Entity, IRemotable, IPersistent, new()
     {
         public DataModel(Filter<T> filter, Columns<T>? columns = null, SortOrder<T>? sort = null)
         {

+ 4 - 1
InABox.Core/Postable/IPoster.cs

@@ -13,8 +13,11 @@ namespace InABox.Core
     /// <typeparam name="TEntity">The type of entity that this poster can process.</typeparam>
     /// <typeparam name="TSettings">The <see cref="PosterSettings"/> specific to this type of poster.</typeparam>
     public interface IPoster<TEntity, TSettings>
-        where TEntity : Entity, IPostable
+        where TEntity : Entity, IPostable, IRemotable, IPersistent, new()
         where TSettings : PosterSettings
     {
+        bool BeforePost(IDataModel<TEntity> model);
+
+        void AfterPost(IDataModel<TEntity> model);
     }
 }

+ 5 - 0
InABox.Core/Postable/PostExceptions.cs

@@ -6,6 +6,11 @@ namespace InABox.Core
 {
     namespace Postable
     {
+        public class EmptyPostException : Exception
+        {
+            public EmptyPostException() { }
+        }
+
         public class MissingSettingsException : Exception
         {
             public Type PostableType { get; }

+ 54 - 12
InABox.Core/Postable/PosterEngine.cs

@@ -12,7 +12,7 @@ namespace InABox.Core
     public interface IPosterEngine<TPostable>
         where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
     {
-        bool Process(IEnumerable<TPostable> posts);
+        bool Process(IDataModel<TPostable> model);
     }
 
     public interface IPosterEngine<TPostable, TPoster, TSettings> : IPosterEngine<TPostable>
@@ -22,6 +22,13 @@ namespace InABox.Core
     {
     }
 
+    /// <summary>
+    /// A base class for all <see cref="IPosterEngine{TPostable}"/>. A concrete instance of this will be loaded by
+    /// <see cref="PosterUtils.Process{T}(IDataModel{T})"/>; a new instance is guaranteed to be created each time that method is called.
+    /// </summary>
+    /// <typeparam name="TPostable"></typeparam>
+    /// <typeparam name="TPoster"></typeparam>
+    /// <typeparam name="TSettings"></typeparam>
     public abstract class PosterEngine<TPostable, TPoster, TSettings> : IPosterEngine<TPostable, TPoster, TSettings>
         where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
         where TPoster : IPoster<TPostable, TSettings>
@@ -66,45 +73,80 @@ namespace InABox.Core
             return settings.ScriptEnabled ? settings.Script : null;
         }
 
-        protected abstract bool DoProcess(IEnumerable<TPostable> posts);
+        protected abstract bool DoProcess(IDataModel<TPostable> model);
 
-        public bool Process(IEnumerable<TPostable> posts)
+        /// <summary>
+        /// Process the <paramref name="model"/> before loading;
+        /// </summary>
+        /// <param name="model"></param>
+        /// <returns><see langword="false"/> if the processing must be cancelled.</returns>
+        public abstract bool BeforePost(IDataModel<TPostable> model);
+
+        /// <summary>
+        /// Prior to saving the <typeparamref name="TPostable"/> entities, make any necessary changes to those entities.
+        /// This is only called if <see cref="Process(IDataModel{TPostable})"/> returned <see langword="true"/>.
+        /// </summary>
+        /// <param name="model"></param>
+        public abstract void AfterPost(IDataModel<TPostable> model);
+
+        public bool Process(IDataModel<TPostable> model)
         {
-            var list = posts.AsList();
-            if(list.Any(x => x.PostedStatus == PostedStatus.Posted))
+            if (!BeforePost(model))
+            {
+                return false;
+            }
+            model.LoadModel();
+
+            var data = model.GetTable<TPostable>();
+
+            if (!data.Rows.Any())
+            {
+                throw new EmptyPostException();
+            }
+            if(data.Rows.Any(x => x.Get<TPostable, PostedStatus>(x => x.PostedStatus) == PostedStatus.Posted))
             {
                 throw new RepostedException();
             }
+
             try
             {
-                var success = DoProcess(list);
+                var success = DoProcess(model);
                 if (success)
                 {
-                    foreach (var post in list)
+                    AfterPost(model);
+                }
+
+                var entities = data.ToObjects<TPostable>().ToList();
+
+                if (success)
+                {
+                    foreach (var post in entities)
                     {
                         post.Posted = DateTime.Now;
                         post.PostedStatus = PostedStatus.Posted;
                     }
-                    new Client<TPostable>().Save(list, "Posted by user.");
+                    new Client<TPostable>().Save(entities, "Posted by user.");
                 }
                 else
                 {
-                    foreach (var post in list)
+                    foreach (var post in entities)
                     {
                         post.PostedStatus = PostedStatus.PostFailed;
                     }
-                    new Client<TPostable>().Save(list, "Post failed by user.");
+                    new Client<TPostable>().Save(entities, "Post failed by user.");
                 }
                 return success;
             }
             catch(Exception e)
             {
                 Logger.Send(LogType.Error, "", $"Post Failed: {CoreUtils.FormatException(e)}");
-                foreach (var post in list)
+
+                var entities = data.ToObjects<TPostable>().ToList();
+                foreach (var post in entities)
                 {
                     post.PostedStatus = PostedStatus.PostFailed;
                 }
-                new Client<TPostable>().Save(list, "Post failed by user.");
+                new Client<TPostable>().Save(entities, "Post failed by user.");
                 throw;
             }
         }

+ 7 - 6
InABox.Core/Postable/PosterUtils.cs

@@ -140,20 +140,21 @@ namespace InABox.Core
         }
 
         /// <summary>
-        /// Process <paramref name="entities"/> with the currently set <see cref="IPosterEngine{TPostable, TPoster, TSettings}"/>
+        /// Process <paramref name="model"/> with the currently set <see cref="IPosterEngine{TPostable, TPoster, TSettings}"/>
         /// for <typeparamref name="T"/>.
         /// </summary>
-        /// <typeparam name="T">The type of <paramref name="entities"/> that needs to be processed.</typeparam>
-        /// <param name="entities"></param>
-        /// <exception cref="RepostedException">If any of the <paramref name="entities"/> has already been processed. In this case, nothing happens.</exception>
+        /// <typeparam name="T">The type of <paramref name="model"/> that needs to be processed.</typeparam>
+        /// <param name="model"></param>
+        /// <exception cref="EmptyPostException">If there are no items to post. In this case, nothing happens.</exception>
+        /// <exception cref="RepostedException">If any of the <typeparamref name="T"/> have already been processed. In this case, nothing happens.</exception>
         /// <exception cref="MissingSettingsException">If the <see cref="PostableSettings"/> for <typeparamref name="T"/> do not exist.</exception>
         /// <exception cref="PostCancelledException">If the post has been cancelled by the user.</exception>
         /// <returns><see langword="true"/> if post was successful.</returns>
         /// 
-        public static bool Process<T>(IEnumerable<T> entities)
+        public static bool Process<T>(IDataModel<T> model)
             where T : Entity, IPostable, IRemotable, IPersistent, new()
         {
-            return CreateEngine<T>().Process(entities);
+            return CreateEngine<T>().Process(model);
         }
     }
 }

+ 55 - 7
InABox.Poster.CSV/CSVPosterEngine.cs

@@ -16,12 +16,18 @@ namespace InABox.Poster.CSV
     public class CSVPosterEngine<TPostable> : PosterEngine<TPostable, ICSVPoster<TPostable>, CSVPosterSettings>
         where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
     {
-        protected override bool DoProcess(IEnumerable<TPostable> posts)
+        private ScriptDocument? _script;
+        private bool _hasCheckedScript;
+
+        private ScriptDocument? GetScriptDocument()
         {
-            var settings = GetSettings();
+            if (_hasCheckedScript)
+            {
+                return _script;
+            }
 
-            ICSVExport results;
-            if(settings.ScriptEnabled && !string.IsNullOrWhiteSpace(settings.Script))
+            var settings = GetSettings();
+            if (settings.ScriptEnabled && !string.IsNullOrWhiteSpace(settings.Script))
             {
                 var document = new ScriptDocument(settings.Script);
                 document.Properties.Add(new ScriptProperty("Results", null));
@@ -29,18 +35,48 @@ namespace InABox.Poster.CSV
                 {
                     throw new Exception("Script failed to compile!");
                 }
-                if(!document.Execute(methodname: "Process", parameters: new object[] { posts }))
+                _script = document;
+            }
+            else
+            {
+                _script = null;
+            }
+
+            _hasCheckedScript = true;
+            return _script;
+        }
+
+        public override bool BeforePost(IDataModel<TPostable> model)
+        {
+            if(GetScriptDocument() is ScriptDocument script)
+            {
+                return script.Execute(methodname: "BeforePost", parameters: new object[] { model });
+            }
+            else
+            {
+                return Poster.BeforePost(model);
+            }
+        }
+
+        protected override bool DoProcess(IDataModel<TPostable> model)
+        {
+            var settings = GetSettings();
+
+            ICSVExport results;
+            if (GetScriptDocument() is ScriptDocument script)
+            {
+                if (!script.Execute(methodname: "Process", parameters: new object[] { model }))
                 {
                     return false;
                 }
 
-                var resultsObject = document.GetValue("Results");
+                var resultsObject = script.GetValue("Results");
                 results = (resultsObject as ICSVExport)
                     ?? throw new Exception($"Script 'Results' property expected to be ICSVExport, got {resultsObject}");
             }
             else
             {
-                results = Poster.Process(posts);
+                results = Poster.Process(model);
             }
 
             var dlg = new SaveFileDialog()
@@ -66,5 +102,17 @@ namespace InABox.Poster.CSV
                 throw new PostCancelledException();
             }
         }
+
+        public override void AfterPost(IDataModel<TPostable> model)
+        {
+            if (GetScriptDocument() is ScriptDocument script)
+            {
+                script.Execute(methodname: "AfterPost", parameters: new object[] { model });
+            }
+            else
+            {
+                Poster.AfterPost(model);
+            }
+        }
     }
 }

+ 20 - 6
InABox.Poster.CSV/CSVPosterSettings.cs

@@ -23,6 +23,7 @@ namespace InABox.Poster.CSV
             return @"
 using " + ns + @";
 using InABox.Poster.CSV;
+using InABox.Core;
 using System.Collections.Generic;
 
 public class Module
@@ -30,30 +31,43 @@ public class Module
     // Output Results for CSV
     public ICSVExport Results { get; set; }
 
-    public bool Process(IEnumerable<" + tName + @"> items)
+    // Customise 'model' before loading data. Return false if the post needs to be cancelled.
+    public bool BeforePost(IDataModel<" + tName + @"> model)
+    {
+        return true;
+    }
+
+    public bool Process(IDataModel<" + tName + @"> model)
     {
         // Create new export object. You can use any object as your map,
-        // but for simple cases just use " + tName +  @"
+        // but for simple cases just use " + tName + @".
         var export = new CSVExport<" + tName + @">();
 
         // Define the mapping from the fields of " + tName + @" to columns in the CSV file.
         export.DefineMapping(new()
         {
-            new(""ID"", " + decapital + @" => " + decapital +  @".ID),
+            new(""ID"", " + decapital + @" => " + decapital + @".ID),
             new(""Column1"", " + decapital + @" => " + decapital +  @".Column1),
             new(""Column2"", " + decapital + @" => " + decapital +  @".Column2),
-            new(""Column3"", " + decapital + @" => " + decapital + @".Column3)
+            new(""Column3"", " + decapital + @" => " + decapital + @".Column3),
             // etc.
         });
-        foreach(var item in items)
+        foreach(var " + decapital + @" in model.GetTable<" + tName + @">().ToObjects<" + tName + @">())
         {
             // Do processing
-            export.Add(item);
+            
+            // Add item to export
+            export.Add(" + decapital + @");
         }
 
         Results = export; // Set result
         return true; // return true for success.
     }
+
+    // Perform any post-processing. All the items of type '" + tName + @" will be saved after this function is called.
+    public void AfterPost(IDataModel<" + tName + @"> model)
+    {
+    }
 }";
         }
     }

+ 2 - 2
InABox.Poster.CSV/ICSVPoster.cs

@@ -89,8 +89,8 @@ namespace InABox.Poster.CSV
     /// <typeparam name="TEntity"></typeparam>
     [Caption("CSV")]
     public interface ICSVPoster<TEntity> : IPoster<TEntity, CSVPosterSettings>
-        where TEntity : Entity, IPostable
+        where TEntity : Entity, IPostable, IRemotable, IPersistent, new()
     {
-        ICSVExport Process(IEnumerable<TEntity> entities);
+        ICSVExport Process(IDataModel<TEntity> model);
     }
 }

+ 0 - 3
inabox.wpf/DynamicGrid/DynamicDataGrid.cs

@@ -271,9 +271,6 @@ namespace InABox.DynamicGrid
                 if (!result.Items.Any(x => string.Equals(x.Property, col.Property)))
                     result.Add(col.Property);
 
-            var props = DatabaseSchema.Properties(typeof(TEntity))
-                .Where(x => x.Setter() != null)
-                .OrderBy(x => CoreUtils.GetPropertySequence(typeof(TEntity), x.Name));
             foreach (var col in result.Items)
             {
                 var prop = DatabaseSchema.Property(typeof(TEntity), col.Property);