Unified · Split
| @@ -58,6 +58,7 @@ · 57 unmodified lines | ||
| 58 | 58 | using System; |
| 59 | 59 | using System.Collections; |
| 60 | 60 | using System.Collections.Generic; |
| 61 | +using System.Collections.Specialized; | |
| 61 | 62 | using System.Linq; |
| 62 | 63 | using System.Text; |
| 63 | 64 | using Random = GSF.Security.Cryptography.Random; |
| @@ -292,6 +293,16 @@ namespace GSF.Collections · 228 unmodified lines | ||
| 292 | 293 | return value; |
| 293 | 294 | } |
| 294 | 295 | |
| 296 | + /// <summary> | |
| 297 | + /// Converts a name/value collection to a dictionary. | |
| 298 | + /// </summary> | |
| 299 | + /// <param name="collection">Name/value collection.</param> | |
| 300 | + /// <returns>Dictionary converted from a name/value collection.</returns> | |
| 301 | + public static Dictionary<string, string> ToDictionary(this NameValueCollection collection) | |
| 302 | + { | |
| 303 | + return collection.AllKeys.ToDictionary(key => key, key => collection[key]); | |
| 304 | + } | |
| 305 | + | |
| 295 | 306 | /// <summary> |
| 296 | 307 | /// Returns <c>true</c> if any item in <see cref="BitArray"/> is equal to <paramref name="value"/>. |
| 297 | 308 | /// </summary> |
| @@ -122,6 +122,7 @@ · 121 unmodified lines | ||
| 122 | 122 | </PropertyGroup> |
| 123 | 123 | <ItemGroup> |
| 124 | 124 | <Reference Include="System" /> |
| 125 | + <Reference Include="System.ComponentModel.DataAnnotations" /> | |
| 125 | 126 | <Reference Include="System.configuration" /> |
| 126 | 127 | <Reference Include="System.Core"> |
| 127 | 128 | <RequiredTargetFramework>3.5</RequiredTargetFramework> |
| @@ -144,6 +145,15 @@ · 16 unmodified lines | ||
| 144 | 145 | <ItemGroup> |
| 145 | 146 | <Compile Include="AdoSecurityProvider.cs" /> |
| 146 | 147 | <Compile Include="AdoSecurityCache.cs" /> |
| 148 | + <Compile Include="Model\AccessLog.cs" /> | |
| 149 | + <Compile Include="Model\ErrorLog.cs" /> | |
| 150 | + <Compile Include="Model\ApplicationRole.cs" /> | |
| 151 | + <Compile Include="Model\ApplicationRoleSecurityGroup.cs" /> | |
| 152 | + <Compile Include="Model\SecurityGroupUserAccount.cs" /> | |
| 153 | + <Compile Include="Model\ApplicationRoleUserAccount.cs" /> | |
| 154 | + <Compile Include="Model\Node.cs" /> | |
| 155 | + <Compile Include="Model\SecurityGroup.cs" /> | |
| 156 | + <Compile Include="Model\UserAccount.cs" /> | |
| 147 | 157 | <Compile Include="UserRoleCache.cs" /> |
| 148 | 158 | <Compile Include="LdapSecurityProvider.cs" /> |
| 149 | 159 | <Compile Include="RestrictAccessAttribute.cs" /> |
| @@ -0,0 +1,58 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// AccessLog.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright � 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/27/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.ComponentModel.DataAnnotations; | |
| 26 | +using GSF.Data.Model; | |
| 27 | + | |
| 28 | +namespace GSF.Security.Model | |
| 29 | +{ | |
| 30 | + /// <summary> | |
| 31 | + /// Model for AccessLog table. | |
| 32 | + /// </summary> | |
| 33 | + public class AccessLog | |
| 34 | + { | |
| 35 | + /// <summary> | |
| 36 | + /// Unique ID field. | |
| 37 | + /// </summary> | |
| 38 | + [PrimaryKey(true)] | |
| 39 | + public int ID { get; set; } | |
| 40 | + | |
| 41 | + /// <summary> | |
| 42 | + /// User name field. | |
| 43 | + /// </summary> | |
| 44 | + [Required] | |
| 45 | + [StringLength(200)] | |
| 46 | + public string UserName { get; set; } | |
| 47 | + | |
| 48 | + /// <summary> | |
| 49 | + /// Access granted flag field. | |
| 50 | + /// </summary> | |
| 51 | + public bool AccessGranted { get; set; } | |
| 52 | + | |
| 53 | + /// <summary> | |
| 54 | + /// Created on field. | |
| 55 | + /// </summary> | |
| 56 | + public DateTime CreatedOn { get; set; } | |
| 57 | + } | |
| 58 | +} | |
| @@ -0,0 +1,79 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// ApplicationRole.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright � 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/27/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.ComponentModel.DataAnnotations; | |
| 26 | +using GSF.Data.Model; | |
| 27 | + | |
| 28 | +namespace GSF.Security.Model | |
| 29 | +{ | |
| 30 | + /// <summary> | |
| 31 | + /// Model for ApplicationRole table. | |
| 32 | + /// </summary> | |
| 33 | + [PrimaryLabel("Name")] | |
| 34 | + public class ApplicationRole | |
| 35 | + { | |
| 36 | + /// <summary> | |
| 37 | + /// Unique application role ID field. | |
| 38 | + /// </summary> | |
| 39 | + [PrimaryKey(true)] | |
| 40 | + public Guid ID { get; set; } | |
| 41 | + | |
| 42 | + /// <summary> | |
| 43 | + /// Name field. | |
| 44 | + /// </summary> | |
| 45 | + [Required] | |
| 46 | + [StringLength(200)] | |
| 47 | + public string Name { get; set; } | |
| 48 | + | |
| 49 | + /// <summary> | |
| 50 | + /// Description field. | |
| 51 | + /// </summary> | |
| 52 | + public string Description { get; set; } | |
| 53 | + | |
| 54 | + /// <summary> | |
| 55 | + /// Node ID field. | |
| 56 | + /// </summary> | |
| 57 | + public Guid NodeID { get; set; } | |
| 58 | + | |
| 59 | + /// <summary> | |
| 60 | + /// Created on field. | |
| 61 | + /// </summary> | |
| 62 | + public DateTime CreatedOn { get; set; } | |
| 63 | + | |
| 64 | + /// <summary> | |
| 65 | + /// Created by field. | |
| 66 | + /// </summary> | |
| 67 | + public string CreatedBy { get; set; } | |
| 68 | + | |
| 69 | + /// <summary> | |
| 70 | + /// Updated on field. | |
| 71 | + /// </summary> | |
| 72 | + public DateTime UpdatedOn { get; set; } | |
| 73 | + | |
| 74 | + /// <summary> | |
| 75 | + /// Updated by field. | |
| 76 | + /// </summary> | |
| 77 | + public string UpdatedBy { get; set; } | |
| 78 | + } | |
| 79 | +} | |
| @@ -0,0 +1,52 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// ApplicationRoleSecurityGroup.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright � 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 03/03/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.ComponentModel.DataAnnotations; | |
| 26 | + | |
| 27 | +namespace GSF.Security.Model | |
| 28 | +{ | |
| 29 | + /// <summary> | |
| 30 | + /// Model for ApplicationRoleSecurityGroup table. | |
| 31 | + /// </summary> | |
| 32 | + public class ApplicationRoleSecurityGroup | |
| 33 | + { | |
| 34 | + /// <summary> | |
| 35 | + /// Application role ID field. | |
| 36 | + /// </summary> | |
| 37 | + [Required] | |
| 38 | + public Guid ApplicationRoleID | |
| 39 | + { | |
| 40 | + get; set; | |
| 41 | + } | |
| 42 | + | |
| 43 | + /// <summary> | |
| 44 | + /// Security group ID field. | |
| 45 | + /// </summary> | |
| 46 | + [Required] | |
| 47 | + public Guid SecurityGroupID | |
| 48 | + { | |
| 49 | + get; set; | |
| 50 | + } | |
| 51 | + } | |
| 52 | +} | |
| @@ -0,0 +1,52 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// ApplicationRoleUserAccount.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright � 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/27/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.ComponentModel.DataAnnotations; | |
| 26 | + | |
| 27 | +namespace GSF.Security.Model | |
| 28 | +{ | |
| 29 | + /// <summary> | |
| 30 | + /// Model for ApplicationRoleUserAccount table. | |
| 31 | + /// </summary> | |
| 32 | + public class ApplicationRoleUserAccount | |
| 33 | + { | |
| 34 | + /// <summary> | |
| 35 | + /// Application role ID field. | |
| 36 | + /// </summary> | |
| 37 | + [Required] | |
| 38 | + public Guid ApplicationRoleID | |
| 39 | + { | |
| 40 | + get; set; | |
| 41 | + } | |
| 42 | + | |
| 43 | + /// <summary> | |
| 44 | + /// User account ID field. | |
| 45 | + /// </summary> | |
| 46 | + [Required] | |
| 47 | + public Guid UserAccountID | |
| 48 | + { | |
| 49 | + get; set; | |
| 50 | + } | |
| 51 | + } | |
| 52 | +} | |
| @@ -0,0 +1,87 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// ErrorLog.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright � 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/27/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.ComponentModel.DataAnnotations; | |
| 26 | +using GSF.Data.Model; | |
| 27 | + | |
| 28 | +namespace GSF.Security.Model | |
| 29 | +{ | |
| 30 | + /// <summary> | |
| 31 | + /// Model for ErrorLog table. | |
| 32 | + /// </summary> | |
| 33 | + public class ErrorLog | |
| 34 | + { | |
| 35 | + /// <summary> | |
| 36 | + /// Unique ID field. | |
| 37 | + /// </summary> | |
| 38 | + [PrimaryKey(true)] | |
| 39 | + public int ID | |
| 40 | + { | |
| 41 | + get; set; | |
| 42 | + } | |
| 43 | + | |
| 44 | + /// <summary> | |
| 45 | + /// Source field. | |
| 46 | + /// </summary> | |
| 47 | + [Required] | |
| 48 | + [StringLength(200)] | |
| 49 | + public string Source | |
| 50 | + { | |
| 51 | + get; set; | |
| 52 | + } | |
| 53 | + | |
| 54 | + /// <summary> | |
| 55 | + /// Type field. | |
| 56 | + /// </summary> | |
| 57 | + public string Type | |
| 58 | + { | |
| 59 | + get; set; | |
| 60 | + } | |
| 61 | + | |
| 62 | + /// <summary> | |
| 63 | + /// Message field. | |
| 64 | + /// </summary> | |
| 65 | + [Required] | |
| 66 | + public string Message | |
| 67 | + { | |
| 68 | + get; set; | |
| 69 | + } | |
| 70 | + | |
| 71 | + /// <summary> | |
| 72 | + /// Detail field. | |
| 73 | + /// </summary> | |
| 74 | + public string Detail | |
| 75 | + { | |
| 76 | + get; set; | |
| 77 | + } | |
| 78 | + | |
| 79 | + /// <summary> | |
| 80 | + /// Created on field. | |
| 81 | + /// </summary> | |
| 82 | + public DateTime CreatedOn | |
| 83 | + { | |
| 84 | + get; set; | |
| 85 | + } | |
| 86 | + } | |
| 87 | +} | |
| @@ -0,0 +1,80 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// Node.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright � 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/27/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.ComponentModel.DataAnnotations; | |
| 26 | +using GSF.Data.Model; | |
| 27 | + | |
| 28 | +namespace GSF.Security.Model | |
| 29 | +{ | |
| 30 | + /// <summary> | |
| 31 | + /// Model for Node table. | |
| 32 | + /// </summary> | |
| 33 | + [PrimaryLabel("Name")] | |
| 34 | + public class Node | |
| 35 | + { | |
| 36 | + /// <summary> | |
| 37 | + /// Unique node ID field. | |
| 38 | + /// </summary> | |
| 39 | + [PrimaryKey(true)] | |
| 40 | + public Guid ID { get; set; } | |
| 41 | + | |
| 42 | + /// <summary> | |
| 43 | + /// Name field. | |
| 44 | + /// </summary> | |
| 45 | + [Required] | |
| 46 | + [StringLength(200)] | |
| 47 | + public string Name { get; set; } | |
| 48 | + | |
| 49 | + /// <summary> | |
| 50 | + /// Description field. | |
| 51 | + /// </summary> | |
| 52 | + public string Description { get; set; } | |
| 53 | + | |
| 54 | + /// <summary> | |
| 55 | + /// Enabled field. | |
| 56 | + /// </summary> | |
| 57 | + [InitialValue("true")] | |
| 58 | + public bool Enabled { get; set; } | |
| 59 | + | |
| 60 | + /// <summary> | |
| 61 | + /// Created on field. | |
| 62 | + /// </summary> | |
| 63 | + public DateTime CreatedOn { get; set; } | |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// Created by field. | |
| 67 | + /// </summary> | |
| 68 | + public string CreatedBy { get; set; } | |
| 69 | + | |
| 70 | + /// <summary> | |
| 71 | + /// Updated on field. | |
| 72 | + /// </summary> | |
| 73 | + public DateTime UpdatedOn { get; set; } | |
| 74 | + | |
| 75 | + /// <summary> | |
| 76 | + /// Updated by field. | |
| 77 | + /// </summary> | |
| 78 | + public string UpdatedBy { get; set; } | |
| 79 | + } | |
| 80 | +} | |
| @@ -0,0 +1,78 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// SecurityGroup.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright � 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/27/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.ComponentModel.DataAnnotations; | |
| 26 | +using GSF.Data.Model; | |
| 27 | + | |
| 28 | +namespace GSF.Security.Model | |
| 29 | +{ | |
| 30 | + /// <summary> | |
| 31 | + /// Model for SecurityGroup table. | |
| 32 | + /// </summary> | |
| 33 | + /// <remarks> | |
| 34 | + /// A record in this table can represent a database defined group with associated users | |
| 35 | + /// or an Active Directory group that maintains its own users. | |
| 36 | + /// </remarks> | |
| 37 | + [PrimaryLabel("Name")] | |
| 38 | + public class SecurityGroup | |
| 39 | + { | |
| 40 | + /// <summary> | |
| 41 | + /// Unique security group ID field. | |
| 42 | + /// </summary> | |
| 43 | + [PrimaryKey(true)] | |
| 44 | + public Guid ID { get; set; } | |
| 45 | + | |
| 46 | + /// <summary> | |
| 47 | + /// Group name field - stores SID for AD authentication. | |
| 48 | + /// </summary> | |
| 49 | + [Required] | |
| 50 | + [StringLength(200)] | |
| 51 | + public string Name { get; set; } | |
| 52 | + | |
| 53 | + /// <summary> | |
| 54 | + /// Description field. | |
| 55 | + /// </summary> | |
| 56 | + public string Description { get; set; } | |
| 57 | + | |
| 58 | + /// <summary> | |
| 59 | + /// Created on field. | |
| 60 | + /// </summary> | |
| 61 | + public DateTime CreatedOn { get; set; } | |
| 62 | + | |
| 63 | + /// <summary> | |
| 64 | + /// Created by field. | |
| 65 | + /// </summary> | |
| 66 | + public string CreatedBy { get; set; } | |
| 67 | + | |
| 68 | + /// <summary> | |
| 69 | + /// Updated on field. | |
| 70 | + /// </summary> | |
| 71 | + public DateTime UpdatedOn { get; set; } | |
| 72 | + | |
| 73 | + /// <summary> | |
| 74 | + /// Updated by field. | |
| 75 | + /// </summary> | |
| 76 | + public string UpdatedBy { get; set; } | |
| 77 | + } | |
| 78 | +} | |
| @@ -0,0 +1,52 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// SecurityGroupUserAccount.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright � 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 03/03/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.ComponentModel.DataAnnotations; | |
| 26 | + | |
| 27 | +namespace GSF.Security.Model | |
| 28 | +{ | |
| 29 | + /// <summary> | |
| 30 | + /// Model for SecurityGroupUserAccount table. | |
| 31 | + /// </summary> | |
| 32 | + public class SecurityGroupUserAccount | |
| 33 | + { | |
| 34 | + /// <summary> | |
| 35 | + /// Security group ID field. | |
| 36 | + /// </summary> | |
| 37 | + [Required] | |
| 38 | + public Guid SecurityGroupID | |
| 39 | + { | |
| 40 | + get; set; | |
| 41 | + } | |
| 42 | + | |
| 43 | + /// <summary> | |
| 44 | + /// User account ID field. | |
| 45 | + /// </summary> | |
| 46 | + [Required] | |
| 47 | + public Guid UserAccountID | |
| 48 | + { | |
| 49 | + get; set; | |
| 50 | + } | |
| 51 | + } | |
| 52 | +} | |
| @@ -0,0 +1,139 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// UserAccount.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright � 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/27/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.ComponentModel.DataAnnotations; | |
| 26 | +using GSF.Data.Model; | |
| 27 | + | |
| 28 | +namespace GSF.Security.Model | |
| 29 | +{ | |
| 30 | + /// <summary> | |
| 31 | + /// Model for UserAccount table. | |
| 32 | + /// </summary> | |
| 33 | + /// <remarks> | |
| 34 | + /// A record in this table can represent a database defined user with associated detail | |
| 35 | + /// or an Active Directory user that maintains its own user details. | |
| 36 | + /// </remarks> | |
| 37 | + [PrimaryLabel("Name")] | |
| 38 | + public class UserAccount | |
| 39 | + { | |
| 40 | + /// <summary> | |
| 41 | + /// Unique user account ID field. | |
| 42 | + /// </summary> | |
| 43 | + [PrimaryKey(true)] | |
| 44 | + public Guid ID { get; set; } | |
| 45 | + | |
| 46 | + /// <summary> | |
| 47 | + /// User name field - stores SID for AD authentication. | |
| 48 | + /// </summary> | |
| 49 | + [Label("User Name")] | |
| 50 | + [Required] | |
| 51 | + [StringLength(200)] | |
| 52 | + public string Name { get; set; } | |
| 53 | + | |
| 54 | + /// <summary> | |
| 55 | + /// Password field - stores password hash for DB authentication only. | |
| 56 | + /// </summary> | |
| 57 | + [Required] | |
| 58 | + [StringLength(200)] | |
| 59 | + public string Password { get; set; } | |
| 60 | + | |
| 61 | + /// <summary> | |
| 62 | + /// First name field - for DB authentication only. | |
| 63 | + /// </summary> | |
| 64 | + [Label("First Name")] | |
| 65 | + [StringLength(200)] | |
| 66 | + public string FirstName { get; set; } | |
| 67 | + | |
| 68 | + /// <summary> | |
| 69 | + /// Last name field - for DB authentication only. | |
| 70 | + /// </summary> | |
| 71 | + [Label("Last Name")] | |
| 72 | + [StringLength(200)] | |
| 73 | + public string LastName { get; set; } | |
| 74 | + | |
| 75 | + /// <summary> | |
| 76 | + /// Default Node ID field. | |
| 77 | + /// </summary> | |
| 78 | + [Required] | |
| 79 | + public Guid DefaultNodeID { get; set; } | |
| 80 | + | |
| 81 | + /// <summary> | |
| 82 | + /// Phone number field - for DB authentication only. | |
| 83 | + /// </summary> | |
| 84 | + [StringLength(200)] | |
| 85 | + public string Phone { get; set; } | |
| 86 | + | |
| 87 | + /// <summary> | |
| 88 | + /// E-mail field - for DB authentication only. | |
| 89 | + /// </summary> | |
| 90 | + [Label("E-Mail")] | |
| 91 | + [StringLength(200)] | |
| 92 | + [RegularExpression(@"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$", ErrorMessage = "Invalid e-mail address.")] | |
| 93 | + public string Email { get; set; } | |
| 94 | + | |
| 95 | + /// <summary> | |
| 96 | + /// Change password on date field - for DB authentication only. | |
| 97 | + /// </summary> | |
| 98 | + public DateTime? ChangePasswordOn | |
| 99 | + { | |
| 100 | + get; set; | |
| 101 | + } | |
| 102 | + | |
| 103 | + /// <summary> | |
| 104 | + /// Use Active Directory authentication field. | |
| 105 | + /// </summary> | |
| 106 | + [Label("Use Active Directory Authentication")] | |
| 107 | + [InitialValue("true")] | |
| 108 | + public bool UseADAuthentication { get; set; } | |
| 109 | + | |
| 110 | + /// <summary> | |
| 111 | + /// User locked-out field. | |
| 112 | + /// </summary> | |
| 113 | + [Label("Locked Out")] | |
| 114 | + public bool LockedOut | |
| 115 | + { | |
| 116 | + get; set; | |
| 117 | + } | |
| 118 | + | |
| 119 | + /// <summary> | |
| 120 | + /// Created on field. | |
| 121 | + /// </summary> | |
| 122 | + public DateTime CreatedOn { get; set; } | |
| 123 | + | |
| 124 | + /// <summary> | |
| 125 | + /// Created by field. | |
| 126 | + /// </summary> | |
| 127 | + public string CreatedBy { get; set; } | |
| 128 | + | |
| 129 | + /// <summary> | |
| 130 | + /// Updated on field. | |
| 131 | + /// </summary> | |
| 132 | + public DateTime UpdatedOn { get; set; } | |
| 133 | + | |
| 134 | + /// <summary> | |
| 135 | + /// Updated by field. | |
| 136 | + /// </summary> | |
| 137 | + public string UpdatedBy { get; set; } | |
| 138 | + } | |
| 139 | +} | |
| @@ -115,15 +115,96 @@ · 114 unmodified lines | ||
| 115 | 115 | <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> |
| 116 | 116 | </PropertyGroup> |
| 117 | 117 | <ItemGroup> |
| 118 | + <Reference Include="Microsoft.AspNet.SignalR.Core, Version=2.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 119 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.SignalR.Core.2.2.0\lib\net45\Microsoft.AspNet.SignalR.Core.dll</HintPath> | |
| 120 | + <Private>True</Private> | |
| 121 | + </Reference> | |
| 122 | + <Reference Include="Microsoft.AspNet.SignalR.SystemWeb, Version=2.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 123 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.SignalR.SystemWeb.2.2.0\lib\net45\Microsoft.AspNet.SignalR.SystemWeb.dll</HintPath> | |
| 124 | + <Private>True</Private> | |
| 125 | + </Reference> | |
| 126 | + <Reference Include="Microsoft.CSharp" /> | |
| 127 | + <Reference Include="Microsoft.Owin, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 128 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.Owin.2.1.0\lib\net45\Microsoft.Owin.dll</HintPath> | |
| 129 | + <Private>True</Private> | |
| 130 | + </Reference> | |
| 131 | + <Reference Include="Microsoft.Owin.Host.SystemWeb, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 132 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.Owin.Host.SystemWeb.2.1.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll</HintPath> | |
| 133 | + <Private>True</Private> | |
| 134 | + </Reference> | |
| 135 | + <Reference Include="Microsoft.Owin.Security, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 136 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll</HintPath> | |
| 137 | + <Private>True</Private> | |
| 138 | + </Reference> | |
| 139 | + <Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 140 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath> | |
| 141 | + <Private>True</Private> | |
| 142 | + </Reference> | |
| 143 | + <Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> | |
| 144 | + <HintPath>..\..\Dependencies\NuGet\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll</HintPath> | |
| 145 | + <Private>True</Private> | |
| 146 | + </Reference> | |
| 147 | + <Reference Include="Owin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f0ebd12fd5e55cc5, processorArchitecture=MSIL"> | |
| 148 | + <HintPath>..\..\Dependencies\NuGet\Owin.1.0\lib\net40\Owin.dll</HintPath> | |
| 149 | + <Private>True</Private> | |
| 150 | + </Reference> | |
| 151 | + <Reference Include="RazorEngine, Version=3.7.7.0, Culture=neutral, PublicKeyToken=9ee697374c7e744a, processorArchitecture=MSIL"> | |
| 152 | + <HintPath>..\..\Dependencies\NuGet\RazorEngine.3.7.7\lib\net45\RazorEngine.dll</HintPath> | |
| 153 | + <Private>True</Private> | |
| 154 | + </Reference> | |
| 118 | 155 | <Reference Include="System" /> |
| 156 | + <Reference Include="System.ComponentModel.DataAnnotations" /> | |
| 119 | 157 | <Reference Include="System.configuration" /> |
| 120 | 158 | <Reference Include="System.Core"> |
| 121 | 159 | <RequiredTargetFramework>3.5</RequiredTargetFramework> |
| 122 | 160 | </Reference> |
| 161 | + <Reference Include="System.IO" /> | |
| 162 | + <Reference Include="System.Net" /> | |
| 163 | + <Reference Include="System.Net.Http"> | |
| 164 | + <HintPath>..\..\Dependencies\NuGet\System.Net.Http.2.0.20126.16343\lib\net40\System.Net.Http.dll</HintPath> | |
| 165 | + <Private>True</Private> | |
| 166 | + </Reference> | |
| 167 | + <Reference Include="System.Net.Http.Formatting, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 168 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll</HintPath> | |
| 169 | + <Private>True</Private> | |
| 170 | + </Reference> | |
| 171 | + <Reference Include="System.Net.Http.WebRequest"> | |
| 172 | + <HintPath>..\..\Dependencies\NuGet\System.Net.Http.2.0.20126.16343\lib\net40\System.Net.Http.WebRequest.dll</HintPath> | |
| 173 | + <Private>True</Private> | |
| 174 | + </Reference> | |
| 175 | + <Reference Include="System.Security" /> | |
| 123 | 176 | <Reference Include="System.ServiceModel" /> |
| 124 | 177 | <Reference Include="System.ServiceModel.Activation" /> |
| 125 | 178 | <Reference Include="System.ServiceModel.Web" /> |
| 126 | 179 | <Reference Include="System.Web" /> |
| 180 | + <Reference Include="System.Web.Helpers, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 181 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll</HintPath> | |
| 182 | + <Private>True</Private> | |
| 183 | + </Reference> | |
| 184 | + <Reference Include="System.Web.Http, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 185 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll</HintPath> | |
| 186 | + <Private>True</Private> | |
| 187 | + </Reference> | |
| 188 | + <Reference Include="System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 189 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll</HintPath> | |
| 190 | + <Private>True</Private> | |
| 191 | + </Reference> | |
| 192 | + <Reference Include="System.Web.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 193 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll</HintPath> | |
| 194 | + <Private>True</Private> | |
| 195 | + </Reference> | |
| 196 | + <Reference Include="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 197 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll</HintPath> | |
| 198 | + <Private>True</Private> | |
| 199 | + </Reference> | |
| 200 | + <Reference Include="System.Web.WebPages.Deployment, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 201 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll</HintPath> | |
| 202 | + <Private>True</Private> | |
| 203 | + </Reference> | |
| 204 | + <Reference Include="System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> | |
| 205 | + <HintPath>..\..\Dependencies\NuGet\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll</HintPath> | |
| 206 | + <Private>True</Private> | |
| 207 | + </Reference> | |
| 127 | 208 | <Reference Include="System.Xml.Linq"> |
| 128 | 209 | <RequiredTargetFramework>3.5</RequiredTargetFramework> |
| 129 | 210 | </Reference> |
| @@ -142,6 +223,8 @@ · 12 unmodified lines | ||
| 142 | 223 | <Compile Include="Hosting\VirtualFileBaseCollection.cs" /> |
| 143 | 224 | <Compile Include="Embedded\ISecurityService.cs" /> |
| 144 | 225 | <Compile Include="IgnoreServicesSecurityModule.cs" /> |
| 226 | + <Compile Include="Model\DataContext.cs" /> | |
| 227 | + <Compile Include="Model\RecordOperationsCache.cs" /> | |
| 145 | 228 | <Compile Include="NamespaceDoc.cs" /> |
| 146 | 229 | <Compile Include="Properties\AssemblyInfo.cs" /> |
| 147 | 230 | <Compile Include="SecurityModule.cs" /> |
| @@ -156,7 +239,14 @@ · 8 unmodified lines | ||
| 156 | 239 | <DependentUpon>SecurityService.svc</DependentUpon> |
| 157 | 240 | </Compile> |
| 158 | 241 | <Compile Include="Embedded\SecurityServiceHostFactory.cs" /> |
| 242 | + <Compile Include="Security\AuthorizationCache.cs" /> | |
| 243 | + <Compile Include="Security\AuthorizeControllerRoleAttribute.cs" /> | |
| 244 | + <Compile Include="Security\AuthorizeHubRoleAttribute.cs" /> | |
| 245 | + <Compile Include="Security\SecurityHub.cs" /> | |
| 246 | + <Compile Include="Templating\NamespaceDoc.cs" /> | |
| 247 | + <Compile Include="Templating\RazorView.cs" /> | |
| 159 | 248 | <Compile Include="UI\ClientSideExtensions.cs" /> |
| 249 | + <Compile Include="WebExtensions.cs" /> | |
| 160 | 250 | </ItemGroup> |
| 161 | 251 | <ItemGroup> |
| 162 | 252 | <ProjectReference Include="..\GSF.Core\GSF.Core.csproj"> |
| @@ -214,6 +304,21 @@ · 51 unmodified lines | ||
| 214 | 304 | </ItemGroup> |
| 215 | 305 | <ItemGroup> |
| 216 | 306 | <None Include="GSF.Web.nuspec" /> |
| 307 | + <EmbeddedResource Include="Model\Views\PagedViewModel.cshtml" /> | |
| 308 | + <None Include="packages.config" /> | |
| 309 | + <EmbeddedResource Include="Templating\Views\AddCheckBoxField.cshtml" /> | |
| 310 | + <EmbeddedResource Include="Templating\Views\AddDateField.cshtml" /> | |
| 311 | + <EmbeddedResource Include="Templating\Views\AddInputField.cshtml" /> | |
| 312 | + <EmbeddedResource Include="Templating\Views\AddSelectField.cshtml" /> | |
| 313 | + <EmbeddedResource Include="Templating\Views\AddTextAreaField.cshtml" /> | |
| 314 | + <EmbeddedResource Include="Security\Views\Groups.cshtml" /> | |
| 315 | + <EmbeddedResource Include="Security\Views\Users.cshtml" /> | |
| 316 | + </ItemGroup> | |
| 317 | + <ItemGroup> | |
| 318 | + <EmbeddedResource Include="Model\Scripts\pagedViewModel.js" /> | |
| 319 | + </ItemGroup> | |
| 320 | + <ItemGroup> | |
| 321 | + <EmbeddedResource Include="Model\Scripts\gsf.web.client.js" /> | |
| 217 | 322 | </ItemGroup> |
| 218 | 323 | <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> |
| 219 | 324 | <!-- To modify your build process, add your task inside one of the targets below and uncomment it. |
| @@ -0,0 +1,955 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// DataContext.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/01/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.Collections.Generic; | |
| 26 | +using System.ComponentModel.DataAnnotations; | |
| 27 | +using System.Linq; | |
| 28 | +using System.Text; | |
| 29 | +using GSF.Collections; | |
| 30 | +using GSF.Data; | |
| 31 | +using GSF.Data.Model; | |
| 32 | +using GSF.Reflection; | |
| 33 | +using GSF.Web.Templating; | |
| 34 | +using RazorEngine.Templating; | |
| 35 | + | |
| 36 | +namespace GSF.Web.Model | |
| 37 | +{ | |
| 38 | + /// <summary> | |
| 39 | + /// Defines a default data context object based on embedded resources. | |
| 40 | + /// </summary> | |
| 41 | + public class DataContext : DataContext<CSharpEmbeddedResource> | |
| 42 | + { | |
| 43 | + /// <summary> | |
| 44 | + /// Creates a new <see cref="DataContext"/>. | |
| 45 | + /// </summary> | |
| 46 | + /// <param name="connection"><see cref="AdoDataConnection"/> to use; defaults to a new connection.</param> | |
| 47 | + /// <param name="disposeConnection">Set to <c>true</c> to dispose the provided <paramref name="connection"/>.</param> | |
| 48 | + /// <param name="exceptionHandler">Delegate to handle exceptions.</param> | |
| 49 | + public DataContext(AdoDataConnection connection = null, bool disposeConnection = false, Action<Exception> exceptionHandler = null) : | |
| 50 | + base(connection, disposeConnection, exceptionHandler) | |
| 51 | + { | |
| 52 | + } | |
| 53 | + | |
| 54 | + /// <summary> | |
| 55 | + /// Creates a new <see cref="DataContext"/> using the specified <paramref name="settingsCategory"/>. | |
| 56 | + /// </summary> | |
| 57 | + /// <param name="settingsCategory">Setting category that contains the connection settings.</param> | |
| 58 | + /// <param name="exceptionHandler">Delegate to handle exceptions.</param> | |
| 59 | + public DataContext(string settingsCategory, Action<Exception> exceptionHandler = null) : | |
| 60 | + base(settingsCategory, exceptionHandler) | |
| 61 | + { | |
| 62 | + } | |
| 63 | + } | |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// Defines a data context for modeled tables. | |
| 67 | + /// </summary> | |
| 68 | + /// <typeparam name="TLanguage"><see cref="LanguageContraint"/> type to use for Razor views.</typeparam> | |
| 69 | + public class DataContext<TLanguage> : IDisposable where TLanguage : LanguageContraint, new() | |
| 70 | + { | |
| 71 | + #region [ Members ] | |
| 72 | + | |
| 73 | + // Fields | |
| 74 | + private AdoDataConnection m_connection; | |
| 75 | + private readonly Dictionary<Type, object> m_tableOperations; | |
| 76 | + private readonly Action<Exception> m_exceptionHandler; | |
| 77 | + private readonly Dictionary<string, Tuple<string, string>> m_fieldValidationParameters; | |
| 78 | + private readonly List<Tuple<string, string>> m_fieldValueInitializers; | |
| 79 | + private readonly List<string> m_definedDateFields; | |
| 80 | + private readonly string m_settingsCategory; | |
| 81 | + private readonly bool m_disposeConnection; | |
| 82 | + private string m_initialFocusField; | |
| 83 | + private string m_addDateFieldTemplate; | |
| 84 | + private string m_addInputFieldTemplate; | |
| 85 | + private string m_addTextAreaFieldTemplate; | |
| 86 | + private string m_addSelectFieldTemplate; | |
| 87 | + private string m_addCheckBoxFieldTemplate; | |
| 88 | + private bool m_disposed; | |
| 89 | + | |
| 90 | + #endregion | |
| 91 | + | |
| 92 | + #region [ Constructors ] | |
| 93 | + | |
| 94 | + /// <summary> | |
| 95 | + /// Creates a new <see cref="DataContext{TLanguage}"/>. | |
| 96 | + /// </summary> | |
| 97 | + /// <param name="connection"><see cref="AdoDataConnection"/> to use; defaults to a new connection.</param> | |
| 98 | + /// <param name="disposeConnection">Set to <c>true</c> to dispose the provided <paramref name="connection"/>.</param> | |
| 99 | + /// <param name="exceptionHandler">Delegate to handle exceptions.</param> | |
| 100 | + public DataContext(AdoDataConnection connection = null, bool disposeConnection = false, Action<Exception> exceptionHandler = null) | |
| 101 | + { | |
| 102 | + m_connection = connection; | |
| 103 | + m_tableOperations = new Dictionary<Type, object>(); | |
| 104 | + m_exceptionHandler = exceptionHandler; | |
| 105 | + m_fieldValidationParameters = new Dictionary<string, Tuple<string, string>>(); | |
| 106 | + m_fieldValueInitializers = new List<Tuple<string, string>>(); | |
| 107 | + m_definedDateFields = new List<string>(); | |
| 108 | + m_settingsCategory = "systemSettings"; | |
| 109 | + m_disposeConnection = disposeConnection || connection == null; | |
| 110 | + } | |
| 111 | + | |
| 112 | + /// <summary> | |
| 113 | + /// Creates a new <see cref="DataContext{TLanguage}"/> using the specified <paramref name="settingsCategory"/>. | |
| 114 | + /// </summary> | |
| 115 | + /// <param name="settingsCategory">Setting category that contains the connection settings.</param> | |
| 116 | + /// <param name="exceptionHandler">Delegate to handle exceptions.</param> | |
| 117 | + public DataContext(string settingsCategory, Action<Exception> exceptionHandler = null) | |
| 118 | + { | |
| 119 | + m_tableOperations = new Dictionary<Type, object>(); | |
| 120 | + m_exceptionHandler = exceptionHandler; | |
| 121 | + m_fieldValidationParameters = new Dictionary<string, Tuple<string, string>>(); | |
| 122 | + m_fieldValueInitializers = new List<Tuple<string, string>>(); | |
| 123 | + m_definedDateFields = new List<string>(); | |
| 124 | + m_settingsCategory = settingsCategory; | |
| 125 | + m_disposeConnection = true; | |
| 126 | + } | |
| 127 | + | |
| 128 | + #endregion | |
| 129 | + | |
| 130 | + #region [ Properties ] | |
| 131 | + | |
| 132 | + /// <summary> | |
| 133 | + /// Gets the <see cref="AdoDataConnection"/> for this <see cref="DataContext{TLanguage}"/>. | |
| 134 | + /// </summary> | |
| 135 | + public AdoDataConnection Connection => m_connection ?? (m_connection = new AdoDataConnection(m_settingsCategory)); | |
| 136 | + | |
| 137 | + /// <summary> | |
| 138 | + /// Gets validation pattern and error message for rendered fields, if any. | |
| 139 | + /// </summary> | |
| 140 | + public Dictionary<string, Tuple<string, string>> FieldValidationParameters => m_fieldValidationParameters; | |
| 141 | + | |
| 142 | + /// <summary> | |
| 143 | + /// Gets field value initializers, if any. | |
| 144 | + /// </summary> | |
| 145 | + public List<Tuple<string, string>> FieldValueInitializers => m_fieldValueInitializers; | |
| 146 | + | |
| 147 | + /// <summary> | |
| 148 | + /// Gets defined date fields, if any. | |
| 149 | + /// </summary> | |
| 150 | + public List<string> DefinedDateFields => m_definedDateFields; | |
| 151 | + | |
| 152 | + /// <summary> | |
| 153 | + /// Gets field name designated for initial focus. | |
| 154 | + /// </summary> | |
| 155 | + public string InitialFocusField => m_initialFocusField; | |
| 156 | + | |
| 157 | + /// <summary> | |
| 158 | + /// Gets or sets the date field razor template file name. | |
| 159 | + /// </summary> | |
| 160 | + public string AddDateFieldTemplate | |
| 161 | + { | |
| 162 | + get | |
| 163 | + { | |
| 164 | + return m_addDateFieldTemplate ?? (m_addDateFieldTemplate = $"{RazorView<TLanguage>.TemplatePath}AddDateField.cshtml"); | |
| 165 | + } | |
| 166 | + set | |
| 167 | + { | |
| 168 | + m_addDateFieldTemplate = value; | |
| 169 | + } | |
| 170 | + } | |
| 171 | + | |
| 172 | + /// <summary> | |
| 173 | + /// Gets or sets the input field razor template file name. | |
| 174 | + /// </summary> | |
| 175 | + public string AddInputFieldTemplate | |
| 176 | + { | |
| 177 | + get | |
| 178 | + { | |
| 179 | + return m_addInputFieldTemplate ?? (m_addInputFieldTemplate = $"{RazorView<TLanguage>.TemplatePath}AddInputField.cshtml"); | |
| 180 | + } | |
| 181 | + set | |
| 182 | + { | |
| 183 | + m_addInputFieldTemplate = value; | |
| 184 | + } | |
| 185 | + } | |
| 186 | + | |
| 187 | + /// <summary> | |
| 188 | + /// Gets or sets the text area field razor template file name. | |
| 189 | + /// </summary> | |
| 190 | + public string AddTextAreaFieldTemplate | |
| 191 | + { | |
| 192 | + get | |
| 193 | + { | |
| 194 | + return m_addTextAreaFieldTemplate ?? (m_addTextAreaFieldTemplate = $"{RazorView<TLanguage>.TemplatePath}AddTextAreaField.cshtml"); | |
| 195 | + } | |
| 196 | + set | |
| 197 | + { | |
| 198 | + m_addTextAreaFieldTemplate = value; | |
| 199 | + } | |
| 200 | + } | |
| 201 | + | |
| 202 | + /// <summary> | |
| 203 | + /// Gets or sets the select field razor template file name. | |
| 204 | + /// </summary> | |
| 205 | + public string AddSelectFieldTemplate | |
| 206 | + { | |
| 207 | + get | |
| 208 | + { | |
| 209 | + return m_addSelectFieldTemplate ?? (m_addSelectFieldTemplate = $"{RazorView<TLanguage>.TemplatePath}AddSelectField.cshtml"); | |
| 210 | + } | |
| 211 | + set | |
| 212 | + { | |
| 213 | + m_addSelectFieldTemplate = value; | |
| 214 | + } | |
| 215 | + } | |
| 216 | + | |
| 217 | + /// <summary> | |
| 218 | + /// Gets or sets the check box field razor template file name. | |
| 219 | + /// </summary> | |
| 220 | + public string AddCheckBoxFieldTemplate | |
| 221 | + { | |
| 222 | + get | |
| 223 | + { | |
| 224 | + return m_addCheckBoxFieldTemplate ?? (m_addCheckBoxFieldTemplate = $"{RazorView<TLanguage>.TemplatePath}AddCheckBoxField.cshtml"); | |
| 225 | + } | |
| 226 | + set | |
| 227 | + { | |
| 228 | + m_addCheckBoxFieldTemplate = value; | |
| 229 | + } | |
| 230 | + } | |
| 231 | + | |
| 232 | + #endregion | |
| 233 | + | |
| 234 | + #region [ Methods ] | |
| 235 | + | |
| 236 | + /// <summary> | |
| 237 | + /// Releases all the resources used by the <see cref="DataContext{TLanguage}"/> object. | |
| 238 | + /// </summary> | |
| 239 | + public void Dispose() | |
| 240 | + { | |
| 241 | + Dispose(true); | |
| 242 | + GC.SuppressFinalize(this); | |
| 243 | + } | |
| 244 | + | |
| 245 | + /// <summary> | |
| 246 | + /// Releases the unmanaged resources used by the <see cref="DataContext{TLanguage}"/> object and optionally releases the managed resources. | |
| 247 | + /// </summary> | |
| 248 | + /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param> | |
| 249 | + protected virtual void Dispose(bool disposing) | |
| 250 | + { | |
| 251 | + if (!m_disposed) | |
| 252 | + { | |
| 253 | + try | |
| 254 | + { | |
| 255 | + if (disposing) | |
| 256 | + { | |
| 257 | + if (m_disposeConnection) | |
| 258 | + m_connection?.Dispose(); | |
| 259 | + } | |
| 260 | + } | |
| 261 | + finally | |
| 262 | + { | |
| 263 | + m_disposed = true; // Prevent duplicate dispose. | |
| 264 | + } | |
| 265 | + } | |
| 266 | + } | |
| 267 | + | |
| 268 | + /// <summary> | |
| 269 | + /// Gets the table operations for the specified modeled table <typeparamref name="T"/>. | |
| 270 | + /// </summary> | |
| 271 | + /// <typeparam name="T">Modeled database table (or view).</typeparam> | |
| 272 | + /// <returns>Table operations for the specified modeled table <typeparamref name="T"/>.</returns> | |
| 273 | + public TableOperations<T> Table<T>() where T : class, new() | |
| 274 | + { | |
| 275 | + return m_tableOperations.GetOrAdd(typeof(T), type => new TableOperations<T>(Connection, m_exceptionHandler)) as TableOperations<T>; | |
| 276 | + } | |
| 277 | + | |
| 278 | + /// <summary> | |
| 279 | + /// Gets the field name targeted as the primary label for the modeled table. | |
| 280 | + /// </summary> | |
| 281 | + /// <typeparam name="T">Modeled database table (or view).</typeparam> | |
| 282 | + /// <returns>Field name targeted as the primary label for the modeled table.</returns> | |
| 283 | + public string GetPrimaryLabelField<T>() where T : class, new() | |
| 284 | + { | |
| 285 | + PrimaryLabelAttribute primaryLabelAttribute; | |
| 286 | + | |
| 287 | + if (typeof(T).TryGetAttribute(out primaryLabelAttribute) && !string.IsNullOrWhiteSpace(primaryLabelAttribute.FieldName)) | |
| 288 | + return primaryLabelAttribute.FieldName; | |
| 289 | + | |
| 290 | + string[] fieldNames = Table<T>().GetFieldNames(); | |
| 291 | + return fieldNames.Length > 0 ? fieldNames[0] : ""; | |
| 292 | + } | |
| 293 | + | |
| 294 | + /// <summary> | |
| 295 | + /// Gets the field name targeted to mark a record as deleted. | |
| 296 | + /// </summary> | |
| 297 | + /// <typeparam name="T">Modeled database table (or view).</typeparam> | |
| 298 | + /// <returns>Field name targeted to mark a record as deleted.</returns> | |
| 299 | + public string GetIsDeletedFlag<T>() where T : class, new() | |
| 300 | + { | |
| 301 | + IsDeletedFlagAttribute isDeletedFlagAttribute; | |
| 302 | + | |
| 303 | + if (typeof(T).TryGetAttribute(out isDeletedFlagAttribute) && !string.IsNullOrWhiteSpace(isDeletedFlagAttribute.FieldName)) | |
| 304 | + return isDeletedFlagAttribute.FieldName; | |
| 305 | + | |
| 306 | + return null; | |
| 307 | + } | |
| 308 | + | |
| 309 | + /// <summary> | |
| 310 | + /// Looks up proper user roles for paged based on modeled security in <see cref="RecordOperationsCache"/>. | |
| 311 | + /// </summary> | |
| 312 | + /// <typeparam name="T">Modeled database table (or view).</typeparam> | |
| 313 | + /// <param name="cache">Data hub record operations cache.</param> | |
| 314 | + /// <param name="viewBag">ViewBag for the current view.</param> | |
| 315 | + /// <remarks> | |
| 316 | + /// Typically used in paged view model scenarios and invoked by controller prior to view rendering. | |
| 317 | + /// Security is controlled at hub level, so failure to call will not impact security but may result | |
| 318 | + /// in screen enabling and/or showing controls that the user does not actually have access to and | |
| 319 | + /// upon attempted use will result in a security error. | |
| 320 | + /// </remarks> | |
| 321 | + public void EstablishUserRolesForPage<T>(RecordOperationsCache cache, dynamic viewBag) where T : class, new() | |
| 322 | + { | |
| 323 | + // Get any authorized roles as defined in hub for key records operations of modeled table | |
| 324 | + Tuple<string, string>[] recordOperations = cache.GetRecordOperations<T>(); | |
| 325 | + | |
| 326 | + // Create a function to check if a method exists for operation - if none is defined, read-only access will be assumed (e.g. for a view) | |
| 327 | + Func<RecordOperation, string> getRoles = operationType => | |
| 328 | + { | |
| 329 | + Tuple<string, string> recordOperation = recordOperations[(int)operationType]; | |
| 330 | + return string.IsNullOrEmpty(recordOperation?.Item1) ? "" : recordOperation.Item2 ?? ""; | |
| 331 | + }; | |
| 332 | + | |
| 333 | + viewBag.EditRoles = getRoles(RecordOperation.UpdateRecord); | |
| 334 | + viewBag.AddNewRoles = getRoles(RecordOperation.AddNewRecord); | |
| 335 | + viewBag.DeleteRoles = getRoles(RecordOperation.DeleteRecord); | |
| 336 | + } | |
| 337 | + | |
| 338 | + /// <summary> | |
| 339 | + /// Renders client-side configuration script for paged view model. | |
| 340 | + /// </summary> | |
| 341 | + /// <typeparam name="T">Modeled database table (or view).</typeparam> | |
| 342 | + /// <param name="cache">Data hub record operations cache.</param> | |
| 343 | + /// <param name="viewBag">ViewBag for the view.</param> | |
| 344 | + /// <param name="defaultSortField">Default sort field name, defaults to first primary key field. Prefix field name with a minus, i.e., '-', to default to descending sort.</param> | |
| 345 | + /// <param name="parentKeys">Primary keys values of the parent record to load.</param> | |
| 346 | + /// <returns>Rendered paged view model configuration script.</returns> | |
| 347 | + public string RenderViewModelConfiguration<T>(RecordOperationsCache cache, dynamic viewBag, string defaultSortField = null, params object[] parentKeys) where T : class, new() | |
| 348 | + { | |
| 349 | + StringBuilder javascript = new StringBuilder(); | |
| 350 | + string[] primaryKeyFields = Table<T>().GetPrimaryKeyFieldNames(); | |
| 351 | + string defaultSortAscending = "true"; | |
| 352 | + | |
| 353 | + defaultSortField = defaultSortField ?? primaryKeyFields[0]; | |
| 354 | + | |
| 355 | + // Handle case for desired default descending sort order | |
| 356 | + if (defaultSortField[0] == '-') | |
| 357 | + { | |
| 358 | + defaultSortField = defaultSortField.Substring(1); | |
| 359 | + defaultSortAscending = "false"; | |
| 360 | + } | |
| 361 | + | |
| 362 | + javascript.Append($@"// Configure view model | |
| 363 | + viewModel.defaultSortField = ""{defaultSortField}""; | |
| 364 | + viewModel.defaultSortAscending = {defaultSortAscending}; | |
| 365 | + viewModel.labelField = ""{GetPrimaryLabelField<T>()}""; | |
| 366 | + viewModel.primaryKeyFields = [{primaryKeyFields.Select(fieldName => $"\"{fieldName}\"").ToDelimitedString(", ")}]; | |
| 367 | + ".FixForwardSpacing()); | |
| 368 | + | |
| 369 | + string showDeletedValue = null; | |
| 370 | + | |
| 371 | + if (viewBag.IsDeletedField != null) | |
| 372 | + showDeletedValue = (viewBag.ShowDeleted ?? false).ToString().ToLower(); | |
| 373 | + | |
| 374 | + Func<string, string> toCamelCase = methodName => methodName == null ? null : $"{char.ToLower(methodName[0])}{methodName.Substring(1)}"; | |
| 375 | + | |
| 376 | + // Get method names for records operations of modeled table | |
| 377 | + Tuple<string, string>[] recordOperations = cache.GetRecordOperations<T>(); | |
| 378 | + string queryRecordCountMethod = toCamelCase(recordOperations[(int)RecordOperation.QueryRecordCount]?.Item1); | |
| 379 | + string queryRecordsMethod = toCamelCase(recordOperations[(int)RecordOperation.QueryRecords]?.Item1); | |
| 380 | + string deleteRecordMethod = toCamelCase(recordOperations[(int)RecordOperation.DeleteRecord]?.Item1); | |
| 381 | + string createNewRecordMethod = toCamelCase(recordOperations[(int)RecordOperation.CreateNewRecord]?.Item1); | |
| 382 | + string addNewRecordMethod = toCamelCase(recordOperations[(int)RecordOperation.AddNewRecord]?.Item1); | |
| 383 | + string updateMethod = toCamelCase(recordOperations[(int)RecordOperation.UpdateRecord]?.Item1); | |
| 384 | + | |
| 385 | + string keyValues = null; | |
| 386 | + | |
| 387 | + if (parentKeys.Length > 0) | |
| 388 | + keyValues = parentKeys.ToDelimitedString(", "); | |
| 389 | + | |
| 390 | + // If modeled table has IsDeletedField marker, the showDeleted parameter should come first in DataHub operations | |
| 391 | + if (showDeletedValue != null) | |
| 392 | + keyValues = keyValues != null ? $"{showDeletedValue}, {keyValues}" : showDeletedValue; | |
| 393 | + | |
| 394 | + if (!string.IsNullOrWhiteSpace(queryRecordCountMethod)) | |
| 395 | + javascript.Append($@" | |
| 396 | + viewModel.setQueryRecordCount(function () {{ | |
| 397 | + return dataHub.{queryRecordCountMethod}({keyValues}); | |
| 398 | + }}); | |
| 399 | + ".FixForwardSpacing()); | |
| 400 | + | |
| 401 | + if (!string.IsNullOrWhiteSpace(queryRecordsMethod)) | |
| 402 | + javascript.Append($@" | |
| 403 | + viewModel.setQueryRecords(function (sortField, ascending, page, pageSize) {{ | |
| 404 | + return dataHub.{queryRecordsMethod}({(keyValues == null ? "" : $"{keyValues}, ")}sortField, ascending, page, pageSize); | |
| 405 | + }}); | |
| 406 | + ".FixForwardSpacing()); | |
| 407 | + | |
| 408 | + if (!string.IsNullOrWhiteSpace(deleteRecordMethod)) | |
| 409 | + javascript.Append($@" | |
| 410 | + viewModel.setDeleteRecord(function (keyValues) {{ | |
| 411 | + return dataHub.{deleteRecordMethod}({Enumerable.Range(0, Table<T>().GetPrimaryKeyFieldNames().Length).Select(index => $"keyValues[{index}]").ToDelimitedString(", ")}); | |
| 412 | + }}); | |
| 413 | + ".FixForwardSpacing()); | |
| 414 | + | |
| 415 | + if (!string.IsNullOrWhiteSpace(createNewRecordMethod)) | |
| 416 | + javascript.Append($@" | |
| 417 | + viewModel.setNewRecord(function () {{ | |
| 418 | + return dataHub.{createNewRecordMethod}(); | |
| 419 | + }}); | |
| 420 | + ".FixForwardSpacing()); | |
| 421 | + | |
| 422 | + if (!string.IsNullOrWhiteSpace(addNewRecordMethod)) | |
| 423 | + javascript.Append($@" | |
| 424 | + viewModel.setAddNewRecord(function (record) {{ | |
| 425 | + return dataHub.{addNewRecordMethod}(record); | |
| 426 | + }}); | |
| 427 | + ".FixForwardSpacing()); | |
| 428 | + | |
| 429 | + if (!string.IsNullOrWhiteSpace(updateMethod)) | |
| 430 | + javascript.Append($@" | |
| 431 | + viewModel.setUpdateRecord(function (record) {{ | |
| 432 | + return dataHub.{updateMethod}(record); | |
| 433 | + }}); | |
| 434 | + ".FixForwardSpacing()); | |
| 435 | + | |
| 436 | + return javascript.ToString(); | |
| 437 | + } | |
| 438 | + | |
| 439 | + /// <summary> | |
| 440 | + /// Adds a new pattern based validation and option error message to a field. | |
| 441 | + /// </summary> | |
| 442 | + /// <param name="observableFieldReference">Observable field reference (from JS view model).</param> | |
| 443 | + /// <param name="validationPattern">Regex based validation pattern.</param> | |
| 444 | + /// <param name="errorMessage">Optional error message to display when pattern fails.</param> | |
| 445 | + public void AddFieldValidation(string observableFieldReference, string validationPattern, string errorMessage = null) | |
| 446 | + { | |
| 447 | + m_fieldValidationParameters[observableFieldReference] = new Tuple<string, string>(validationPattern, errorMessage); | |
| 448 | + } | |
| 449 | + | |
| 450 | + /// <summary> | |
| 451 | + /// Adds a new field value initializer. | |
| 452 | + /// </summary> | |
| 453 | + /// <param name="fieldName">Field name (as defined in model).</param> | |
| 454 | + /// <param name="initialValue">Javascript based initial value for field.</param> | |
| 455 | + public void AddFieldValueInitializer(string fieldName, string initialValue = null) | |
| 456 | + { | |
| 457 | + m_fieldValueInitializers.Add(new Tuple<string, string>(fieldName, initialValue ?? "\"\"")); | |
| 458 | + } | |
| 459 | + | |
| 460 | + /// <summary> | |
| 461 | + /// Adds a new field value initializer based on modeled table field attributes. | |
| 462 | + /// </summary> | |
| 463 | + /// <param name="fieldName">Field name (as defined in model).</param> | |
| 464 | + public void AddFieldValueInitializer<T>(string fieldName) where T : class, new() | |
| 465 | + { | |
| 466 | + InitialValueAttribute initialValueAttribute; | |
| 467 | + | |
| 468 | + if (Table<T>().TryGetFieldAttribute(fieldName, out initialValueAttribute)) | |
| 469 | + AddFieldValueInitializer(fieldName, initialValueAttribute.InitialValue); | |
| 470 | + } | |
| 471 | + | |
| 472 | + /// <summary> | |
| 473 | + /// Generates template based input date field based on reflected modeled table field attributes. | |
| 474 | + /// </summary> | |
| 475 | + /// <typeparam name="T">Modeled table.</typeparam> | |
| 476 | + /// <param name="fieldName">Field name for input date field.</param> | |
| 477 | + /// <param name="inputType">Input field type, defaults to text.</param> | |
| 478 | + /// <param name="fieldLabel">Label name for input date field, pulls from <see cref="LabelAttribute"/> if defined, otherwise defaults to <paramref name="fieldName"/>.</param> | |
| 479 | + /// <param name="fieldID">ID to use for input field; defaults to date + <paramref name="fieldName"/>.</param> | |
| 480 | + /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param> | |
| 481 | + /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param> | |
| 482 | + /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param> | |
| 483 | + /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param> | |
| 484 | + /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param> | |
| 485 | + /// <param name="toolTip">Tool tip text to apply to field, if any.</param> | |
| 486 | + /// <param name="initialFocus">Use field for initial focus.</param> | |
| 487 | + /// <returns>Generated HTML for new date based input field based on modeled table field attributes.</returns> | |
| 488 | + public string AddDateField<T>(string fieldName, string inputType = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) where T : class, new() | |
| 489 | + { | |
| 490 | + TableOperations<T> tableOperations = Table<T>(); | |
| 491 | + StringLengthAttribute stringLengthAttribute; | |
| 492 | + RegularExpressionAttribute regularExpressionAttribute; | |
| 493 | + | |
| 494 | + if (string.IsNullOrEmpty(fieldLabel)) | |
| 495 | + { | |
| 496 | + LabelAttribute labelAttribute; | |
| 497 | + | |
| 498 | + if (tableOperations.TryGetFieldAttribute(fieldName, out labelAttribute)) | |
| 499 | + fieldLabel = labelAttribute.Label; | |
| 500 | + } | |
| 501 | + | |
| 502 | + tableOperations.TryGetFieldAttribute(fieldName, out stringLengthAttribute); | |
| 503 | + tableOperations.TryGetFieldAttribute(fieldName, out regularExpressionAttribute); | |
| 504 | + | |
| 505 | + if ((object)regularExpressionAttribute != null) | |
| 506 | + { | |
| 507 | + string observableReference; | |
| 508 | + | |
| 509 | + if (string.IsNullOrEmpty(groupDataBinding)) | |
| 510 | + observableReference = $"viewModel.currentRecord().{fieldName}"; | |
| 511 | + else // "with: $root.connectionString" | |
| 512 | + observableReference = $"viewModel.{groupDataBinding.Substring(groupDataBinding.IndexOf('.') + 1)}"; | |
| 513 | + | |
| 514 | + AddFieldValidation(observableReference, regularExpressionAttribute.Pattern, regularExpressionAttribute.ErrorMessage ?? "Invalid format."); | |
| 515 | + } | |
| 516 | + | |
| 517 | + AddFieldValueInitializer<T>(fieldName); | |
| 518 | + | |
| 519 | + return AddDateField(fieldName, tableOperations.FieldHasAttribute<RequiredAttribute>(fieldName), | |
| 520 | + stringLengthAttribute?.MaximumLength ?? 0, inputType, fieldLabel, fieldID, groupDataBinding, labelDataBinding, requiredDataBinding, customDataBinding, dependencyFieldName, toolTip, initialFocus); | |
| 521 | + } | |
| 522 | + | |
| 523 | + /// <summary> | |
| 524 | + /// Generates template based input date field based on specified parameters. | |
| 525 | + /// </summary> | |
| 526 | + /// <param name="fieldName">Field name for input date field.</param> | |
| 527 | + /// <param name="required">Determines if field name is required.</param> | |
| 528 | + /// <param name="maxLength">Defines maximum input field length.</param> | |
| 529 | + /// <param name="inputType">Input field type, defaults to text.</param> | |
| 530 | + /// <param name="fieldLabel">Label name for input date field, defaults to <paramref name="fieldName"/>.</param> | |
| 531 | + /// <param name="fieldID">ID to use for input field; defaults to date + <paramref name="fieldName"/>.</param> | |
| 532 | + /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param> | |
| 533 | + /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param> | |
| 534 | + /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param> | |
| 535 | + /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param> | |
| 536 | + /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param> | |
| 537 | + /// <param name="toolTip">Tool tip text to apply to field, if any.</param> | |
| 538 | + /// <param name="initialFocus">Use field for initial focus.</param> | |
| 539 | + /// <returns>Generated HTML for new date based input field based on specified parameters.</returns> | |
| 540 | + public string AddDateField(string fieldName, bool required, int maxLength = 0, string inputType = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) | |
| 541 | + { | |
| 542 | + RazorView<TLanguage> addDateFieldTemplate = new RazorView<TLanguage>(AddDateFieldTemplate, m_exceptionHandler); | |
| 543 | + DynamicViewBag viewBag = addDateFieldTemplate.ViewBag; | |
| 544 | + | |
| 545 | + if (string.IsNullOrEmpty(fieldID)) | |
| 546 | + fieldID = $"date{fieldName}"; | |
| 547 | + | |
| 548 | + if (initialFocus) | |
| 549 | + m_initialFocusField = fieldID; | |
| 550 | + | |
| 551 | + viewBag.AddValue("FieldName", fieldName); | |
| 552 | + viewBag.AddValue("Required", required); | |
| 553 | + viewBag.AddValue("MaxLength", maxLength); | |
| 554 | + viewBag.AddValue("InputType", inputType ?? "text"); | |
| 555 | + viewBag.AddValue("FieldLabel", fieldLabel ?? fieldName); | |
| 556 | + viewBag.AddValue("FieldID", fieldID); | |
| 557 | + viewBag.AddValue("GroupDataBinding", groupDataBinding); | |
| 558 | + viewBag.AddValue("LabelDataBinding", labelDataBinding); | |
| 559 | + viewBag.AddValue("RequiredDataBinding", requiredDataBinding); | |
| 560 | + viewBag.AddValue("CustomDataBinding", customDataBinding); | |
| 561 | + viewBag.AddValue("DependencyFieldName", dependencyFieldName); | |
| 562 | + viewBag.AddValue("ToolTip", toolTip); | |
| 563 | + | |
| 564 | + m_definedDateFields.Add(fieldName); | |
| 565 | + | |
| 566 | + return addDateFieldTemplate.Execute(); | |
| 567 | + } | |
| 568 | + | |
| 569 | + /// <summary> | |
| 570 | + /// Generates template based input text field based on reflected modeled table field attributes. | |
| 571 | + /// </summary> | |
| 572 | + /// <typeparam name="T">Modeled table.</typeparam> | |
| 573 | + /// <param name="fieldName">Field name for input text field.</param> | |
| 574 | + /// <param name="inputType">Input field type, defaults to text.</param> | |
| 575 | + /// <param name="fieldLabel">Label name for input text field, pulls from <see cref="LabelAttribute"/> if defined, otherwise defaults to <paramref name="fieldName"/>.</param> | |
| 576 | + /// <param name="fieldID">ID to use for input field; defaults to input + <paramref name="fieldName"/>.</param> | |
| 577 | + /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param> | |
| 578 | + /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param> | |
| 579 | + /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param> | |
| 580 | + /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param> | |
| 581 | + /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param> | |
| 582 | + /// <param name="toolTip">Tool tip text to apply to field, if any.</param> | |
| 583 | + /// <param name="initialFocus">Use field for initial focus.</param> | |
| 584 | + /// <returns>Generated HTML for new input field based on modeled table field attributes.</returns> | |
| 585 | + public string AddInputField<T>(string fieldName, string inputType = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) where T : class, new() | |
| 586 | + { | |
| 587 | + TableOperations<T> tableOperations = Table<T>(); | |
| 588 | + StringLengthAttribute stringLengthAttribute; | |
| 589 | + RegularExpressionAttribute regularExpressionAttribute; | |
| 590 | + | |
| 591 | + if (string.IsNullOrEmpty(inputType)) | |
| 592 | + { | |
| 593 | + Type fieldType = tableOperations.GetFieldType(fieldName); | |
| 594 | + | |
| 595 | + if (IsNumericType(fieldType)) | |
| 596 | + inputType = "number"; | |
| 597 | + else if (fieldType == typeof(DateTime)) | |
| 598 | + inputType = "date"; | |
| 599 | + } | |
| 600 | + | |
| 601 | + if (string.IsNullOrEmpty(fieldLabel)) | |
| 602 | + { | |
| 603 | + LabelAttribute labelAttribute; | |
| 604 | + | |
| 605 | + if (tableOperations.TryGetFieldAttribute(fieldName, out labelAttribute)) | |
| 606 | + fieldLabel = labelAttribute.Label; | |
| 607 | + } | |
| 608 | + | |
| 609 | + tableOperations.TryGetFieldAttribute(fieldName, out stringLengthAttribute); | |
| 610 | + tableOperations.TryGetFieldAttribute(fieldName, out regularExpressionAttribute); | |
| 611 | + | |
| 612 | + if ((object)regularExpressionAttribute != null) | |
| 613 | + { | |
| 614 | + string observableReference; | |
| 615 | + | |
| 616 | + if (string.IsNullOrEmpty(groupDataBinding)) | |
| 617 | + observableReference = $"viewModel.currentRecord().{fieldName}"; | |
| 618 | + else // "with: $root.connectionString" | |
| 619 | + observableReference = $"viewModel.{groupDataBinding.Substring(groupDataBinding.IndexOf('.') + 1)}"; | |
| 620 | + | |
| 621 | + AddFieldValidation(observableReference, regularExpressionAttribute.Pattern, regularExpressionAttribute.ErrorMessage ?? "Invalid format."); | |
| 622 | + } | |
| 623 | + | |
| 624 | + AddFieldValueInitializer<T>(fieldName); | |
| 625 | + | |
| 626 | + return AddInputField(fieldName, tableOperations.FieldHasAttribute<RequiredAttribute>(fieldName), | |
| 627 | + stringLengthAttribute?.MaximumLength ?? 0, inputType, fieldLabel, fieldID, groupDataBinding, labelDataBinding, requiredDataBinding, customDataBinding, dependencyFieldName, toolTip, initialFocus); | |
| 628 | + } | |
| 629 | + | |
| 630 | + /// <summary> | |
| 631 | + /// Generates template based input text field based on specified parameters. | |
| 632 | + /// </summary> | |
| 633 | + /// <param name="fieldName">Field name for input text field.</param> | |
| 634 | + /// <param name="required">Determines if field name is required.</param> | |
| 635 | + /// <param name="maxLength">Defines maximum input field length.</param> | |
| 636 | + /// <param name="inputType">Input field type, defaults to text.</param> | |
| 637 | + /// <param name="fieldLabel">Label name for input text field, defaults to <paramref name="fieldName"/>.</param> | |
| 638 | + /// <param name="fieldID">ID to use for input field; defaults to input + <paramref name="fieldName"/>.</param> | |
| 639 | + /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param> | |
| 640 | + /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param> | |
| 641 | + /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param> | |
| 642 | + /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param> | |
| 643 | + /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param> | |
| 644 | + /// <param name="toolTip">Tool tip text to apply to field, if any.</param> | |
| 645 | + /// <param name="initialFocus">Use field for initial focus.</param> | |
| 646 | + /// <returns>Generated HTML for new input field based on specified parameters.</returns> | |
| 647 | + public string AddInputField(string fieldName, bool required, int maxLength = 0, string inputType = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) | |
| 648 | + { | |
| 649 | + RazorView<TLanguage> addInputFieldTemplate = new RazorView<TLanguage>(AddInputFieldTemplate, m_exceptionHandler); | |
| 650 | + DynamicViewBag viewBag = addInputFieldTemplate.ViewBag; | |
| 651 | + | |
| 652 | + if (string.IsNullOrEmpty(fieldID)) | |
| 653 | + fieldID = $"input{fieldName}"; | |
| 654 | + | |
| 655 | + if (initialFocus) | |
| 656 | + m_initialFocusField = fieldID; | |
| 657 | + | |
| 658 | + viewBag.AddValue("FieldName", fieldName); | |
| 659 | + viewBag.AddValue("Required", required); | |
| 660 | + viewBag.AddValue("MaxLength", maxLength); | |
| 661 | + viewBag.AddValue("InputType", inputType ?? "text"); | |
| 662 | + viewBag.AddValue("FieldLabel", fieldLabel ?? fieldName); | |
| 663 | + viewBag.AddValue("FieldID", fieldID); | |
| 664 | + viewBag.AddValue("GroupDataBinding", groupDataBinding); | |
| 665 | + viewBag.AddValue("LabelDataBinding", labelDataBinding); | |
| 666 | + viewBag.AddValue("RequiredDataBinding", requiredDataBinding); | |
| 667 | + viewBag.AddValue("CustomDataBinding", customDataBinding); | |
| 668 | + viewBag.AddValue("DependencyFieldName", dependencyFieldName); | |
| 669 | + viewBag.AddValue("ToolTip", toolTip); | |
| 670 | + | |
| 671 | + return addInputFieldTemplate.Execute(); | |
| 672 | + } | |
| 673 | + | |
| 674 | + /// <summary> | |
| 675 | + /// Generates template based text area field based on reflected modeled table field attributes. | |
| 676 | + /// </summary> | |
| 677 | + /// <typeparam name="T">Modeled table.</typeparam> | |
| 678 | + /// <param name="fieldName">Field name for text area field.</param> | |
| 679 | + /// <param name="rows">Number of rows for text area.</param> | |
| 680 | + /// <param name="fieldLabel">Label name for text area field, pulls from <see cref="LabelAttribute"/> if defined, otherwise defaults to <paramref name="fieldName"/>.</param> | |
| 681 | + /// <param name="fieldID">ID to use for text area field; defaults to text + <paramref name="fieldName"/>.</param> | |
| 682 | + /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param> | |
| 683 | + /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param> | |
| 684 | + /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param> | |
| 685 | + /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param> | |
| 686 | + /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param> | |
| 687 | + /// <param name="toolTip">Tool tip text to apply to field, if any.</param> | |
| 688 | + /// <param name="initialFocus">Use field for initial focus.</param> | |
| 689 | + /// <returns>Generated HTML for new text area field based on modeled table field attributes.</returns> | |
| 690 | + public string AddTextAreaField<T>(string fieldName, int rows = 2, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) where T : class, new() | |
| 691 | + { | |
| 692 | + TableOperations<T> tableOperations = Table<T>(); | |
| 693 | + StringLengthAttribute stringLengthAttribute; | |
| 694 | + RegularExpressionAttribute regularExpressionAttribute; | |
| 695 | + | |
| 696 | + if (string.IsNullOrEmpty(fieldLabel)) | |
| 697 | + { | |
| 698 | + LabelAttribute labelAttribute; | |
| 699 | + | |
| 700 | + if (tableOperations.TryGetFieldAttribute(fieldName, out labelAttribute)) | |
| 701 | + fieldLabel = labelAttribute.Label; | |
| 702 | + } | |
| 703 | + | |
| 704 | + tableOperations.TryGetFieldAttribute(fieldName, out stringLengthAttribute); | |
| 705 | + tableOperations.TryGetFieldAttribute(fieldName, out regularExpressionAttribute); | |
| 706 | + | |
| 707 | + if ((object)regularExpressionAttribute != null) | |
| 708 | + { | |
| 709 | + string observableReference; | |
| 710 | + | |
| 711 | + if (string.IsNullOrEmpty(groupDataBinding)) | |
| 712 | + observableReference = $"viewModel.currentRecord().{fieldName}"; | |
| 713 | + else // "with: $root.connectionString" | |
| 714 | + observableReference = $"viewModel.{groupDataBinding.Substring(groupDataBinding.IndexOf('.') + 1)}"; | |
| 715 | + | |
| 716 | + AddFieldValidation(observableReference, regularExpressionAttribute.Pattern, regularExpressionAttribute.ErrorMessage ?? "Invalid format."); | |
| 717 | + } | |
| 718 | + | |
| 719 | + AddFieldValueInitializer<T>(fieldName); | |
| 720 | + | |
| 721 | + return AddTextAreaField(fieldName, tableOperations.FieldHasAttribute<RequiredAttribute>(fieldName), | |
| 722 | + stringLengthAttribute?.MaximumLength ?? 0, rows, fieldLabel, fieldID, groupDataBinding, labelDataBinding, requiredDataBinding, customDataBinding, dependencyFieldName, toolTip, initialFocus); | |
| 723 | + } | |
| 724 | + | |
| 725 | + /// <summary> | |
| 726 | + /// Generates template based text area field based on specified parameters. | |
| 727 | + /// </summary> | |
| 728 | + /// <param name="fieldName">Field name for text area field.</param> | |
| 729 | + /// <param name="required">Determines if field name is required.</param> | |
| 730 | + /// <param name="maxLength">Defines maximum text area field length.</param> | |
| 731 | + /// <param name="rows">Number of rows for text area.</param> | |
| 732 | + /// <param name="fieldLabel">Label name for text area field, defaults to <paramref name="fieldName"/>.</param> | |
| 733 | + /// <param name="fieldID">ID to use for text area field; defaults to text + <paramref name="fieldName"/>.</param> | |
| 734 | + /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param> | |
| 735 | + /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param> | |
| 736 | + /// <param name="requiredDataBinding">Boolean data-bind operation to apply to required state, if any.</param> | |
| 737 | + /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param> | |
| 738 | + /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param> | |
| 739 | + /// <param name="toolTip">Tool tip text to apply to field, if any.</param> | |
| 740 | + /// <param name="initialFocus">Use field for initial focus.</param> | |
| 741 | + /// <returns>Generated HTML for new text area field based on specified parameters.</returns> | |
| 742 | + public string AddTextAreaField(string fieldName, bool required, int maxLength = 0, int rows = 2, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string requiredDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) | |
| 743 | + { | |
| 744 | + RazorView<TLanguage> addTextAreaTemplate = new RazorView<TLanguage>(AddTextAreaFieldTemplate, m_exceptionHandler); | |
| 745 | + DynamicViewBag viewBag = addTextAreaTemplate.ViewBag; | |
| 746 | + | |
| 747 | + if (string.IsNullOrEmpty(fieldID)) | |
| 748 | + fieldID = $"text{fieldName}"; | |
| 749 | + | |
| 750 | + if (initialFocus) | |
| 751 | + m_initialFocusField = fieldID; | |
| 752 | + | |
| 753 | + viewBag.AddValue("FieldName", fieldName); | |
| 754 | + viewBag.AddValue("Required", required); | |
| 755 | + viewBag.AddValue("MaxLength", maxLength); | |
| 756 | + viewBag.AddValue("Rows", rows); | |
| 757 | + viewBag.AddValue("FieldLabel", fieldLabel ?? fieldName); | |
| 758 | + viewBag.AddValue("FieldID", fieldID); | |
| 759 | + viewBag.AddValue("GroupDataBinding", groupDataBinding); | |
| 760 | + viewBag.AddValue("LabelDataBinding", labelDataBinding); | |
| 761 | + viewBag.AddValue("RequiredDataBinding", requiredDataBinding); | |
| 762 | + viewBag.AddValue("CustomDataBinding", customDataBinding); | |
| 763 | + viewBag.AddValue("DependencyFieldName", dependencyFieldName); | |
| 764 | + viewBag.AddValue("ToolTip", toolTip); | |
| 765 | + | |
| 766 | + return addTextAreaTemplate.Execute(); | |
| 767 | + } | |
| 768 | + | |
| 769 | + /// <summary> | |
| 770 | + /// Generates template based select field based on reflected modeled table field attributes. | |
| 771 | + /// </summary> | |
| 772 | + /// <typeparam name="TSelect">Modeled table for select field.</typeparam> | |
| 773 | + /// <typeparam name="TOption">Modeled table for option data.</typeparam> | |
| 774 | + /// <param name="fieldName">Field name for value of select field.</param> | |
| 775 | + /// <param name="optionValueFieldName">Field name for ID of option data.</param> | |
| 776 | + /// <param name="optionLabelFieldName">Field name for label of option data, defaults to <paramref name="optionValueFieldName"/></param> | |
| 777 | + /// <param name="optionSortFieldName">Field name for sort order of option data, defaults to <paramref name="optionLabelFieldName"/></param> | |
| 778 | + /// <param name="fieldLabel">Label name for select field, pulls from <see cref="LabelAttribute"/> if defined, otherwise defaults to <paramref name="fieldName"/>.</param> | |
| 779 | + /// <param name="fieldID">ID to use for select field; defaults to select + <paramref name="fieldName"/>.</param> | |
| 780 | + /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param> | |
| 781 | + /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param> | |
| 782 | + /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param> | |
| 783 | + /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param> | |
| 784 | + /// <param name="optionDataBinding">Data-bind operations to apply to each option value, if any.</param> | |
| 785 | + /// <param name="toolTip">Tool tip text to apply to field, if any.</param> | |
| 786 | + /// <param name="initialFocus">Use field for initial focus.</param> | |
| 787 | + /// <param name="restriction">Record restriction to apply, if any.</param> | |
| 788 | + /// <returns>Generated HTML for new text field based on modeled table field attributes.</returns> | |
| 789 | + public string AddSelectField<TSelect, TOption>(string fieldName, string optionValueFieldName, string optionLabelFieldName = null, string optionSortFieldName = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string optionDataBinding = null, string toolTip = null, bool initialFocus = false, RecordRestriction restriction = null) where TSelect : class, new() where TOption : class, new() | |
| 790 | + { | |
| 791 | + if (string.IsNullOrEmpty(fieldLabel)) | |
| 792 | + { | |
| 793 | + LabelAttribute labelAttribute; | |
| 794 | + | |
| 795 | + if (Table<TSelect>().TryGetFieldAttribute(fieldName, out labelAttribute)) | |
| 796 | + fieldLabel = labelAttribute.Label; | |
| 797 | + } | |
| 798 | + | |
| 799 | + AddFieldValueInitializer<TSelect>(fieldName); | |
| 800 | + | |
| 801 | + return AddSelectField<TOption>(fieldName, Table<TSelect>().FieldHasAttribute<RequiredAttribute>(fieldName), | |
| 802 | + optionValueFieldName, optionLabelFieldName, optionSortFieldName, fieldLabel, fieldID, groupDataBinding, labelDataBinding, customDataBinding, dependencyFieldName, optionDataBinding, toolTip, initialFocus, restriction); | |
| 803 | + } | |
| 804 | + | |
| 805 | + /// <summary> | |
| 806 | + /// Generates template based select field based on specified parameters. | |
| 807 | + /// </summary> | |
| 808 | + /// <typeparam name="TOption">Modeled table for option data.</typeparam> | |
| 809 | + /// <param name="fieldName">Field name for value of select field.</param> | |
| 810 | + /// <param name="required">Determines if field name is required.</param> | |
| 811 | + /// <param name="optionValueFieldName">Field name for ID of option data.</param> | |
| 812 | + /// <param name="optionLabelFieldName">Field name for label of option data, defaults to <paramref name="optionValueFieldName"/></param> | |
| 813 | + /// <param name="optionSortFieldName">Field name for sort order of option data, defaults to <paramref name="optionLabelFieldName"/></param> | |
| 814 | + /// <param name="fieldLabel">Label name for select field, defaults to <paramref name="fieldName"/>.</param> | |
| 815 | + /// <param name="fieldID">ID to use for select field; defaults to select + <paramref name="fieldName"/>.</param> | |
| 816 | + /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param> | |
| 817 | + /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param> | |
| 818 | + /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param> | |
| 819 | + /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param> | |
| 820 | + /// <param name="optionDataBinding">Data-bind operations to apply to each option value, if any.</param> | |
| 821 | + /// <param name="toolTip">Tool tip text to apply to field, if any.</param> | |
| 822 | + /// <param name="initialFocus">Use field for initial focus.</param> | |
| 823 | + /// <param name="restriction">Record restriction to apply, if any.</param> | |
| 824 | + /// <returns>Generated HTML for new text field based on specified parameters.</returns> | |
| 825 | + public string AddSelectField<TOption>(string fieldName, bool required, string optionValueFieldName, string optionLabelFieldName = null, string optionSortFieldName = null, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string optionDataBinding = null, string toolTip = null, bool initialFocus = false, RecordRestriction restriction = null) where TOption : class, new() | |
| 826 | + { | |
| 827 | + RazorView<TLanguage> addSelectFieldTemplate = new RazorView<TLanguage>(AddSelectFieldTemplate, m_exceptionHandler); | |
| 828 | + DynamicViewBag viewBag = addSelectFieldTemplate.ViewBag; | |
| 829 | + TableOperations<TOption> optionTableOperations = Table<TOption>(); | |
| 830 | + Dictionary<string, string> options = new Dictionary<string, string>(); | |
| 831 | + string optionTableName = typeof(TOption).Name; | |
| 832 | + | |
| 833 | + optionLabelFieldName = optionLabelFieldName ?? optionValueFieldName; | |
| 834 | + optionSortFieldName = optionSortFieldName ?? optionLabelFieldName; | |
| 835 | + fieldLabel = fieldLabel ?? optionTableName; | |
| 836 | + | |
| 837 | + if (string.IsNullOrEmpty(fieldID)) | |
| 838 | + fieldID = $"select{fieldName}"; | |
| 839 | + | |
| 840 | + if (initialFocus) | |
| 841 | + m_initialFocusField = fieldID; | |
| 842 | + | |
| 843 | + viewBag.AddValue("FieldName", fieldName); | |
| 844 | + viewBag.AddValue("Required", required); | |
| 845 | + viewBag.AddValue("FieldLabel", fieldLabel); | |
| 846 | + viewBag.AddValue("FieldID", fieldID); | |
| 847 | + viewBag.AddValue("GroupDataBinding", groupDataBinding); | |
| 848 | + viewBag.AddValue("LabelDataBinding", labelDataBinding); | |
| 849 | + viewBag.AddValue("CustomDataBinding", customDataBinding); | |
| 850 | + viewBag.AddValue("DependencyFieldName", dependencyFieldName); | |
| 851 | + viewBag.AddValue("ToolTip", toolTip); | |
| 852 | + viewBag.AddValue("OptionDataBinding", optionDataBinding); | |
| 853 | + | |
| 854 | + foreach (TOption record in Table<TOption>().QueryRecords(optionSortFieldName, restriction)) | |
| 855 | + { | |
| 856 | + if (record != null) | |
| 857 | + options.Add(optionTableOperations.GetFieldValue(record, optionValueFieldName).ToString(), optionTableOperations.GetFieldValue(record, optionLabelFieldName).ToNonNullString(fieldLabel)); | |
| 858 | + } | |
| 859 | + | |
| 860 | + viewBag.AddValue("Options", options); | |
| 861 | + | |
| 862 | + return addSelectFieldTemplate.Execute(); | |
| 863 | + } | |
| 864 | + | |
| 865 | + /// <summary> | |
| 866 | + /// Generates template based check box field based on reflected modeled table field attributes. | |
| 867 | + /// </summary> | |
| 868 | + /// <typeparam name="T">Modeled table.</typeparam> | |
| 869 | + /// <param name="fieldName">Field name for value of check box field.</param> | |
| 870 | + /// <param name="fieldLabel">Label name for check box field, defaults to <paramref name="fieldName"/>.</param> | |
| 871 | + /// <param name="fieldID">ID to use for check box field; defaults to check + <paramref name="fieldName"/>.</param> | |
| 872 | + /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param> | |
| 873 | + /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param> | |
| 874 | + /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param> | |
| 875 | + /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param> | |
| 876 | + /// <param name="toolTip">Tool tip text to apply to field, if any.</param> | |
| 877 | + /// <param name="initialFocus">Use field for initial focus.</param> | |
| 878 | + /// <returns>Generated HTML for new check box field based on modeled table field attributes.</returns> | |
| 879 | + public string AddCheckBoxField<T>(string fieldName, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) where T : class, new() | |
| 880 | + { | |
| 881 | + if (string.IsNullOrEmpty(fieldLabel)) | |
| 882 | + { | |
| 883 | + LabelAttribute labelAttribute; | |
| 884 | + | |
| 885 | + if (Table<T>().TryGetFieldAttribute(fieldName, out labelAttribute)) | |
| 886 | + fieldLabel = labelAttribute.Label; | |
| 887 | + } | |
| 888 | + | |
| 889 | + AddFieldValueInitializer<T>(fieldName); | |
| 890 | + | |
| 891 | + return AddCheckBoxField(fieldName, fieldLabel, fieldID, groupDataBinding, labelDataBinding, customDataBinding, dependencyFieldName, toolTip, initialFocus); | |
| 892 | + } | |
| 893 | + | |
| 894 | + /// <summary> | |
| 895 | + /// Generates template based check box field based on specified parameters. | |
| 896 | + /// </summary> | |
| 897 | + /// <param name="fieldName">Field name for value of check box field.</param> | |
| 898 | + /// <param name="fieldLabel">Label name for check box field, defaults to <paramref name="fieldName"/>.</param> | |
| 899 | + /// <param name="fieldID">ID to use for check box field; defaults to check + <paramref name="fieldName"/>.</param> | |
| 900 | + /// <param name="groupDataBinding">Data-bind operations to apply to outer form-group div, if any.</param> | |
| 901 | + /// <param name="labelDataBinding">Data-bind operations to apply to label, if any.</param> | |
| 902 | + /// <param name="customDataBinding">Extra custom data-binding operations to apply to field, if any.</param> | |
| 903 | + /// <param name="dependencyFieldName">Defines default "enabled" subordinate data-bindings based a single boolean field, e.g., a check-box.</param> | |
| 904 | + /// <param name="toolTip">Tool tip text to apply to field, if any.</param> | |
| 905 | + /// <param name="initialFocus">Use field for initial focus.</param> | |
| 906 | + /// <returns>Generated HTML for new check box field based on specified parameters.</returns> | |
| 907 | + public string AddCheckBoxField(string fieldName, string fieldLabel = null, string fieldID = null, string groupDataBinding = null, string labelDataBinding = null, string customDataBinding = null, string dependencyFieldName = null, string toolTip = null, bool initialFocus = false) | |
| 908 | + { | |
| 909 | + RazorView<TLanguage> addCheckBoxFieldTemplate = new RazorView<TLanguage>(AddCheckBoxFieldTemplate, m_exceptionHandler); | |
| 910 | + DynamicViewBag viewBag = addCheckBoxFieldTemplate.ViewBag; | |
| 911 | + | |
| 912 | + if (string.IsNullOrEmpty(fieldID)) | |
| 913 | + fieldID = $"check{fieldName}"; | |
| 914 | + | |
| 915 | + if (initialFocus) | |
| 916 | + m_initialFocusField = fieldID; | |
| 917 | + | |
| 918 | + viewBag.AddValue("FieldName", fieldName); | |
| 919 | + viewBag.AddValue("FieldLabel", fieldLabel ?? fieldName); | |
| 920 | + viewBag.AddValue("FieldID", fieldID); | |
| 921 | + viewBag.AddValue("GroupDataBinding", groupDataBinding); | |
| 922 | + viewBag.AddValue("LabelDataBinding", labelDataBinding); | |
| 923 | + viewBag.AddValue("CustomDataBinding", customDataBinding); | |
| 924 | + viewBag.AddValue("DependencyFieldName", dependencyFieldName); | |
| 925 | + viewBag.AddValue("ToolTip", toolTip); | |
| 926 | + | |
| 927 | + return addCheckBoxFieldTemplate.Execute(); | |
| 928 | + } | |
| 929 | + | |
| 930 | + #endregion | |
| 931 | + | |
| 932 | + #region [ Static ] | |
| 933 | + | |
| 934 | + // Static Methods | |
| 935 | + private static bool IsNumericType(Type type) | |
| 936 | + { | |
| 937 | + return | |
| 938 | + type == typeof(byte) || | |
| 939 | + type == typeof(sbyte) || | |
| 940 | + type == typeof(short) || | |
| 941 | + type == typeof(ushort) || | |
| 942 | + type == typeof(Int24) || | |
| 943 | + type == typeof(UInt24) || | |
| 944 | + type == typeof(int) || | |
| 945 | + type == typeof(uint) || | |
| 946 | + type == typeof(long) || | |
| 947 | + type == typeof(ulong) || | |
| 948 | + type == typeof(float) || | |
| 949 | + type == typeof(double) || | |
| 950 | + type == typeof(decimal); | |
| 951 | + } | |
| 952 | + | |
| 953 | + #endregion | |
| 954 | + } | |
| 955 | +} | |
| @@ -0,0 +1,91 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// RecordOperationsCache.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 01/14/2016 - Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.Collections.Generic; | |
| 26 | +using System.Reflection; | |
| 27 | +using GSF.Collections; | |
| 28 | +using GSF.Data.Model; | |
| 29 | +using GSF.Reflection; | |
| 30 | +using GSF.Web.Security; | |
| 31 | + | |
| 32 | +namespace GSF.Web.Model | |
| 33 | +{ | |
| 34 | + /// <summary> | |
| 35 | + /// Defines class that caches data hub operations. | |
| 36 | + /// </summary> | |
| 37 | + public class RecordOperationsCache | |
| 38 | + { | |
| 39 | + #region [ Members ] | |
| 40 | + | |
| 41 | + // Fields | |
| 42 | + private readonly Dictionary<Type, Tuple<string, string>[]> m_recordOperations; | |
| 43 | + | |
| 44 | + #endregion | |
| 45 | + | |
| 46 | + #region [ Constructors ] | |
| 47 | + | |
| 48 | + /// <summary> | |
| 49 | + /// Creates a new <see cref="RecordOperationsCache"/>. | |
| 50 | + /// </summary> | |
| 51 | + /// <param name="hub">Hub to analyze for record operations.</param> | |
| 52 | + public RecordOperationsCache(Type hub) | |
| 53 | + { | |
| 54 | + int recordOperations = Enum.GetValues(typeof(RecordOperation)).Length; | |
| 55 | + m_recordOperations = new Dictionary<Type, Tuple<string, string>[]>(); | |
| 56 | + | |
| 57 | + // Analyze and cache data hub methods that are targeted for record operations | |
| 58 | + foreach (MethodInfo method in hub.GetMethods(BindingFlags.Public | BindingFlags.Instance)) | |
| 59 | + { | |
| 60 | + AuthorizeHubRoleAttribute authorizeHubRoleAttribute; | |
| 61 | + RecordOperationAttribute recordOperationAttribute; | |
| 62 | + | |
| 63 | + method.TryGetAttribute(out authorizeHubRoleAttribute); | |
| 64 | + | |
| 65 | + if (method.TryGetAttribute(out recordOperationAttribute)) | |
| 66 | + { | |
| 67 | + // Cache method name and any defined authorized roles for current record operation | |
| 68 | + m_recordOperations.GetOrAdd(recordOperationAttribute.ModelType, | |
| 69 | + type => new Tuple<string, string>[recordOperations])[(int)recordOperationAttribute.Operation] = | |
| 70 | + new Tuple<string, string>(method.Name, authorizeHubRoleAttribute?.Roles); | |
| 71 | + } | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 75 | + #endregion | |
| 76 | + | |
| 77 | + #region [ Methods ] | |
| 78 | + | |
| 79 | + /// <summary> | |
| 80 | + /// Gets record operation methods for specified modeled table. | |
| 81 | + /// </summary> | |
| 82 | + /// <typeparam name="T">Modeled table.</typeparam> | |
| 83 | + /// <returns>record operation methods for specified modeled table.</returns> | |
| 84 | + public Tuple<string, string>[] GetRecordOperations<T>() where T : class, new() | |
| 85 | + { | |
| 86 | + return m_recordOperations[typeof(T)]; | |
| 87 | + } | |
| 88 | + | |
| 89 | + #endregion | |
| 90 | + } | |
| 91 | +} | |
| @@ -0,0 +1,667 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// gsf.web.client.js - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 01/26/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | +// ReSharper disable NativeTypePrototypeExtending | |
| 24 | + | |
| 25 | +// Grid Solutions Framework Core Web Client Script Functions | |
| 26 | +"use strict"; | |
| 27 | + | |
| 28 | +const isIE = detectIE(); | |
| 29 | +var textMetrics; | |
| 30 | + | |
| 31 | +$(function () { | |
| 32 | + // Create a canvas object that will be used for text metrics calculations | |
| 33 | + $("<canvas id=\"textMetricsCanvas\" height=\"1px\" width=\"1px\" style=\"visibility: hidden\"></canvas>").appendTo("body"); | |
| 34 | + | |
| 35 | + // Get text metrics canvas context | |
| 36 | + textMetrics = document.getElementById("textMetricsCanvas").getContext("2d"); | |
| 37 | +}); | |
| 38 | + | |
| 39 | +// Miscellaneous functions | |
| 40 | +function notNull(value, nonNullValue) { | |
| 41 | + return value || (nonNullValue || ""); | |
| 42 | +} | |
| 43 | + | |
| 44 | +function detectIE() { | |
| 45 | + const ua = window.navigator.userAgent; | |
| 46 | + const msie = ua.indexOf("MSIE "); | |
| 47 | + | |
| 48 | + if (msie > 0) { | |
| 49 | + // IE 10 or older => return version number | |
| 50 | + return parseInt(ua.substring(msie + 5, ua.indexOf(".", msie)), 10); | |
| 51 | + } | |
| 52 | + | |
| 53 | + const trident = ua.indexOf("Trident/"); | |
| 54 | + | |
| 55 | + if (trident > 0) { | |
| 56 | + // IE 11 => return version number | |
| 57 | + const rv = ua.indexOf("rv:"); | |
| 58 | + return parseInt(ua.substring(rv + 3, ua.indexOf(".", rv)), 10); | |
| 59 | + } | |
| 60 | + | |
| 61 | + const edge = ua.indexOf("Edge/"); | |
| 62 | + | |
| 63 | + if (edge > 0) { | |
| 64 | + // Edge (IE 12+) => return version number | |
| 65 | + return parseInt(ua.substring(edge + 5, ua.indexOf(".", edge)), 10); | |
| 66 | + } | |
| 67 | + | |
| 68 | + // Other browser | |
| 69 | + return false; | |
| 70 | +} | |
| 71 | + | |
| 72 | +function isNumber(val) { | |
| 73 | + return !isNaN(parseFloat(val)) && isFinite(val); | |
| 74 | +} | |
| 75 | + | |
| 76 | +function isBool(val) { | |
| 77 | + if (typeof value === "boolean") | |
| 78 | + return true; | |
| 79 | + | |
| 80 | + const lval = val.toString().toLowerCase(); | |
| 81 | + return lval === "true" || lval === "false"; | |
| 82 | +} | |
| 83 | + | |
| 84 | +function getBool(val) { | |
| 85 | + const num = +val; | |
| 86 | + return !isNaN(num) ? !!num : !!String(val).toLowerCase().replace(false, ""); | |
| 87 | +} | |
| 88 | + | |
| 89 | +// Number functions | |
| 90 | +Number.prototype.truncate = function() { | |
| 91 | + if (typeof Math.trunc != "function") | |
| 92 | + return parseInt(this.toString()); | |
| 93 | + | |
| 94 | + return Math.trunc(this); | |
| 95 | +} | |
| 96 | + | |
| 97 | +Number.prototype.padLeft = function (totalWidth, paddingChar) { | |
| 98 | + return this.truncate().toString().padLeft(totalWidth, paddingChar || "0"); | |
| 99 | +} | |
| 100 | + | |
| 101 | +Number.prototype.padRight = function (totalWidth, paddingChar) { | |
| 102 | + return this.truncate().toString().padRight(totalWidth, paddingChar || "0"); | |
| 103 | +} | |
| 104 | + | |
| 105 | +// Array functions | |
| 106 | + | |
| 107 | +// Combines a dictionary of key-value pairs into a string. Values will be escaped within startValueDelimiter and endValueDelimiter | |
| 108 | +// to contain nested key/value pair expressions like the following: "normalKVP=-1; nestedKVP={p1=true; p2=0.001}" when either the | |
| 109 | +// parameterDelimiter or the keyValueDelimiter are detected in the value of the key/value pair. | |
| 110 | +function joinKeyValuePairs (source, parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) { | |
| 111 | + if (!parameterDelimiter) | |
| 112 | + parameterDelimiter = ";"; | |
| 113 | + | |
| 114 | + if (!keyValueDelimiter) | |
| 115 | + keyValueDelimiter = "="; | |
| 116 | + | |
| 117 | + if (!startValueDelimiter) | |
| 118 | + startValueDelimiter = "{"; | |
| 119 | + | |
| 120 | + if (!endValueDelimiter) | |
| 121 | + endValueDelimiter = "}"; | |
| 122 | + | |
| 123 | + const values = []; | |
| 124 | + | |
| 125 | + for (var key in source) { | |
| 126 | + if (source.hasOwnProperty(key)) { | |
| 127 | + var value = source[key]; | |
| 128 | + | |
| 129 | + if (isBool(value)) | |
| 130 | + value = value.toString().toLowerCase(); | |
| 131 | + else | |
| 132 | + value = value ? value.toString() : ""; | |
| 133 | + | |
| 134 | + if (value.indexOf(parameterDelimiter) >= 0 || value.indexOf(keyValueDelimiter) >= 0) | |
| 135 | + value = startValueDelimiter + value + endValueDelimiter; | |
| 136 | + | |
| 137 | + values.push(key + keyValueDelimiter + value); | |
| 138 | + } | |
| 139 | + } | |
| 140 | + | |
| 141 | + return values.join(parameterDelimiter + " "); | |
| 142 | +}; | |
| 143 | + | |
| 144 | +Array.prototype.joinKeyValuePairs = function (parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) { | |
| 145 | + return joinKeyValuePairs(this, parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter); | |
| 146 | +}; | |
| 147 | + | |
| 148 | +// Represents a dictionary style class with case-insensitive keys | |
| 149 | +function Dictionary(source) { | |
| 150 | + const self = this; | |
| 151 | + | |
| 152 | + if (!source) | |
| 153 | + source = []; | |
| 154 | + | |
| 155 | + self._keys = []; | |
| 156 | + self._values = []; | |
| 157 | + | |
| 158 | + self.count = function () { | |
| 159 | + var size = 0; | |
| 160 | + | |
| 161 | + for (var property in self._values) { | |
| 162 | + if (self._values.hasOwnProperty(property)) | |
| 163 | + size++; | |
| 164 | + } | |
| 165 | + | |
| 166 | + return size; | |
| 167 | + } | |
| 168 | + | |
| 169 | + self.keys = function () { | |
| 170 | + const keys = []; | |
| 171 | + | |
| 172 | + for (var property in self._keys) { | |
| 173 | + if (self._keys.hasOwnProperty(property)) | |
| 174 | + keys.push(self._keys[property]); | |
| 175 | + } | |
| 176 | + | |
| 177 | + return keys; | |
| 178 | + } | |
| 179 | + | |
| 180 | + self.values = function () { | |
| 181 | + const values = []; | |
| 182 | + | |
| 183 | + for (var property in self._values) { | |
| 184 | + if (self._keys.hasOwnProperty(property)) | |
| 185 | + values.push(self._values[property]); | |
| 186 | + } | |
| 187 | + | |
| 188 | + return values; | |
| 189 | + } | |
| 190 | + | |
| 191 | + self.get = function (key) { | |
| 192 | + return self._values[String(key).toLowerCase()]; | |
| 193 | + } | |
| 194 | + | |
| 195 | + self.set = function (key, value) { | |
| 196 | + const lkey = String(key).toLowerCase(); | |
| 197 | + | |
| 198 | + if (!self._keys[lkey] || self._keys[lkey].toLowerCase() !== key) | |
| 199 | + self._keys[lkey] = key; | |
| 200 | + | |
| 201 | + if (isBool(value)) | |
| 202 | + self._values[lkey] = getBool(value); | |
| 203 | + else | |
| 204 | + self._values[lkey] = value; | |
| 205 | + } | |
| 206 | + | |
| 207 | + self.remove = function (key) { | |
| 208 | + const lkey = String(key).toLowerCase(); | |
| 209 | + delete self._keys[lkey]; | |
| 210 | + delete self._values[lkey]; | |
| 211 | + } | |
| 212 | + | |
| 213 | + self.containsKey = function (key) { | |
| 214 | + const lkey = String(key).toLowerCase(); | |
| 215 | + | |
| 216 | + for (var property in self._values) { | |
| 217 | + if (self._values.hasOwnProperty(property) && property === lkey) | |
| 218 | + return true; | |
| 219 | + } | |
| 220 | + | |
| 221 | + return false; | |
| 222 | + } | |
| 223 | + | |
| 224 | + self.containsValue = function (value) { | |
| 225 | + for (var property in self._values) { | |
| 226 | + if (self._values.hasOwnProperty(property) && self._values[property] === value) | |
| 227 | + return true; | |
| 228 | + } | |
| 229 | + | |
| 230 | + return false; | |
| 231 | + } | |
| 232 | + | |
| 233 | + self.clear = function () { | |
| 234 | + self._keys = []; | |
| 235 | + self._values = []; | |
| 236 | + } | |
| 237 | + | |
| 238 | + self.joinKeyValuePairs = function (parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) { | |
| 239 | + const keyValuePairs = []; | |
| 240 | + | |
| 241 | + for (var property in self._values) { | |
| 242 | + if (self._values.hasOwnProperty(property)) | |
| 243 | + keyValuePairs[self._keys[property]] = self._values[property]; | |
| 244 | + } | |
| 245 | + | |
| 246 | + return keyValuePairs.joinKeyValuePairs(parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter); | |
| 247 | + } | |
| 248 | + | |
| 249 | + self.pushAll = function (source) { | |
| 250 | + for (var property in source) | |
| 251 | + if (source.hasOwnProperty(property)) | |
| 252 | + self.set(property, source[property]); | |
| 253 | + } | |
| 254 | + | |
| 255 | + self.toObservableDictionary = function (useLowerKeys) { | |
| 256 | + // See ko.observableDictionary.js | |
| 257 | + const observableDictionary = new ko.observableDictionary(); | |
| 258 | + | |
| 259 | + for (var property in self._values) { | |
| 260 | + if (self._values.hasOwnProperty(property)) | |
| 261 | + observableDictionary.push(useLowerKeys ? property : self._keys[property], self._values[property]); | |
| 262 | + } | |
| 263 | + | |
| 264 | + return observableDictionary; | |
| 265 | + } | |
| 266 | + | |
| 267 | + self.updateObservableDictionary = function (observableDictionary, useLowerKeys) { | |
| 268 | + for (var property in self._values) { | |
| 269 | + if (self._values.hasOwnProperty(property)) | |
| 270 | + observableDictionary.set(useLowerKeys ? property : self._keys[property], self._values[property]); | |
| 271 | + } | |
| 272 | + } | |
| 273 | + | |
| 274 | + // Construction | |
| 275 | + if (source instanceof Dictionary) { | |
| 276 | + for (var property in source._values) | |
| 277 | + if (source._values.hasOwnProperty(property)) | |
| 278 | + self.set(source._keys[property], source._values[property]); | |
| 279 | + } | |
| 280 | + else { | |
| 281 | + for (var property in source) { | |
| 282 | + if (source.hasOwnProperty(property)) | |
| 283 | + self.set(property, source[property]); | |
| 284 | + } | |
| 285 | + } | |
| 286 | +} | |
| 287 | + | |
| 288 | +Dictionary.fromObservableDictionary = function (observableDictionary) { | |
| 289 | + var dictionary = new Dictionary(); | |
| 290 | + dictionary.pushAll(observableDictionary.toJSON()); | |
| 291 | + return dictionary; | |
| 292 | +} | |
| 293 | + | |
| 294 | +// String functions | |
| 295 | +function isEmpty(str) { | |
| 296 | + return !str || String(str).length === 0; | |
| 297 | +} | |
| 298 | + | |
| 299 | +String.prototype.truncate = function (limit) { | |
| 300 | + const text = this.trim(); | |
| 301 | + | |
| 302 | + if (text.length > limit) | |
| 303 | + return text.substr(0, limit - 3) + "..."; | |
| 304 | + | |
| 305 | + return text; | |
| 306 | +} | |
| 307 | + | |
| 308 | +String.prototype.replaceAll = function (findText, replaceWith, ignoreCase) { | |
| 309 | + return this.replace( | |
| 310 | + new RegExp(findText.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g, "\\$&"), (ignoreCase ? "gi" : "g")), | |
| 311 | + (typeof replaceWith == "string") ? replaceWith.replace(/\$/g, "$$$$") : replaceWith); | |
| 312 | +} | |
| 313 | + | |
| 314 | +String.prototype.padLeft = function (totalWidth, paddingChar) { | |
| 315 | + if (totalWidth > this.length) | |
| 316 | + return Array(totalWidth - this.length + 1).join(paddingChar || " ") + this; | |
| 317 | + | |
| 318 | + return this; | |
| 319 | +} | |
| 320 | + | |
| 321 | +String.prototype.padRight = function (totalWidth, paddingChar) { | |
| 322 | + if (totalWidth > this.length) | |
| 323 | + return this + Array(totalWidth - this.length + 1).join(paddingChar || " "); | |
| 324 | + | |
| 325 | + return this; | |
| 326 | +} | |
| 327 | + | |
| 328 | +String.prototype.countOccurrences = function (searchString) { | |
| 329 | + return (this.split(searchString).length - 1); | |
| 330 | +} | |
| 331 | + | |
| 332 | +if (!String.prototype.startsWith) { | |
| 333 | + String.prototype.startsWith = function (searchString, position) { | |
| 334 | + position = position || 0; | |
| 335 | + return this.substr(position, searchString.length) === searchString; | |
| 336 | + } | |
| 337 | +} | |
| 338 | + | |
| 339 | +String.prototype.regexEncode = function () { | |
| 340 | + return "\\u" + this.charCodeAt(0).toString(16).padLeft(4, "0"); | |
| 341 | +} | |
| 342 | + | |
| 343 | +// Returns a Dictionary of the parsed key/value pair expressions from a string. Parameter pairs are delimited by keyValueDelimiter | |
| 344 | +// and multiple pairs separated by parameterDelimiter. Supports encapsulated nested expressions. | |
| 345 | +String.prototype.parseKeyValuePairs = function (parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter, ignoreDuplicateKeys) { | |
| 346 | + if (!parameterDelimiter) | |
| 347 | + parameterDelimiter = ";"; | |
| 348 | + | |
| 349 | + if (!keyValueDelimiter) | |
| 350 | + keyValueDelimiter = "="; | |
| 351 | + | |
| 352 | + if (!startValueDelimiter) | |
| 353 | + startValueDelimiter = "{"; | |
| 354 | + | |
| 355 | + if (!endValueDelimiter) | |
| 356 | + endValueDelimiter = "}"; | |
| 357 | + | |
| 358 | + if (ignoreDuplicateKeys === undefined) | |
| 359 | + ignoreDuplicateKeys = true; | |
| 360 | + | |
| 361 | + if (parameterDelimiter === keyValueDelimiter || | |
| 362 | + parameterDelimiter === startValueDelimiter || | |
| 363 | + parameterDelimiter === endValueDelimiter || | |
| 364 | + keyValueDelimiter === startValueDelimiter || | |
| 365 | + keyValueDelimiter === endValueDelimiter || | |
| 366 | + startValueDelimiter === endValueDelimiter) | |
| 367 | + throw "All delimiters must be unique"; | |
| 368 | + | |
| 369 | + const escapedParameterDelimiter = parameterDelimiter.regexEncode(); | |
| 370 | + const escapedKeyValueDelimiter = keyValueDelimiter.regexEncode(); | |
| 371 | + const escapedStartValueDelimiter = startValueDelimiter.regexEncode(); | |
| 372 | + const escapedEndValueDelimiter = endValueDelimiter.regexEncode(); | |
| 373 | + const backslashDelimiter = "\\".regexEncode(); | |
| 374 | + | |
| 375 | + var keyValuePairs = new Dictionary(); | |
| 376 | + var escapedValue = []; | |
| 377 | + var valueEscaped = false; | |
| 378 | + var delimiterDepth = 0; | |
| 379 | + | |
| 380 | + // Escape any parameter or key/value delimiters within tagged value sequences | |
| 381 | + // For example, the following string: | |
| 382 | + // "normalKVP=-1; nestedKVP={p1=true; p2=false}") | |
| 383 | + // would be encoded as: | |
| 384 | + // "normalKVP=-1; nestedKVP=p1\\u003dtrue\\u003b p2\\u003dfalse") | |
| 385 | + for (var i = 0; i < this.length; i++) { | |
| 386 | + var character = this[i]; | |
| 387 | + | |
| 388 | + if (character === startValueDelimiter) { | |
| 389 | + if (!valueEscaped) { | |
| 390 | + valueEscaped = true; | |
| 391 | + continue; // Don't add tag start delimiter to final value | |
| 392 | + } | |
| 393 | + | |
| 394 | + // Handle nested delimiters | |
| 395 | + delimiterDepth++; | |
| 396 | + } | |
| 397 | + | |
| 398 | + if (character === endValueDelimiter) { | |
| 399 | + if (valueEscaped) { | |
| 400 | + if (delimiterDepth > 0) { | |
| 401 | + // Handle nested delimiters | |
| 402 | + delimiterDepth--; | |
| 403 | + } | |
| 404 | + else { | |
| 405 | + valueEscaped = false; | |
| 406 | + continue; // Don't add tag stop delimiter to final value | |
| 407 | + } | |
| 408 | + } | |
| 409 | + else { | |
| 410 | + throw "Failed to parse key/value pairs: invalid delimiter mismatch. Encountered end value delimiter \"" + endValueDelimiter + "\" before start value delimiter \"" + startValueDelimiter + "\"."; | |
| 411 | + } | |
| 412 | + } | |
| 413 | + | |
| 414 | + if (valueEscaped) { | |
| 415 | + // Escape any delimiter characters inside nested key/value pair | |
| 416 | + if (character === parameterDelimiter) | |
| 417 | + escapedValue.push(escapedParameterDelimiter); | |
| 418 | + else if (character === keyValueDelimiter) | |
| 419 | + escapedValue.push(escapedKeyValueDelimiter); | |
| 420 | + else if (character === startValueDelimiter) | |
| 421 | + escapedValue.push(escapedStartValueDelimiter); | |
| 422 | + else if (character === endValueDelimiter) | |
| 423 | + escapedValue.push(escapedEndValueDelimiter); | |
| 424 | + else if (character === "\\") | |
| 425 | + escapedValue.push(backslashDelimiter); | |
| 426 | + else | |
| 427 | + escapedValue.push(character); | |
| 428 | + } | |
| 429 | + else { | |
| 430 | + if (character === "\\") | |
| 431 | + escapedValue.push(backslashDelimiter); | |
| 432 | + else | |
| 433 | + escapedValue.push(character); | |
| 434 | + } | |
| 435 | + } | |
| 436 | + | |
| 437 | + if (delimiterDepth !== 0 || valueEscaped) { | |
| 438 | + // If value is still escaped, tagged expression was not terminated | |
| 439 | + if (valueEscaped) | |
| 440 | + delimiterDepth = 1; | |
| 441 | + | |
| 442 | + throw "Failed to parse key/value pairs: invalid delimiter mismatch. Encountered more " + | |
| 443 | + (delimiterDepth > 0 ? "start value delimiters \"" + startValueDelimiter + "\"" : "end value delimiters \"" + endValueDelimiter + "\"") + " than " + | |
| 444 | + (delimiterDepth < 0 ? "start value delimiters \"" + startValueDelimiter + "\"" : "end value delimiters \"" + endValueDelimiter + "\"") + "."; | |
| 445 | + } | |
| 446 | + | |
| 447 | + // Parse key/value pairs from escaped value | |
| 448 | + var pairs = escapedValue.join("").split(parameterDelimiter); | |
| 449 | + | |
| 450 | + for (var i = 0; i < pairs.length; i++) { | |
| 451 | + // Separate key from value | |
| 452 | + var elements = pairs[i].split(keyValueDelimiter); | |
| 453 | + | |
| 454 | + if (elements.length === 2) { | |
| 455 | + // Get key | |
| 456 | + var key = elements[0].trim(); | |
| 457 | + | |
| 458 | + // Get unescaped value | |
| 459 | + var unescapedValue = elements[1].trim(). | |
| 460 | + replaceAll(escapedParameterDelimiter, parameterDelimiter). | |
| 461 | + replaceAll(escapedKeyValueDelimiter, keyValueDelimiter). | |
| 462 | + replaceAll(escapedStartValueDelimiter, startValueDelimiter). | |
| 463 | + replaceAll(escapedEndValueDelimiter, endValueDelimiter). | |
| 464 | + replaceAll(backslashDelimiter, "\\"); | |
| 465 | + | |
| 466 | + // Add key/value pair to dictionary | |
| 467 | + if (ignoreDuplicateKeys) { | |
| 468 | + // Add or replace key elements with unescaped value | |
| 469 | + keyValuePairs.set(key, unescapedValue); | |
| 470 | + } | |
| 471 | + else { | |
| 472 | + // Add key elements with unescaped value throwing an exception for encountered duplicate keys | |
| 473 | + if (keyValuePairs.containsKey(key)) | |
| 474 | + throw "Failed to parse key/value pairs: duplicate key encountered. Key \"" + key + "\" is not unique within the string: \"" + this + "\""; | |
| 475 | + | |
| 476 | + keyValuePairs.set(key, unescapedValue); | |
| 477 | + } | |
| 478 | + } | |
| 479 | + } | |
| 480 | + | |
| 481 | + return keyValuePairs; | |
| 482 | +} | |
| 483 | + | |
| 484 | +// Date Functions | |
| 485 | +Date.prototype.addDays = function (days) { | |
| 486 | + return new Date(this.setDate(this.getDate() + days)); | |
| 487 | +} | |
| 488 | + | |
| 489 | +Date.prototype.toUTC = function () { | |
| 490 | + this.setMinutes(this.getMinutes() - this.getTimezoneOffset()); | |
| 491 | + return this; | |
| 492 | +} | |
| 493 | + | |
| 494 | +Date.prototype.daysBetween = function (startDate) { | |
| 495 | + const millisecondsPerDay = 24 * 60 * 60 * 1000; | |
| 496 | + return (this.toUTC() - startDate.toUTC()) / millisecondsPerDay; | |
| 497 | +} | |
| 498 | + | |
| 499 | +String.prototype.toDate = function () { | |
| 500 | + return new Date(Date.parse(this)); | |
| 501 | +}; | |
| 502 | + | |
| 503 | +String.prototype.formatDate = function (format, utc) { | |
| 504 | + return formatDate(this.toDate(), format, utc); | |
| 505 | +}; | |
| 506 | + | |
| 507 | +Date.prototype.formatDate = function (format, utc) { | |
| 508 | + return formatDate(this, format, utc); | |
| 509 | +}; | |
| 510 | + | |
| 511 | +function formatDate(date, format, utc) { | |
| 512 | + if (typeof date === "string") | |
| 513 | + return formatDate(date.toDate(), format, utc); | |
| 514 | + | |
| 515 | + if (format === undefined && DateTimeFormat != undefined) | |
| 516 | + format = DateTimeFormat; | |
| 517 | + | |
| 518 | + if (utc === undefined) | |
| 519 | + utc = true; | |
| 520 | + | |
| 521 | + var MMMM = ["\x00", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; | |
| 522 | + var MMM = ["\x01", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | |
| 523 | + var dddd = ["\x02", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; | |
| 524 | + var ddd = ["\x03", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; | |
| 525 | + | |
| 526 | + function ii(i, len) { | |
| 527 | + var ss = i + ""; | |
| 528 | + len = len || 2; | |
| 529 | + while (ss.length < len) ss = "0" + ss; | |
| 530 | + return ss; | |
| 531 | + } | |
| 532 | + | |
| 533 | + var y = utc ? date.getUTCFullYear() : date.getFullYear(); | |
| 534 | + format = format.replace(/(^|[^\\])yyyy+/g, "$1" + y); | |
| 535 | + format = format.replace(/(^|[^\\])yy/g, "$1" + y.toString().substr(2, 2)); | |
| 536 | + format = format.replace(/(^|[^\\])y/g, "$1" + y); | |
| 537 | + | |
| 538 | + var M = (utc ? date.getUTCMonth() : date.getMonth()) + 1; | |
| 539 | + format = format.replace(/(^|[^\\])MMMM+/g, "$1" + MMMM[0]); | |
| 540 | + format = format.replace(/(^|[^\\])MMM/g, "$1" + MMM[0]); | |
| 541 | + format = format.replace(/(^|[^\\])MM/g, "$1" + ii(M)); | |
| 542 | + format = format.replace(/(^|[^\\])M/g, "$1" + M); | |
| 543 | + | |
| 544 | + var d = utc ? date.getUTCDate() : date.getDate(); | |
| 545 | + format = format.replace(/(^|[^\\])dddd+/g, "$1" + dddd[0]); | |
| 546 | + format = format.replace(/(^|[^\\])ddd/g, "$1" + ddd[0]); | |
| 547 | + format = format.replace(/(^|[^\\])dd/g, "$1" + ii(d)); | |
| 548 | + format = format.replace(/(^|[^\\])d/g, "$1" + d); | |
| 549 | + | |
| 550 | + var H = utc ? date.getUTCHours() : date.getHours(); | |
| 551 | + format = format.replace(/(^|[^\\])HH+/g, "$1" + ii(H)); | |
| 552 | + format = format.replace(/(^|[^\\])H/g, "$1" + H); | |
| 553 | + | |
| 554 | + var h = H > 12 ? H - 12 : H === 0 ? 12 : H; | |
| 555 | + format = format.replace(/(^|[^\\])hh+/g, "$1" + ii(h)); | |
| 556 | + format = format.replace(/(^|[^\\])h/g, "$1" + h); | |
| 557 | + | |
| 558 | + var m = utc ? date.getUTCMinutes() : date.getMinutes(); | |
| 559 | + format = format.replace(/(^|[^\\])mm+/g, "$1" + ii(m)); | |
| 560 | + format = format.replace(/(^|[^\\])m/g, "$1" + m); | |
| 561 | + | |
| 562 | + var s = utc ? date.getUTCSeconds() : date.getSeconds(); | |
| 563 | + format = format.replace(/(^|[^\\])ss+/g, "$1" + ii(s)); | |
| 564 | + format = format.replace(/(^|[^\\])s/g, "$1" + s); | |
| 565 | + | |
| 566 | + var f = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); | |
| 567 | + format = format.replace(/(^|[^\\])fff+/g, "$1" + ii(f, 3)); | |
| 568 | + f = Math.round(f / 10); | |
| 569 | + format = format.replace(/(^|[^\\])ff/g, "$1" + ii(f)); | |
| 570 | + f = Math.round(f / 10); | |
| 571 | + format = format.replace(/(^|[^\\])f/g, "$1" + f); | |
| 572 | + | |
| 573 | + var T = H < 12 ? "AM" : "PM"; | |
| 574 | + format = format.replace(/(^|[^\\])TT+/g, "$1" + T); | |
| 575 | + format = format.replace(/(^|[^\\])T/g, "$1" + T.charAt(0)); | |
| 576 | + | |
| 577 | + var t = T.toLowerCase(); | |
| 578 | + format = format.replace(/(^|[^\\])tt+/g, "$1" + t); | |
| 579 | + format = format.replace(/(^|[^\\])t/g, "$1" + t.charAt(0)); | |
| 580 | + | |
| 581 | + var tz = -date.getTimezoneOffset(); | |
| 582 | + var K = utc || !tz ? "Z" : tz > 0 ? "+" : "-"; | |
| 583 | + if (!utc) { | |
| 584 | + tz = Math.abs(tz); | |
| 585 | + var tzHrs = Math.floor(tz / 60); | |
| 586 | + var tzMin = tz % 60; | |
| 587 | + K += ii(tzHrs) + ":" + ii(tzMin); | |
| 588 | + } | |
| 589 | + | |
| 590 | + format = format.replace(/(^|[^\\])K/g, "$1" + K); | |
| 591 | + var day = (utc ? date.getUTCDay() : date.getDay()) + 1; | |
| 592 | + | |
| 593 | + format = format.replace(new RegExp(dddd[0], "g"), dddd[day]); | |
| 594 | + format = format.replace(new RegExp(ddd[0], "g"), ddd[day]); | |
| 595 | + | |
| 596 | + format = format.replace(new RegExp(MMMM[0], "g"), MMMM[M]); | |
| 597 | + format = format.replace(new RegExp(MMM[0], "g"), MMM[M]); | |
| 598 | + | |
| 599 | + format = format.replace(/\\(.)/g, "$1"); | |
| 600 | + | |
| 601 | + return format; | |
| 602 | +}; | |
| 603 | + | |
| 604 | +// jQuery extensions | |
| 605 | +$.fn.paddingHeight = function () { | |
| 606 | + return this.outerHeight(true) - this.height(); | |
| 607 | +} | |
| 608 | + | |
| 609 | +$.fn.paddingWidth = function () { | |
| 610 | + return this.outerWidth(true) - this.width(); | |
| 611 | +} | |
| 612 | + | |
| 613 | +// Cell truncations should only be used with .table-cell-hard-wrap style | |
| 614 | +$.fn.truncateToWidth = function (text, rows) { | |
| 615 | + if (isEmpty(text)) | |
| 616 | + return ""; | |
| 617 | + | |
| 618 | + if (!rows) | |
| 619 | + rows = 1; | |
| 620 | + | |
| 621 | + textMetrics.font = this.css("font"); | |
| 622 | + | |
| 623 | + var targetWidth = this.innerWidth(); | |
| 624 | + var textWidth = textMetrics.measureText(text).width; | |
| 625 | + | |
| 626 | + if (rows > 1) | |
| 627 | + targetWidth *= ((isIE ? 0.45 : 0.75) * rows); | |
| 628 | + | |
| 629 | + var limit = Math.min(text.length, Math.ceil(targetWidth / (textWidth / text.length))); | |
| 630 | + | |
| 631 | + while (textWidth > targetWidth && limit > 1) { | |
| 632 | + limit--; | |
| 633 | + text = text.truncate(limit); | |
| 634 | + textWidth = textMetrics.measureText(text).width; | |
| 635 | + } | |
| 636 | + | |
| 637 | + return text; | |
| 638 | +} | |
| 639 | + | |
| 640 | +$.fn.visible = function () { | |
| 641 | + return this.css("visibility", "visible"); | |
| 642 | +} | |
| 643 | + | |
| 644 | +$.fn.invisible = function () { | |
| 645 | + return this.css("visibility", "hidden"); | |
| 646 | +} | |
| 647 | + | |
| 648 | +// The following target arrays of promises | |
| 649 | +$.fn.whenAny = function () { | |
| 650 | + var finish = $.Deferred(); | |
| 651 | + | |
| 652 | + if (this.length === 0) | |
| 653 | + finish.resolve(); | |
| 654 | + else | |
| 655 | + $.each(this, function (index, deferred) { | |
| 656 | + deferred.done(finish.resolve); | |
| 657 | + }); | |
| 658 | + | |
| 659 | + return finish.promise(); | |
| 660 | +} | |
| 661 | + | |
| 662 | +$.fn.whenAll = function () { | |
| 663 | + if (this.length > 0) | |
| 664 | + return $.when.apply($, this); | |
| 665 | + | |
| 666 | + return $.Deferred().resolve().promise(); | |
| 667 | +} | |
| @@ -0,0 +1,568 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// pagedViewModel.js - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 01/23/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +// Paged view model base class scripts | |
| 25 | +"use strict"; | |
| 26 | + | |
| 27 | +var RecordMode = { | |
| 28 | + View: 0, | |
| 29 | + Edit: 1, | |
| 30 | + AddNew: 2 | |
| 31 | +} | |
| 32 | + | |
| 33 | +// Define paged view model class | |
| 34 | +function PagedViewModel() { | |
| 35 | + const self = this; | |
| 36 | + | |
| 37 | + // Configuration fields | |
| 38 | + self.labelField = "{name}"; // Field name that best represents row for delete confirmation | |
| 39 | + self.primaryKeyFields = ["{id}"]; // Primary key field names array | |
| 40 | + self.defaultSortField = "{id}"; // Default sort field | |
| 41 | + self.defaultSortAscending = true; // Default sort ascending flag | |
| 42 | + self.initialFocusField = ""; // Initial add/edit field with focus | |
| 43 | + self.modelName = "{name}"; // Name of model used for cookie names, defaults to page title | |
| 44 | + | |
| 45 | + // Observable fields | |
| 46 | + self.pageRecords = ko.observableArray(); // Records queried for current page | |
| 47 | + self.recordCount = ko.observable(0); // Queried record count | |
| 48 | + self.sortField = ko.observable(self.defaultSortField); // Current sort field, persisted per model | |
| 49 | + self.sortAscending = ko.observable(self.defaultSortAscending); // Current sort ascending flag, persisted per model | |
| 50 | + self.unassignedFieldCount = ko.observable(0); // Number of bound fields with missing data | |
| 51 | + self.dataHubIsConnected = ko.observable(false); // Data hub connected observable flag - externally managed | |
| 52 | + self.errors = ko.validation.group([ko.observable(0)]); // Validation errors array | |
| 53 | + self.canEdit = ko.observable(true); // Can edit flag - normally controlled by external roles | |
| 54 | + self.canAddNew = ko.observable(true); // Can add new flag - normally controlled by external roles | |
| 55 | + self.canDelete = ko.observable(true); // Can delete flag - normally controlled by external roles | |
| 56 | + | |
| 57 | + // Internal fields | |
| 58 | + self._currentPageSize = ko.observable(1); | |
| 59 | + self._currentPage = ko.observable(0); | |
| 60 | + self._currentRecord = ko.observable(); | |
| 61 | + self._recordMode = ko.observable(RecordMode.View); | |
| 62 | + self._isDirty = ko.observable(false); | |
| 63 | + self._columnWidths = []; | |
| 64 | + | |
| 65 | + // Properties | |
| 66 | + | |
| 67 | + // Gets or sets the number records to display on the page | |
| 68 | + self.currentPageSize = ko.pureComputed({ | |
| 69 | + read: self._currentPageSize, | |
| 70 | + write: function (value) { | |
| 71 | + if (value < 1) | |
| 72 | + value = 1; | |
| 73 | + | |
| 74 | + if (value !== self._currentPageSize()) { | |
| 75 | + self._currentPageSize(value); | |
| 76 | + | |
| 77 | + // Validate current page after page size change | |
| 78 | + if (self.currentPage()) | |
| 79 | + self.currentPage(self.currentPage()); | |
| 80 | + } | |
| 81 | + }, | |
| 82 | + owner: self | |
| 83 | + }); | |
| 84 | + | |
| 85 | + // Gets or sets the current page number | |
| 86 | + self.currentPage = ko.pureComputed({ | |
| 87 | + read: self._currentPage, | |
| 88 | + write: function (value) { | |
| 89 | + if (value < 1) | |
| 90 | + value = 1; | |
| 91 | + else if (value > self.totalPages()) | |
| 92 | + value = self.totalPages(); | |
| 93 | + | |
| 94 | + // Requery records for page when current page changes | |
| 95 | + if (value !== self._currentPage()) { | |
| 96 | + self._currentPage(value); | |
| 97 | + self.queryPageRecords(); | |
| 98 | + } | |
| 99 | + }, | |
| 100 | + owner: self | |
| 101 | + }); | |
| 102 | + | |
| 103 | + // Gets or sets current record on page | |
| 104 | + self.currentRecord = ko.pureComputed({ | |
| 105 | + read: self._currentRecord, | |
| 106 | + write: function(value) { | |
| 107 | + self._currentRecord(value); | |
| 108 | + self.isDirty(false); | |
| 109 | + $(self).trigger("currentRecordChanged"); | |
| 110 | + self.applyValidationParameters(); | |
| 111 | + | |
| 112 | + // Watch for changes to fields in current record | |
| 113 | + ko.watch(self._currentRecord(), function (parents, child, item) { | |
| 114 | + self.isDirty(true); | |
| 115 | + $(self).trigger("currentRecordUpdated", [child]); | |
| 116 | + | |
| 117 | + // Recalculate unassigned field count after bindings may have been updated by field change | |
| 118 | + setTimeout(self.calculateUnassignedFieldCount, 10); | |
| 119 | + }); | |
| 120 | + }, | |
| 121 | + owner: self | |
| 122 | + }); | |
| 123 | + | |
| 124 | + // Gets or sets the record mode, i.e., view, edit or add new | |
| 125 | + self.recordMode = ko.pureComputed({ | |
| 126 | + read: self._recordMode, | |
| 127 | + write: function (newMode) { | |
| 128 | + const oldMode = self._recordMode(); | |
| 129 | + | |
| 130 | + if (newMode !== oldMode) { | |
| 131 | + self._recordMode(newMode); | |
| 132 | + | |
| 133 | + if (oldMode === RecordMode.View && newMode === RecordMode.Edit) | |
| 134 | + self.setFocusOnInitialField(); | |
| 135 | + | |
| 136 | + $(self).trigger("recordModeChanged", [oldMode, newMode]); | |
| 137 | + } | |
| 138 | + }, | |
| 139 | + owner: self | |
| 140 | + }); | |
| 141 | + | |
| 142 | + // Gets or sets current record changed flag | |
| 143 | + self.isDirty = ko.pureComputed({ | |
| 144 | + read: self._isDirty, | |
| 145 | + write: function (value) { | |
| 146 | + if (value === undefined) | |
| 147 | + value = true; | |
| 148 | + | |
| 149 | + self._isDirty(value); | |
| 150 | + self.calculateUnassignedFieldCount(); | |
| 151 | + }, | |
| 152 | + owner: self | |
| 153 | + }); | |
| 154 | + | |
| 155 | + // Gets total number pages based on total record count and current page size | |
| 156 | + self.totalPages = ko.pureComputed(function () { | |
| 157 | + return Math.max(Math.ceil(self.recordCount() / self.currentPageSize()), 1); | |
| 158 | + }); | |
| 159 | + | |
| 160 | + // Gets flag that determines if current page is first page | |
| 161 | + self.onFirstPage = ko.pureComputed(function () { | |
| 162 | + return self.currentPage() <= 1; | |
| 163 | + }); | |
| 164 | + | |
| 165 | + // Gets flag that determines if current page is last page | |
| 166 | + self.onLastPage = ko.pureComputed(function () { | |
| 167 | + return self.currentPage() >= self.totalPages(); | |
| 168 | + }); | |
| 169 | + | |
| 170 | + // Return validation errors, i.e., non-required field errors | |
| 171 | + self.validationErrors = ko.pureComputed(function() { | |
| 172 | + return self.errors().length - self.unassignedFieldCount(); | |
| 173 | + }).extend({ notify: "always" }); | |
| 174 | + | |
| 175 | + // Delegates | |
| 176 | + self.queryRecordCount = function () { }; | |
| 177 | + self.queryRecords = function (/* sortField, ascending, page, pageSize */) { }; | |
| 178 | + self.deleteRecord = function (/* keyValues[] */) { }; | |
| 179 | + self.newRecord = function () { }; | |
| 180 | + self.addNewRecord = function (/* record */) { }; | |
| 181 | + self.updateRecord = function (/* record */) { }; | |
| 182 | + self.applyValidationParameters = function () { }; | |
| 183 | + | |
| 184 | + // Setters needed to assign delegate properties, 'cause Javascript | |
| 185 | + self.setQueryRecordCount = function (queryRecordCountFunction) { | |
| 186 | + self.queryRecordCount = queryRecordCountFunction; | |
| 187 | + } | |
| 188 | + | |
| 189 | + self.setQueryRecords = function (queryRecordsFunction) { | |
| 190 | + self.queryRecords = queryRecordsFunction; | |
| 191 | + } | |
| 192 | + | |
| 193 | + self.setDeleteRecord = function (deleteRecordFunction) { | |
| 194 | + self.deleteRecord = deleteRecordFunction; | |
| 195 | + } | |
| 196 | + | |
| 197 | + self.setNewRecord = function (newRecordFunction) { | |
| 198 | + self.newRecord = newRecordFunction; | |
| 199 | + } | |
| 200 | + | |
| 201 | + self.setAddNewRecord = function (addNewRecordFunction) { | |
| 202 | + self.addNewRecord = addNewRecordFunction; | |
| 203 | + } | |
| 204 | + | |
| 205 | + self.setUpdateRecord = function (updateRecordFunction) { | |
| 206 | + self.updateRecord = updateRecordFunction; | |
| 207 | + } | |
| 208 | + | |
| 209 | + self.setApplyValidationParameters = function (applyValidationParametersFunction) { | |
| 210 | + self.applyValidationParameters = applyValidationParametersFunction; | |
| 211 | + } | |
| 212 | + | |
| 213 | + // Methods | |
| 214 | + self.initialize = function () { | |
| 215 | + // Restore any previous sort order | |
| 216 | + const lastSortField = Cookies.get(self.modelName + "!LastSortField"); | |
| 217 | + const lastSortAscending = Cookies.get(self.modelName + "!LastSortAscending"); | |
| 218 | + | |
| 219 | + if (lastSortField === undefined) | |
| 220 | + self.sortField(self.defaultSortField); | |
| 221 | + else | |
| 222 | + self.sortField(lastSortField); | |
| 223 | + | |
| 224 | + if (lastSortAscending === undefined) | |
| 225 | + self.sortAscending(self.defaultSortAscending); | |
| 226 | + else | |
| 227 | + self.sortAscending(lastSortAscending === "true"); | |
| 228 | + | |
| 229 | + if (self.dataHubIsConnected()) { | |
| 230 | + // Query total record count | |
| 231 | + self.queryRecordCount().done(function (count) { | |
| 232 | + // Update record count observable | |
| 233 | + self.recordCount(count); | |
| 234 | + | |
| 235 | + // Force page refresh when record count has been updated | |
| 236 | + const currentPage = self.currentPage(); | |
| 237 | + self._currentPage(0); | |
| 238 | + self.currentPage(currentPage); | |
| 239 | + }); | |
| 240 | + | |
| 241 | + // Initialize current record with an empty row | |
| 242 | + self.newRecord().done(function (emptyRecord) { | |
| 243 | + self.deriveObservableRecord(emptyRecord).done(function (observableRecord) { | |
| 244 | + self.currentRecord(observableRecord); | |
| 245 | + }); | |
| 246 | + }); | |
| 247 | + } | |
| 248 | + | |
| 249 | + // Initialize column widths array | |
| 250 | + self._columnWidths = []; | |
| 251 | + const columns = $("#recordRow").find("td"); | |
| 252 | + | |
| 253 | + for (let i = 0; i < columns.length; i++) { | |
| 254 | + self._columnWidths.push($(columns[i]).width()); | |
| 255 | + } | |
| 256 | + } | |
| 257 | + | |
| 258 | + self.calculatePageSize = function (forceRefresh) { | |
| 259 | + // Calculate total number of table rows that will fit within current page height | |
| 260 | + const remainingHeight = calculateRemainingBodyHeight() - | |
| 261 | + $("#contentWell").paddingHeight() - | |
| 262 | + $("#responsiveTableDiv").paddingHeight() - | |
| 263 | + $("#recordsTable").paddingHeight() - | |
| 264 | + $("#pageControlsRow").outerHeight(true); | |
| 265 | + | |
| 266 | + // Estimate page size based on height of first record row | |
| 267 | + const firstRow = $("#recordRow"); | |
| 268 | + var pageSize = (remainingHeight / firstRow.outerHeight(true)).truncate(); | |
| 269 | + | |
| 270 | + if (!pageSize || isNaN(pageSize) || !isFinite(pageSize) || pageSize < 1) | |
| 271 | + pageSize = 1; | |
| 272 | + | |
| 273 | + if (forceRefresh === undefined) | |
| 274 | + forceRefresh = false; | |
| 275 | + | |
| 276 | + // Check for dynamic Bootstrap column resizing, in which case we need to refresh data | |
| 277 | + // for cases where binding may be truncating data lengths, see $.fn.truncateToWidth | |
| 278 | + const columns = firstRow.find("td"); | |
| 279 | + var columnWidth; | |
| 280 | + | |
| 281 | + for (let i = 0; i < columns.length; i++) { | |
| 282 | + columnWidth = $(columns[i]).width(); | |
| 283 | + | |
| 284 | + if (self._columnWidths[i] !== columnWidth) { | |
| 285 | + self._columnWidths[i] = columnWidth; | |
| 286 | + forceRefresh = true; | |
| 287 | + } | |
| 288 | + } | |
| 289 | + | |
| 290 | + if (pageSize !== self.currentPageSize() || forceRefresh) { | |
| 291 | + const currentPage = self.currentPage(); | |
| 292 | + | |
| 293 | + // Updating page size will validate current page number | |
| 294 | + self.currentPageSize(pageSize); | |
| 295 | + | |
| 296 | + // Requery data for page unless current page was dynamically changed | |
| 297 | + // by page size update and has reloaded data already | |
| 298 | + if (currentPage === self.currentPage()) | |
| 299 | + self.queryPageRecords(); | |
| 300 | + } | |
| 301 | + } | |
| 302 | + | |
| 303 | + self.calculateUnassignedFieldCount = function () { | |
| 304 | + // Derive unassigned field count based on existence of Bootstrap "has-error" class | |
| 305 | + self.unassignedFieldCount($("#addNewEditDialog div.form-group.has-error").length); | |
| 306 | + } | |
| 307 | + | |
| 308 | + self.refreshValidationErrors = function () { | |
| 309 | + // Force re-evaluation of validation errors property | |
| 310 | + self.unassignedFieldCount.valueHasMutated(); | |
| 311 | + | |
| 312 | + // Make sure any initial validation error messages are visible | |
| 313 | + self.errors.showAllMessages(); | |
| 314 | + } | |
| 315 | + | |
| 316 | + // Convert observable object to a simple Javascript record | |
| 317 | + self.deriveJSRecord = function () { | |
| 318 | + return $.Deferred(function (deferred) { | |
| 319 | + const observableRecord = self.currentRecord(); | |
| 320 | + const promises = []; | |
| 321 | + | |
| 322 | + // Allow customization of observable record before conversion | |
| 323 | + $(self).trigger("derivingJSRecord", [observableRecord, promises]); | |
| 324 | + | |
| 325 | + $(promises).whenAll().done(function () { | |
| 326 | + deferred.resolve(ko.mapping.toJS(observableRecord)); | |
| 327 | + }); | |
| 328 | + }).promise(); | |
| 329 | + } | |
| 330 | + | |
| 331 | + // Convert simple Javascript record to an observable object | |
| 332 | + self.deriveObservableRecord = function (record) { | |
| 333 | + return $.Deferred(function (deferred) { | |
| 334 | + const observableRecord = ko.mapping.fromJS(record); | |
| 335 | + const promises = []; | |
| 336 | + | |
| 337 | + // Allow customization of new observable record | |
| 338 | + $(self).trigger("derivingObservableRecord", [observableRecord, promises]); | |
| 339 | + | |
| 340 | + $(promises).whenAll().done(function () { | |
| 341 | + // Apply validation binding to current observable record | |
| 342 | + self.errors = ko.validation.group(observableRecord); | |
| 343 | + self.refreshValidationErrors(); | |
| 344 | + deferred.resolve(observableRecord); | |
| 345 | + }); | |
| 346 | + }).promise(); | |
| 347 | + } | |
| 348 | + | |
| 349 | + self.setFocusOnInitialField = function () { | |
| 350 | + if (!isEmpty(self.initialFocusField)) | |
| 351 | + $("#" + self.initialFocusField).focus(); | |
| 352 | + } | |
| 353 | + | |
| 354 | + self.nextPage = function () { | |
| 355 | + if (self.currentPage() < self.totalPages()) | |
| 356 | + self.currentPage(self.currentPage() + 1); | |
| 357 | + } | |
| 358 | + | |
| 359 | + self.previousPage = function () { | |
| 360 | + if (self.currentPage() > 1) | |
| 361 | + self.currentPage(self.currentPage() - 1); | |
| 362 | + } | |
| 363 | + | |
| 364 | + self.updateSortOrder = function (fieldName, ascending) { | |
| 365 | + self.sortField(fieldName); | |
| 366 | + self.sortAscending(ascending); | |
| 367 | + self.queryPageRecords(); | |
| 368 | + | |
| 369 | + // Save last sort order | |
| 370 | + Cookies.set(self.modelName + "!LastSortField", self.sortField(), { expires: 365 }); | |
| 371 | + Cookies.set(self.modelName + "!LastSortAscending", self.sortAscending().toString(), { expires: 365 }); | |
| 372 | + } | |
| 373 | + | |
| 374 | + self.isSortOrder = function (fieldName, ascending) { | |
| 375 | + return self.sortField().toUpperCase() === fieldName.toUpperCase() && self.sortAscending() === ascending; | |
| 376 | + } | |
| 377 | + | |
| 378 | + self.queryPageRecords = function () { | |
| 379 | + if (self.dataHubIsConnected()) | |
| 380 | + self.queryRecords(self.sortField(), self.sortAscending(), self.currentPage(), self.currentPageSize()).done(function (records) { | |
| 381 | + $(self).trigger("pageRecordsQueried", [records]); | |
| 382 | + self.pageRecords.removeAll(); | |
| 383 | + self.pageRecords(records); | |
| 384 | + refreshHubDependentControlState(); | |
| 385 | + $("[id='recordRow']").css("visibility", "visible"); | |
| 386 | + $("#loadingDataLabel").hide(); | |
| 387 | + | |
| 388 | + // Validate proper page size after any record refresh | |
| 389 | + setTimeout(self.calculatePageSize, 150); | |
| 390 | + }).fail(function (error) { | |
| 391 | + showErrorMessage(error); | |
| 392 | + }); | |
| 393 | + } | |
| 394 | + | |
| 395 | + self.removePageRecord = function (record) { | |
| 396 | + if (!self.canDelete()) | |
| 397 | + return; | |
| 398 | + | |
| 399 | + if (self.dataHubIsConnected() && confirm("Are you sure you want to delete \"" + record[self.labelField] + "\"?")) { | |
| 400 | + const keyValues = []; | |
| 401 | + | |
| 402 | + for (var i = 0; i < self.primaryKeyFields.length; i++) { | |
| 403 | + keyValues.push(record[self.primaryKeyFields[i]]); | |
| 404 | + } | |
| 405 | + | |
| 406 | + self.deleteRecord(keyValues).done(function () { | |
| 407 | + self.pageRecords.remove(record); | |
| 408 | + self.initialize(); | |
| 409 | + $(self).trigger("recordDeleted", [record]); | |
| 410 | + showInfoMessage("Deleted record..."); | |
| 411 | + }).fail(function (error) { | |
| 412 | + showErrorMessage(error); | |
| 413 | + }); | |
| 414 | + } | |
| 415 | + } | |
| 416 | + | |
| 417 | + self.savePageRecord = function () { | |
| 418 | + switch (self.recordMode()) { | |
| 419 | + case RecordMode.Edit: | |
| 420 | + self.saveEditedRecord(); | |
| 421 | + break; | |
| 422 | + case RecordMode.AddNew: | |
| 423 | + self.saveNewRecord(); | |
| 424 | + break; | |
| 425 | + } | |
| 426 | + } | |
| 427 | + | |
| 428 | + self.saveEditedRecord = function () { | |
| 429 | + if (!self.canEdit()) | |
| 430 | + return; | |
| 431 | + | |
| 432 | + if (self.dataHubIsConnected()) { | |
| 433 | + self.deriveJSRecord().done(function (record) { | |
| 434 | + self.updateRecord(record).done(function () { | |
| 435 | + self.initialize(); | |
| 436 | + $(self).trigger("recordSaved", [record, false]); | |
| 437 | + showInfoMessage("Saved updated record..."); | |
| 438 | + }).fail(function (error) { | |
| 439 | + showErrorMessage(error); | |
| 440 | + }); | |
| 441 | + }); | |
| 442 | + } | |
| 443 | + } | |
| 444 | + | |
| 445 | + self.saveNewRecord = function () { | |
| 446 | + if (!self.canAddNew()) | |
| 447 | + return; | |
| 448 | + | |
| 449 | + if (self.dataHubIsConnected()) { | |
| 450 | + self.deriveJSRecord().done(function (record) { | |
| 451 | + self.addNewRecord(record).done(function () { | |
| 452 | + self.initialize(); | |
| 453 | + $(self).trigger("recordSaved", [record, true]); | |
| 454 | + showInfoMessage("Saved new record..."); | |
| 455 | + }).fail(function (error) { | |
| 456 | + showErrorMessage(error); | |
| 457 | + }); | |
| 458 | + }); | |
| 459 | + } | |
| 460 | + } | |
| 461 | + | |
| 462 | + self.viewPageRecord = function (record) { | |
| 463 | + self.deriveObservableRecord(record).done(function (observableRecord) { | |
| 464 | + self.recordMode(RecordMode.View); | |
| 465 | + self.currentRecord(observableRecord); | |
| 466 | + $("#addNewEditDialog").modal("show"); | |
| 467 | + }); | |
| 468 | + } | |
| 469 | + | |
| 470 | + self.editPageRecord = function (record) { | |
| 471 | + if (!self.canEdit()) | |
| 472 | + return; | |
| 473 | + | |
| 474 | + self.deriveObservableRecord(record).done(function (observableRecord) { | |
| 475 | + self.recordMode(RecordMode.Edit); | |
| 476 | + self.currentRecord(observableRecord); | |
| 477 | + $("#addNewEditDialog").modal("show"); | |
| 478 | + }); | |
| 479 | + } | |
| 480 | + | |
| 481 | + self.addPageRecord = function () { | |
| 482 | + if (!self.canAddNew()) | |
| 483 | + return; | |
| 484 | + | |
| 485 | + if (self.dataHubIsConnected()) { | |
| 486 | + self.newRecord().done(function (emptyRecord) { | |
| 487 | + // Raise event to allow any new record initialization | |
| 488 | + $(self).trigger("newRecord", [emptyRecord]); | |
| 489 | + | |
| 490 | + self.deriveObservableRecord(emptyRecord).done(function (observableRecord) { | |
| 491 | + self.recordMode(RecordMode.AddNew); | |
| 492 | + self.currentRecord(observableRecord); | |
| 493 | + $("#addNewEditDialog").modal("show"); | |
| 494 | + }); | |
| 495 | + }); | |
| 496 | + } | |
| 497 | + } | |
| 498 | + | |
| 499 | + self.cancelPageRecord = function () { | |
| 500 | + if (!self.isDirty() || confirm("Are you sure you want to discard unsaved changes?")) | |
| 501 | + $("#addNewEditDialog").modal("hide"); | |
| 502 | + } | |
| 503 | +}; | |
| 504 | + | |
| 505 | +// Define page scoped view model instance | |
| 506 | +var viewModel = new PagedViewModel(); | |
| 507 | + | |
| 508 | +(function ($, viewPort) { | |
| 509 | + $("#bodyContainer").addClass("fill-height"); | |
| 510 | + | |
| 511 | + $("#titleText").html("Records: <span data-bind='text: recordCount'>calculating...</span>"); | |
| 512 | + | |
| 513 | + $("#firstPageButton").click(function () { | |
| 514 | + viewModel.currentPage(1); | |
| 515 | + }); | |
| 516 | + | |
| 517 | + $("#previousPageButton").click(function () { | |
| 518 | + viewModel.previousPage(); | |
| 519 | + }); | |
| 520 | + | |
| 521 | + $("#nextPageButton").click(function () { | |
| 522 | + viewModel.nextPage(); | |
| 523 | + }); | |
| 524 | + | |
| 525 | + $("#lastPageButton").click(function () { | |
| 526 | + viewModel.currentPage(viewModel.totalPages()); | |
| 527 | + }); | |
| 528 | + | |
| 529 | + $(window).on("hubConnected", function (event) { | |
| 530 | + viewModel.dataHubIsConnected(true); | |
| 531 | + viewModel.initialize(); | |
| 532 | + }); | |
| 533 | + | |
| 534 | + $(window).on("hubDisconnected", function (event) { | |
| 535 | + viewModel.dataHubIsConnected(false); | |
| 536 | + }); | |
| 537 | + | |
| 538 | + $(window).on("messageVisibiltyChanged", function (event) { | |
| 539 | + viewModel.calculatePageSize(); | |
| 540 | + }); | |
| 541 | + | |
| 542 | + $("#addNewEditDialog").on("shown.bs.modal", function () { | |
| 543 | + viewModel.setFocusOnInitialField(); | |
| 544 | + $("[data-toggle='tooltip']").tooltip(); | |
| 545 | + }); | |
| 546 | + | |
| 547 | + $(window).resize( | |
| 548 | + viewPort.changed(function () { | |
| 549 | + viewModel.calculatePageSize(); | |
| 550 | + })); | |
| 551 | + | |
| 552 | + viewModel.calculatePageSize(); | |
| 553 | + | |
| 554 | + ko.validation.rules.pattern.message = "Invalid"; | |
| 555 | + | |
| 556 | + ko.validation.init({ | |
| 557 | + registerExtenders: true, | |
| 558 | + messagesOnModified: true, | |
| 559 | + insertMessages: true, | |
| 560 | + parseInputAttributes: true, | |
| 561 | + allowHtmlMessages: true, | |
| 562 | + messageTemplate: null, | |
| 563 | + grouping: { deep: true, observable: true, live: true } | |
| 564 | + }, true); | |
| 565 | + | |
| 566 | + ko.applyBindings(viewModel); | |
| 567 | + | |
| 568 | +})(jQuery, ResponsiveBootstrapToolkit); | |
| @@ -0,0 +1,348 @@ | ||
| 1 | +@******************************************************************************************************* | |
| 2 | +// PagedViewModel.cshtml - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 01/22/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//*****************************************************************************************************@ | |
| 23 | +@using System | |
| 24 | +@using System.Collections.Generic | |
| 25 | +@using System.Security.Policy | |
| 26 | +@using System.Text | |
| 27 | +@using GSF | |
| 28 | +@using GSF.Web | |
| 29 | +@using GSF.Web.Model | |
| 30 | +@using GSF.Web.Templating | |
| 31 | +@{ | |
| 32 | + if (ViewBag.PageControlScripts == null) | |
| 33 | + { | |
| 34 | + ViewBag.PageControlScripts = new StringBuilder(); | |
| 35 | + } | |
| 36 | + | |
| 37 | + StringBuilder pageControlScripts = ViewBag.PageControlScripts; | |
| 38 | + | |
| 39 | + // Setup page control scripts | |
| 40 | + pageControlScripts.Append(Scripts.Render("~/js.bundle/knockout")); | |
| 41 | + pageControlScripts.Append($"<script src=\"{Url.Content("~/Scripts/pagedViewModel.js")}\"></script>"); | |
| 42 | + | |
| 43 | + pageControlScripts.Append(@" | |
| 44 | + <script id=""validationMessageTemplate"" type=""text/html""><!-- | |
| 45 | + --><em class=""validationPrompt"" data-bind=""validationMessage: field""></em> | |
| 46 | + </script> | |
| 47 | + <script> | |
| 48 | + $(function () { | |
| 49 | + $(""#clearSortOrder"").click(function () { | |
| 50 | + viewModel.updateSortOrder(viewModel.defaultSortField, viewModel.defaultSortAscending); | |
| 51 | + }); | |
| 52 | + "); | |
| 53 | + | |
| 54 | + // Check for optional page behaviors | |
| 55 | + | |
| 56 | + // Hide the add new button | |
| 57 | + bool hideAddNewButton = ViewBag.HideAddNewButton ?? false; | |
| 58 | + | |
| 59 | + // Hide unauthorized controls as disallowed by Edit/AddNew/Delete roles - otherwise unauthorized controls will be disabled | |
| 60 | + bool hideUnauthorizedControls = ViewBag.HideUnauthorizedControls ?? false; | |
| 61 | + | |
| 62 | + // Auto pop-up add new dialog | |
| 63 | + bool autoShowAddNew = ViewBag.RouteID?.Equals("AddNew", StringComparison.OrdinalIgnoreCase) ?? false; | |
| 64 | + | |
| 65 | + bool showIsDeletedToggleLink = false; | |
| 66 | + bool showingDeletedRecords = false; | |
| 67 | + | |
| 68 | + // Check for existence of ShowDeleted property to control toggle link visibility | |
| 69 | + if (ViewBag.ShowDeleted != null) | |
| 70 | + { | |
| 71 | + showIsDeletedToggleLink = true; | |
| 72 | + showingDeletedRecords = Convert.ToBoolean(ViewBag.ShowDeleted); | |
| 73 | + } | |
| 74 | + | |
| 75 | + // Get current allowed create / update / delete states for user - default to none. | |
| 76 | + // Note that overriding view bag settings will only affect UI, back end security | |
| 77 | + // will still be applicable | |
| 78 | + bool canEdit = ViewBag.CanEdit ?? false; | |
| 79 | + bool canAddNew = ViewBag.CanAddNew ?? false; | |
| 80 | + bool canDelete = ViewBag.CanDelete ?? false; | |
| 81 | +} | |
| 82 | +@functions | |
| 83 | +{ | |
| 84 | + string SingularForm(string value) | |
| 85 | + { | |
| 86 | + if (value.EndsWith("ies", StringComparison.Ordinal)) | |
| 87 | + return value.Substring(0, value.Length - 3) + "y"; | |
| 88 | + | |
| 89 | + if (value.EndsWith("IES", StringComparison.Ordinal)) | |
| 90 | + return value.Substring(0, value.Length - 3) + "Y"; | |
| 91 | + | |
| 92 | + if (value.EndsWith("es", StringComparison.OrdinalIgnoreCase)) | |
| 93 | + return value.Substring(0, value.Length - 2); | |
| 94 | + | |
| 95 | + if (value.EndsWith("s", StringComparison.OrdinalIgnoreCase)) | |
| 96 | + return value.Substring(0, value.Length - 1); | |
| 97 | + | |
| 98 | + return value; | |
| 99 | + } | |
| 100 | + | |
| 101 | + string RemoveSpaces(string value) | |
| 102 | + { | |
| 103 | + return value.RemoveWhiteSpace().RemoveInvalidFileNameCharacters(); | |
| 104 | + } | |
| 105 | + | |
| 106 | + string RenderSortScript(string fieldName, bool ascending) | |
| 107 | + { | |
| 108 | + const string ScriptFormat = @" | |
| 109 | + $(""#sort{0} #{1}"").click(function () {{ | |
| 110 | + viewModel.updateSortOrder(""{0}"", {2}); | |
| 111 | + }}); | |
| 112 | + "; | |
| 113 | + | |
| 114 | + return string.Format(ScriptFormat, fieldName, ascending ? "asc" : "desc", ascending.ToString().ToLower()); | |
| 115 | + } | |
| 116 | +} | |
| 117 | +@helper WriteFieldHeader(StringBuilder pageControlScripts, string fieldName, string labelName = null, string classes = null) | |
| 118 | +{ | |
| 119 | + if (string.IsNullOrEmpty(fieldName)) | |
| 120 | + { | |
| 121 | + <th class="@Html.Raw(classes ?? "text-center")" nowrap>@Html.Raw(labelName ?? "")</th> | |
| 122 | + } | |
| 123 | + else | |
| 124 | + { | |
| 125 | + <th class="@Html.Raw(classes ?? "text-center")" id="sort@(fieldName)" nowrap>@Html.Raw(labelName ?? fieldName) | |
| 126 | + <div class="btn-group-vertical btn-group-sort"> | |
| 127 | + <button type="button" class="btn" id="asc" title="Sort ascending..." data-bind="css: {'btn-primary': isSortOrder('@fieldName', true)}, enable: dataHubIsConnected"><span class="glyphicon glyphicon-chevron-up"></span></button> | |
| 128 | + <button type="button" class="btn" id="desc" title="Sort descending..." data-bind="css: {'btn-primary': isSortOrder('@fieldName', false)}, enable: dataHubIsConnected"><span class="glyphicon glyphicon-chevron-down"></span></button> | |
| 129 | + </div> | |
| 130 | + </th> | |
| 131 | + | |
| 132 | + pageControlScripts.Append(RenderSortScript(fieldName, true)); | |
| 133 | + pageControlScripts.Append(RenderSortScript(fieldName, false)); | |
| 134 | + } | |
| 135 | +} | |
| 136 | +<div class="well well-dynamic-content" id="contentWell" content-fill-height> | |
| 137 | + <div class="table-responsive" id="responsiveTableDiv"> | |
| 138 | + <table class="table table-condensed table-hover" id="recordsTable"> | |
| 139 | + <thead> | |
| 140 | + <tr> | |
| 141 | + @foreach (string[] headerColumn in ViewBag.HeaderColumns) { | |
| 142 | + @WriteFieldHeader(pageControlScripts, headerColumn[0], headerColumn[1], headerColumn[2]) | |
| 143 | + } | |
| 144 | + <th class="text-center"> | |
| 145 | + <button type="button" class="btn btn-link btn-xs" style="line-height: 1.1" id="clearSortOrder" data-bind="enable: dataHubIsConnected">Clear<br/>Sort</button> | |
| 146 | + </th> | |
| 147 | + </tr> | |
| 148 | + </thead> | |
| 149 | + <tbody data-bind="foreach: pageRecords"> | |
| 150 | + <tr style="visibility: hidden" id="recordRow"@Html.Raw(ViewBag.ShowDeleted ?? false ? $" data-bind=\"css: {{'danger': {ViewBag.IsDeletedField}}}\"" : "")> | |
| 151 | + @Html.Raw(ViewBag.BodyRows) | |
| 152 | + </tr> | |
| 153 | + </tbody> | |
| 154 | + </table> | |
| 155 | + <div id="loadingDataLabel"> | |
| 156 | + Loading <span class="glyphicon glyphicon-refresh glyphicon-spin"></span> | |
| 157 | + </div> | |
| 158 | + </div> | |
| 159 | + <div class="pull-right" id="pageControlsRow"> | |
| 160 | + @if (!hideAddNewButton && (canAddNew || !hideUnauthorizedControls)) { | |
| 161 | + <button type="button" class="btn btn-sm btn-primary pull-right" id="addRecordButton" @Html.Raw(canAddNew ? "data-bind=\"enable: dataHubIsConnected\"" : "disabled")> | |
| 162 | + <span class="glyphicon glyphicon-plus"></span> Add New | |
| 163 | + </button> | |
| 164 | + <br /> | |
| 165 | + <hr class="half-break" /> | |
| 166 | + } | |
| 167 | + <div class="btn-group btn-group-sm"> | |
| 168 | + <button type="button" class="btn btn-default" id="firstPageButton" data-bind="css: {'disabled': onFirstPage()}, enable: dataHubIsConnected() && !onFirstPage()"><span class="glyphicon glyphicon-backward"></span></button> | |
| 169 | + <button type="button" class="btn btn-default" id="previousPageButton" data-bind="css: {'disabled': onFirstPage()}, enable: dataHubIsConnected() && !onFirstPage()"><span class="glyphicon glyphicon-triangle-left"></span></button> | |
| 170 | + </div> | |
| 171 | + <input type="number" class="content input-sm" style="padding: 0 0 0 5px; width: 55px" id="selectedPage" data-bind="numeric, textInput: currentPage, enable: dataHubIsConnected" value="1"> | |
| 172 | + of | |
| 173 | + <span data-bind="text: totalPages">1</span> | |
| 174 | + | |
| 175 | + <div class="btn-group btn-group-sm"> | |
| 176 | + <button type="button" class="btn btn-default" id="nextPageButton" data-bind="css: {'disabled': onLastPage()}, enable: dataHubIsConnected() && !onLastPage()"><span class="glyphicon glyphicon-triangle-right"></span></button> | |
| 177 | + <button type="button" class="btn btn-default" id="lastPageButton" data-bind="css: {'disabled': onLastPage()}, enable: dataHubIsConnected() && !onLastPage()"><span class="glyphicon glyphicon-forward"></span></button> | |
| 178 | + </div> | |
| 179 | + </div> | |
| 180 | +</div> | |
| 181 | +<div id="addNewEditDialog" class="modal fade" role="dialog"> | |
| 182 | + <div class="modal-dialog modal-lg"> | |
| 183 | + <div class="modal-content"> | |
| 184 | + <div class="modal-header" data-bind="css: {'modal-readonly': recordMode()===RecordMode.View}"> | |
| 185 | + <button type="button" class="close" id="dismissDialogButton">×</button> | |
| 186 | + <h4 class="modal-title"> | |
| 187 | + <span data-bind="visible: recordMode()===RecordMode.View">View</span> | |
| 188 | + <span data-bind="visible: recordMode()===RecordMode.Edit">Edit</span> | |
| 189 | + <span data-bind="visible: recordMode()===RecordMode.AddNew">Add New</span> | |
| 190 | + @SingularForm(ViewBag.Title ?? "") | |
| 191 | + </h4> | |
| 192 | + </div> | |
| 193 | + <div class="modal-body auto-height" data-bind="with: currentRecord, validationOptions: {messageTemplate: 'validationMessageTemplate'}, css: {'modal-readonly': recordMode()===RecordMode.View}"> | |
| 194 | + <form role="form"> | |
| 195 | + @Html.Raw(ViewBag.AddNewEditDialog) | |
| 196 | + </form> | |
| 197 | + </div> | |
| 198 | + <div class="modal-footer" data-bind="css: {'modal-readonly': recordMode()===RecordMode.View}"> | |
| 199 | + <em data-bind="visible: unassignedFieldCount() > 0 || validationErrors() > 0"> | |
| 200 | + <span data-bind="visible: unassignedFieldCount() > 0">Missing <span data-bind="text: unassignedFieldCount"></span> required field<span data-bind="visible: unassignedFieldCount() > 1">s</span></span> | |
| 201 | + <span data-bind="visible: unassignedFieldCount() > 0 && validationErrors() > 0"> and </span> | |
| 202 | + <span data-bind="visible: validationErrors() > 0"><span data-bind="text: validationErrors()"></span> validation error<span data-bind="visible: validationErrors() > 1">s</span></span> | |
| 203 | + ... | |
| 204 | + </em> | |
| 205 | + @if (canEdit || !hideUnauthorizedControls) { | |
| 206 | + <button type="button" class="btn btn-info" data-bind="visible: recordMode()===RecordMode.View@(Html.Raw(canEdit ? ", enable: dataHubIsConnected" : ""))" id="editRecordButton"@Html.Raw(canEdit ? "" : " disabled")>Edit</button> | |
| 207 | + } | |
| 208 | + @if (canEdit || canAddNew || !hideUnauthorizedControls) { | |
| 209 | + <button type="submit" class="btn btn-primary" data-dismiss="modal" data-bind="visible: recordMode()!==RecordMode.View@(Html.Raw(canEdit || canAddNew ? ", disable: unassignedFieldCount() > 0 || validationErrors() > 0 || !dataHubIsConnected()" : ""))" id="saveRecordButton"@Html.Raw(canEdit || canAddNew ? "" : " disabled")>Save</button> | |
| 210 | + } | |
| 211 | + <button type="button" class="btn btn-default" id="cancelRecordButton">Cancel</button> | |
| 212 | + </div> | |
| 213 | + </div> | |
| 214 | + </div> | |
| 215 | +</div> | |
| 216 | +@{ | |
| 217 | + DataContext<CSharpEmbeddedResource> dataContext = ViewBag.DataContext; | |
| 218 | + | |
| 219 | + if (autoShowAddNew) { | |
| 220 | + pageControlScripts.Append(@" | |
| 221 | + // Add new pop-up requested via URL parameter | |
| 222 | + setTimeout(viewModel.addPageRecord, 500); | |
| 223 | + "); | |
| 224 | + } | |
| 225 | + | |
| 226 | + if (showIsDeletedToggleLink) { | |
| 227 | + string baseURL = Request?.Url?.AbsolutePath ?? "/"; | |
| 228 | + baseURL = baseURL.Substring(0, baseURL.Length - (ViewBag.RouteID ?? "").Length); | |
| 229 | + | |
| 230 | + if (showingDeletedRecords) { | |
| 231 | + pageControlScripts.Append($@" | |
| 232 | + $(""#titleText"").append(""<br /><div style='margin-top: 3px' class='small text-center'><a href='{baseURL}'>Hide Deleted</a></div>""); | |
| 233 | + "); | |
| 234 | + } | |
| 235 | + else { | |
| 236 | + pageControlScripts.Append($@" | |
| 237 | + $(""#titleText"").append(""<br /><div style='margin-top: 3px' class='small text-center'><a href='{baseURL.EnsureEnd('/')}ShowDeleted'>Show Deleted</a></div>""); | |
| 238 | + "); | |
| 239 | + } | |
| 240 | + } | |
| 241 | + | |
| 242 | + pageControlScripts.Append(@" | |
| 243 | + $(""#addRecordButton"").click(function () { | |
| 244 | + viewModel.addPageRecord(); | |
| 245 | + }); | |
| 246 | + | |
| 247 | + $(""#editRecordButton"").click(function () { | |
| 248 | + viewModel.recordMode(RecordMode.Edit); | |
| 249 | + }); | |
| 250 | + | |
| 251 | + $(""#saveRecordButton"").click(function () { | |
| 252 | + viewModel.savePageRecord(); | |
| 253 | + }); | |
| 254 | + | |
| 255 | + $(""#cancelRecordButton"").click(function () { | |
| 256 | + viewModel.cancelPageRecord(); | |
| 257 | + }); | |
| 258 | + | |
| 259 | + $(""#dismissDialogButton"").click(function () { | |
| 260 | + viewModel.cancelPageRecord(); | |
| 261 | + }); | |
| 262 | + | |
| 263 | + $(""#addNewEditDialog"").modal({show: false, backdrop: ""static"", keyboard: false}); | |
| 264 | + | |
| 265 | + $(""#addNewEditDialog"").on(""shown.bs.modal"", function () { | |
| 266 | + $(""#addNewEditDialog .input-group.date"").datepicker({ | |
| 267 | + todayBtn: ""linked"", | |
| 268 | + enableOnReadonly: false, | |
| 269 | + autoclose: true | |
| 270 | + }); | |
| 271 | + }); | |
| 272 | + | |
| 273 | + $(window).on(""beforeunload"", function () { | |
| 274 | + if ($(""#addNewEditDialog"").hasClass(""in"") && viewModel.isDirty()) | |
| 275 | + return ""There are unsaved changes to this record.""; | |
| 276 | + | |
| 277 | + return undefined; | |
| 278 | + }); | |
| 279 | + }); | |
| 280 | + "); | |
| 281 | + | |
| 282 | + pageControlScripts.AppendFormat(@" | |
| 283 | + viewModel.canEdit({0}); | |
| 284 | + viewModel.canAddNew({1}); | |
| 285 | + viewModel.canDelete({2}); | |
| 286 | + | |
| 287 | + viewModel.initialFocusField = ""{3}""; | |
| 288 | + viewModel.modelName = ""{4}"";", | |
| 289 | + canEdit.ToString().ToLower(), | |
| 290 | + canAddNew.ToString().ToLower(), | |
| 291 | + canDelete.ToString().ToLower(), | |
| 292 | + dataContext.InitialFocusField, | |
| 293 | + RemoveSpaces(ViewBag.Title)); | |
| 294 | + | |
| 295 | + if (dataContext.FieldValueInitializers.Count > 0) { | |
| 296 | + pageControlScripts.AppendFormat(@" | |
| 297 | + | |
| 298 | + $(viewModel).on(""newRecord"", function(event, newRecord) {{ | |
| 299 | + // Set initial field values"); | |
| 300 | + | |
| 301 | + foreach (Tuple<string, string> initialValue in dataContext.FieldValueInitializers) { | |
| 302 | + pageControlScripts.AppendFormat("\r\n newRecord.{0} = {1};", | |
| 303 | + initialValue.Item1, initialValue.Item2); | |
| 304 | + } | |
| 305 | + | |
| 306 | + pageControlScripts.Append("\r\n });"); | |
| 307 | + } | |
| 308 | + | |
| 309 | + if (dataContext.DefinedDateFields.Count > 0) { | |
| 310 | + pageControlScripts.AppendFormat(@" | |
| 311 | + | |
| 312 | + $(viewModel).on(""derivingObservableRecord"", function(event, observableRecord) {{ | |
| 313 | + // Convert date objects to strings"); | |
| 314 | + | |
| 315 | + foreach (string dateField in dataContext.DefinedDateFields) { | |
| 316 | + pageControlScripts.AppendFormat("\r\n observableRecord.{0}(notNull(observableRecord.{0}()).formatDate(DateFormat));", dateField); | |
| 317 | + } | |
| 318 | + | |
| 319 | + pageControlScripts.Append("\r\n });"); | |
| 320 | + | |
| 321 | + pageControlScripts.AppendFormat(@" | |
| 322 | + | |
| 323 | + $(viewModel).on(""derivingJSRecord"", function(event, observableRecord) {{ | |
| 324 | + // Convert date strings back to date objects before serialization"); | |
| 325 | + | |
| 326 | + foreach (string dateField in dataContext.DefinedDateFields) { | |
| 327 | + pageControlScripts.AppendFormat("\r\n observableRecord.{0}(observableRecord.{0}().toDate());", dateField); | |
| 328 | + } | |
| 329 | + | |
| 330 | + pageControlScripts.Append("\r\n });"); | |
| 331 | + } | |
| 332 | + | |
| 333 | + if (dataContext.FieldValidationParameters.Count > 0) { | |
| 334 | + pageControlScripts.Append("\r\n\r\n viewModel.setApplyValidationParameters(function () {"); | |
| 335 | + | |
| 336 | + foreach (KeyValuePair<string, Tuple<string, string>> fieldValidation in dataContext.FieldValidationParameters) { | |
| 337 | + pageControlScripts.AppendFormat("\r\n {0}.extend({{ pattern: {{ message: \"{1}\", params: \"{2}\" }} }});", | |
| 338 | + fieldValidation.Key, | |
| 339 | + fieldValidation.Value.Item2.JavaScriptEncode(), | |
| 340 | + fieldValidation.Value.Item1.JavaScriptEncode()); | |
| 341 | + } | |
| 342 | + | |
| 343 | + pageControlScripts.Append("\r\n });"); | |
| 344 | + } | |
| 345 | + | |
| 346 | + pageControlScripts.Append(@" | |
| 347 | + </script>"); | |
| 348 | +} | |
| @@ -0,0 +1,39 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// AuthorizationCache.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 03/11/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.Collections.Concurrent; | |
| 26 | + | |
| 27 | +namespace GSF.Web.Security | |
| 28 | +{ | |
| 29 | + /// <summary> | |
| 30 | + /// Defines authorization cache. | |
| 31 | + /// </summary> | |
| 32 | + public class AuthorizationCache | |
| 33 | + { | |
| 34 | + /// <summary> | |
| 35 | + /// Gets the current user ID cache. | |
| 36 | + /// </summary> | |
| 37 | + public static readonly ConcurrentDictionary<string, Guid> UserIDs = new ConcurrentDictionary<string, Guid>(StringComparer.OrdinalIgnoreCase); | |
| 38 | + } | |
| 39 | +} | |
| @@ -0,0 +1,156 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// RoleBasedSecurityAttribute.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/23/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.Linq; | |
| 26 | +using System.Security; | |
| 27 | +using System.Threading; | |
| 28 | +using System.Web.Mvc; | |
| 29 | +using GSF.Collections; | |
| 30 | +using GSF.Data; | |
| 31 | +using GSF.Identity; | |
| 32 | +using GSF.Security; | |
| 33 | + | |
| 34 | +namespace GSF.Web.Security | |
| 35 | +{ | |
| 36 | + /// <summary> | |
| 37 | + /// Defines an MVC filter attribute to handle the GSF role based security model. | |
| 38 | + /// </summary> | |
| 39 | + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)] | |
| 40 | + public class AuthorizeControllerRoleAttribute : FilterAttribute, IAuthorizationFilter, IExceptionFilter | |
| 41 | + { | |
| 42 | + #region [ Members ] | |
| 43 | + | |
| 44 | + // Fields | |
| 45 | + | |
| 46 | + /// <summary> | |
| 47 | + /// Gets allowed roles as a string array. | |
| 48 | + /// </summary> | |
| 49 | + public readonly string[] AllowedRoles; | |
| 50 | + | |
| 51 | + #endregion | |
| 52 | + | |
| 53 | + #region [ Constructors ] | |
| 54 | + | |
| 55 | + /// <summary> | |
| 56 | + /// Creates a new MVC based authorization attribute. | |
| 57 | + /// </summary> | |
| 58 | + public AuthorizeControllerRoleAttribute() | |
| 59 | + { | |
| 60 | + AllowedRoles = new string[0]; | |
| 61 | + SettingsCategory = "securityProvider"; | |
| 62 | + SecurityExceptionViewName = "SecurityError"; | |
| 63 | + } | |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// Creates a new MVC based authorization attribute for specified roles. | |
| 67 | + /// </summary> | |
| 68 | + public AuthorizeControllerRoleAttribute(string allowedRoles) | |
| 69 | + { | |
| 70 | + AllowedRoles = allowedRoles?.Split(',').Select(role => role.Trim()). | |
| 71 | + Where(role => !string.IsNullOrEmpty(role)).ToArray() ?? new string[0]; | |
| 72 | + | |
| 73 | + SettingsCategory = "securityProvider"; | |
| 74 | + SecurityExceptionViewName = "SecurityError"; | |
| 75 | + } | |
| 76 | + | |
| 77 | + #endregion | |
| 78 | + | |
| 79 | + #region [ Properties ] | |
| 80 | + | |
| 81 | + /// <summary> | |
| 82 | + /// Gets or sets settings category to use for loading data context for security info. | |
| 83 | + /// </summary> | |
| 84 | + public string SettingsCategory | |
| 85 | + { | |
| 86 | + get; set; | |
| 87 | + } | |
| 88 | + | |
| 89 | + /// <summary> | |
| 90 | + /// Gets or sets view name to show for security exceptions. | |
| 91 | + /// </summary> | |
| 92 | + public string SecurityExceptionViewName | |
| 93 | + { | |
| 94 | + get; set; | |
| 95 | + } | |
| 96 | + | |
| 97 | + #endregion | |
| 98 | + | |
| 99 | + #region [ Methods ] | |
| 100 | + | |
| 101 | + /// <summary> | |
| 102 | + /// Called when authorization is required. | |
| 103 | + /// </summary> | |
| 104 | + /// <param name="filterContext">The filter context.</param> | |
| 105 | + public void OnAuthorization(AuthorizationContext filterContext) | |
| 106 | + { | |
| 107 | + // Initialize the security principal from caller's windows identity if uninitialized. | |
| 108 | + if (SecurityProviderCache.CurrentProvider == null) | |
| 109 | + SecurityProviderCache.CurrentProvider = SecurityProviderUtility.CreateProvider(string.Empty); | |
| 110 | + | |
| 111 | + // Setup the principal | |
| 112 | + filterContext.HttpContext.User = Thread.CurrentPrincipal; | |
| 113 | + | |
| 114 | + // Get current user name | |
| 115 | + string userName = Thread.CurrentPrincipal.Identity.Name; | |
| 116 | + | |
| 117 | + // Verify that the current thread principal has been authenticated. | |
| 118 | + if (!Thread.CurrentPrincipal.Identity.IsAuthenticated) | |
| 119 | + throw new SecurityException($"Authentication failed for user '{userName}': {SecurityProviderCache.CurrentProvider.AuthenticationFailureReason}"); | |
| 120 | + | |
| 121 | + if (AllowedRoles.Length > 0 && !AllowedRoles.Any(role => filterContext.HttpContext.User.IsInRole(role))) | |
| 122 | + throw new SecurityException($"Access is denied for user '{userName}' defined: minimum required roles = {AllowedRoles.ToDelimitedString(", ")}."); | |
| 123 | + | |
| 124 | + // Make sure current user ID is cached | |
| 125 | + if (!AuthorizationCache.UserIDs.ContainsKey(userName)) | |
| 126 | + { | |
| 127 | + using (AdoDataConnection connection = new AdoDataConnection(SettingsCategory)) | |
| 128 | + { | |
| 129 | + AuthorizationCache.UserIDs.TryAdd(userName, connection.ExecuteScalar<Guid?>("SELECT ID FROM UserAccount WHERE Name={0}", UserInfo.UserNameToSID(userName)) ?? Guid.Empty); | |
| 130 | + } | |
| 131 | + } | |
| 132 | + } | |
| 133 | + | |
| 134 | + /// <summary> | |
| 135 | + /// Called when an exception occurs. | |
| 136 | + /// </summary> | |
| 137 | + /// <param name="filterContext">The filter context.</param> | |
| 138 | + public void OnException(ExceptionContext filterContext) | |
| 139 | + { | |
| 140 | + if (filterContext.Exception is SecurityException) | |
| 141 | + { | |
| 142 | + filterContext.Result = new ViewResult | |
| 143 | + { | |
| 144 | + ViewName = SecurityExceptionViewName, | |
| 145 | + ViewData = new ViewDataDictionary | |
| 146 | + { | |
| 147 | + ["Exception"] = filterContext.Exception | |
| 148 | + } | |
| 149 | + }; | |
| 150 | + filterContext.ExceptionHandled = true; | |
| 151 | + } | |
| 152 | + } | |
| 153 | + | |
| 154 | + #endregion | |
| 155 | + } | |
| 156 | +} | |
| @@ -0,0 +1,138 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// AuthorizeHubRole.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/25/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.Linq; | |
| 26 | +using System.Security; | |
| 27 | +using System.Security.Principal; | |
| 28 | +using System.Threading; | |
| 29 | +using GSF.Collections; | |
| 30 | +using GSF.Data; | |
| 31 | +using GSF.Identity; | |
| 32 | +using GSF.Security; | |
| 33 | +using Microsoft.AspNet.SignalR; | |
| 34 | + | |
| 35 | +namespace GSF.Web.Security | |
| 36 | +{ | |
| 37 | + /// <summary> | |
| 38 | + /// Defines a SignalR authorization attribute to handle the GSF role based security model. | |
| 39 | + /// </summary> | |
| 40 | + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)] | |
| 41 | + public class AuthorizeHubRoleAttribute : AuthorizeAttribute | |
| 42 | + { | |
| 43 | + #region [ Members ] | |
| 44 | + | |
| 45 | + // Fields | |
| 46 | + private string[] m_allowedRoles; | |
| 47 | + | |
| 48 | + #endregion | |
| 49 | + | |
| 50 | + #region [ Constructors ] | |
| 51 | + | |
| 52 | + /// <summary> | |
| 53 | + /// Creates a new <see cref="AuthorizeHubRoleAttribute"/>. | |
| 54 | + /// </summary> | |
| 55 | + public AuthorizeHubRoleAttribute() | |
| 56 | + { | |
| 57 | + SettingsCategory = "securityProvider"; | |
| 58 | + } | |
| 59 | + | |
| 60 | + /// <summary> | |
| 61 | + /// Creates a new <see cref="AuthorizeHubRoleAttribute"/> with specified allowed roles. | |
| 62 | + /// </summary> | |
| 63 | + public AuthorizeHubRoleAttribute(string allowedRoles) | |
| 64 | + { | |
| 65 | + Roles = allowedRoles; | |
| 66 | + SettingsCategory = "securityProvider"; | |
| 67 | + } | |
| 68 | + | |
| 69 | + #endregion | |
| 70 | + | |
| 71 | + #region [ Properties ] | |
| 72 | + | |
| 73 | + /// <summary> | |
| 74 | + /// Gets or sets settings category to use for loading data context for security info. | |
| 75 | + /// </summary> | |
| 76 | + public string SettingsCategory | |
| 77 | + { | |
| 78 | + get; set; | |
| 79 | + } | |
| 80 | + | |
| 81 | + /// <summary> | |
| 82 | + /// Gets the allowed <see cref="AuthorizeAttribute.Roles"/> as a string array. | |
| 83 | + /// </summary> | |
| 84 | + public string[] AllowedRoles => m_allowedRoles ?? (m_allowedRoles = Roles?.Split(',').Select(role => role.Trim()).Where(role => !string.IsNullOrEmpty(role)).ToArray() ?? new string[0]); | |
| 85 | + | |
| 86 | + #endregion | |
| 87 | + | |
| 88 | + #region [ Methods ] | |
| 89 | + | |
| 90 | + /// <summary> | |
| 91 | + /// Provides an entry point for custom authorization checks. | |
| 92 | + /// </summary> | |
| 93 | + /// <param name="user">The <see cref="IPrincipal"/> for the client being authorize</param> | |
| 94 | + /// <returns> | |
| 95 | + /// <c>true</c> if the user is authorized, otherwise, <c>false</c>. | |
| 96 | + /// </returns> | |
| 97 | + protected override bool UserAuthorized(IPrincipal user) | |
| 98 | + { | |
| 99 | + // Get current user name | |
| 100 | + string userName = user.Identity.Name; | |
| 101 | + | |
| 102 | + // Initialize the security principal from caller's windows identity if uninitialized, note that | |
| 103 | + // simply by checking current provider any existing cached security principal will be restored | |
| 104 | + if (SecurityProviderCache.CurrentProvider == null) | |
| 105 | + { | |
| 106 | + lock (typeof(AuthorizeHubRoleAttribute)) | |
| 107 | + { | |
| 108 | + // Let's see if we won the race... | |
| 109 | + if (SecurityProviderCache.CurrentProvider == null) | |
| 110 | + SecurityProviderCache.CurrentProvider = SecurityProviderUtility.CreateProvider(userName); | |
| 111 | + } | |
| 112 | + } | |
| 113 | + | |
| 114 | + // Setup the principal | |
| 115 | + user = Thread.CurrentPrincipal; | |
| 116 | + | |
| 117 | + // Verify that the current thread principal has been authenticated. | |
| 118 | + if (!Thread.CurrentPrincipal.Identity.IsAuthenticated) | |
| 119 | + throw new SecurityException($"Authentication failed for user '{userName}': {SecurityProviderCache.CurrentProvider.AuthenticationFailureReason}"); | |
| 120 | + | |
| 121 | + if (AllowedRoles.Length > 0 && !AllowedRoles.Any(role => user.IsInRole(role))) | |
| 122 | + throw new SecurityException($"Access is denied for user '{userName}' defined: minimum required roles = {AllowedRoles.ToDelimitedString(", ")}."); | |
| 123 | + | |
| 124 | + // Make sure current user ID is cached | |
| 125 | + if (!AuthorizationCache.UserIDs.ContainsKey(userName)) | |
| 126 | + { | |
| 127 | + using (AdoDataConnection connection = new AdoDataConnection(SettingsCategory)) | |
| 128 | + { | |
| 129 | + AuthorizationCache.UserIDs.TryAdd(userName, connection.ExecuteScalar<Guid?>("SELECT ID FROM UserAccount WHERE Name={0}", UserInfo.UserNameToSID(userName)) ?? Guid.Empty); | |
| 130 | + } | |
| 131 | + } | |
| 132 | + | |
| 133 | + return true; | |
| 134 | + } | |
| 135 | + | |
| 136 | + #endregion | |
| 137 | + } | |
| 138 | +} | |
| @@ -0,0 +1,292 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// SecurityHub.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 03/03/2016 - Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.Collections.Generic; | |
| 26 | +using GSF.Configuration; | |
| 27 | +using GSF.Data; | |
| 28 | +using GSF.Data.Model; | |
| 29 | +using GSF.Identity; | |
| 30 | +using GSF.Security.Model; | |
| 31 | +using GSF.Web.Model; | |
| 32 | +using Microsoft.AspNet.SignalR; | |
| 33 | +using Microsoft.AspNet.SignalR.Hubs; | |
| 34 | + | |
| 35 | +namespace GSF.Web.Security | |
| 36 | +{ | |
| 37 | + /// <summary> | |
| 38 | + /// Defines a SignalR security hub for managing users, groups and SID management. | |
| 39 | + /// </summary> | |
| 40 | + [AuthorizeHubRole] | |
| 41 | + public class SecurityHub : Hub | |
| 42 | + { | |
| 43 | + #region [ Members ] | |
| 44 | + | |
| 45 | + // Fields | |
| 46 | + private readonly DataContext m_dataContext; | |
| 47 | + private bool m_disposed; | |
| 48 | + | |
| 49 | + #endregion | |
| 50 | + | |
| 51 | + #region [ Constructors ] | |
| 52 | + | |
| 53 | + /// <summary> | |
| 54 | + /// Creates a new <see cref="SecurityHub"/>. | |
| 55 | + /// </summary> | |
| 56 | + public SecurityHub() | |
| 57 | + { | |
| 58 | + m_dataContext = new DataContext(); | |
| 59 | + } | |
| 60 | + | |
| 61 | + #endregion | |
| 62 | + | |
| 63 | + #region [ Methods ] | |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// Releases the unmanaged resources used by the <see cref="SecurityHub"/> object and optionally releases the managed resources. | |
| 67 | + /// </summary> | |
| 68 | + /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param> | |
| 69 | + protected override void Dispose(bool disposing) | |
| 70 | + { | |
| 71 | + if (!m_disposed) | |
| 72 | + { | |
| 73 | + try | |
| 74 | + { | |
| 75 | + if (disposing) | |
| 76 | + m_dataContext?.Dispose(); | |
| 77 | + } | |
| 78 | + finally | |
| 79 | + { | |
| 80 | + m_disposed = true; // Prevent duplicate dispose. | |
| 81 | + base.Dispose(disposing); // Call base class Dispose(). | |
| 82 | + } | |
| 83 | + } | |
| 84 | + } | |
| 85 | + | |
| 86 | + #endregion | |
| 87 | + | |
| 88 | + // Client-side script functionality | |
| 89 | + | |
| 90 | + /// <summary> | |
| 91 | + /// Gets the specified user account record. | |
| 92 | + /// </summary> | |
| 93 | + /// <param name="id">ID of requested user.</param> | |
| 94 | + /// <returns>Specified user account record.</returns> | |
| 95 | + public UserAccount QueryUserAccount(Guid id) | |
| 96 | + { | |
| 97 | + return m_dataContext.Table<UserAccount>().LoadRecord(id); | |
| 98 | + } | |
| 99 | + | |
| 100 | + /// <summary> | |
| 101 | + /// Gets the current application role records. | |
| 102 | + /// </summary> | |
| 103 | + /// <returns>Current application role records.</returns> | |
| 104 | + public IEnumerable<ApplicationRole> QueryApplicationRoles() | |
| 105 | + { | |
| 106 | + return m_dataContext.Table<ApplicationRole>().QueryRecords("Name", new RecordRestriction("NodeID={0}", DefaultNodeID)); | |
| 107 | + } | |
| 108 | + | |
| 109 | + /// <summary> | |
| 110 | + /// Determines if user is in role based on database ID values. | |
| 111 | + /// </summary> | |
| 112 | + /// <param name="userID">User ID value.</param> | |
| 113 | + /// <param name="roleID">Role ID value.</param> | |
| 114 | + /// <returns><c>true</c> if user is in role; otherwise, <c>false</c>.</returns> | |
| 115 | + public bool UserIsInRole(Guid userID, Guid roleID) | |
| 116 | + { | |
| 117 | + return m_dataContext.Table<ApplicationRoleUserAccount>().QueryRecordCount(new RecordRestriction("UserAccountID={0} AND ApplicationRoleID={1}", userID, roleID)) > 0; | |
| 118 | + } | |
| 119 | + | |
| 120 | + /// <summary> | |
| 121 | + /// Adds user to a role. | |
| 122 | + /// </summary> | |
| 123 | + /// <param name="userID">User ID value.</param> | |
| 124 | + /// <param name="roleID">Role ID value.</param> | |
| 125 | + /// <returns><c>true</c> if user was added; otherwise <c>false</c>.</returns> | |
| 126 | + [AuthorizeHubRole("Administrator")] | |
| 127 | + public bool AddUserToRole(Guid userID, Guid roleID) | |
| 128 | + { | |
| 129 | + // Nothing to do if user is already in role | |
| 130 | + if (UserIsInRole(userID, roleID)) | |
| 131 | + return false; | |
| 132 | + | |
| 133 | + return m_dataContext.Table<ApplicationRoleUserAccount>().AddNewRecord(new ApplicationRoleUserAccount | |
| 134 | + { | |
| 135 | + ApplicationRoleID = roleID, | |
| 136 | + UserAccountID = userID | |
| 137 | + }) > 0; | |
| 138 | + } | |
| 139 | + | |
| 140 | + /// <summary> | |
| 141 | + /// Removes user from a role. | |
| 142 | + /// </summary> | |
| 143 | + /// <param name="userID">User ID value.</param> | |
| 144 | + /// <param name="roleID">Role ID value.</param> | |
| 145 | + /// <returns><c>true</c> if user was removed; otherwise <c>false</c>.</returns> | |
| 146 | + [AuthorizeHubRole("Administrator")] | |
| 147 | + public bool RemoveUserFromRole(Guid userID, Guid roleID) | |
| 148 | + { | |
| 149 | + // Nothing to do if user is not currently in role | |
| 150 | + if (!UserIsInRole(userID, roleID)) | |
| 151 | + return false; | |
| 152 | + | |
| 153 | + return m_dataContext.Table<ApplicationRoleUserAccount>().DeleteRecord(new RecordRestriction("UserAccountID={0} AND ApplicationRoleID={1}", userID, roleID)) > 0; | |
| 154 | + } | |
| 155 | + | |
| 156 | + /// <summary> | |
| 157 | + /// Gets SID for a given user name. | |
| 158 | + /// </summary> | |
| 159 | + /// <param name="userName">User name to convert to SID.</param> | |
| 160 | + /// <returns>SID for a given user name.</returns> | |
| 161 | + public string UserNameToSID(string userName) | |
| 162 | + { | |
| 163 | + return UserInfo.UserNameToSID(userName); | |
| 164 | + } | |
| 165 | + | |
| 166 | + /// <summary> | |
| 167 | + /// Determines if SID is for a user. | |
| 168 | + /// </summary> | |
| 169 | + /// <param name="sid">Security identifier to test.</param> | |
| 170 | + /// <returns><c>true</c>if <paramref name="sid"/> is for a user; otherwise, <c>false</c>.</returns> | |
| 171 | + [HubMethodName("isUserSID")] | |
| 172 | + public bool IsUserSID(string sid) | |
| 173 | + { | |
| 174 | + return UserInfo.IsUserSID(sid); | |
| 175 | + } | |
| 176 | + | |
| 177 | + /// <summary> | |
| 178 | + /// Determines if group is in role based on database ID values. | |
| 179 | + /// </summary> | |
| 180 | + /// <param name="groupID">Group ID value.</param> | |
| 181 | + /// <param name="roleID">Role ID value.</param> | |
| 182 | + /// <returns><c>true</c> if group is in role; otherwise, <c>false</c>.</returns> | |
| 183 | + public bool GroupIsInRole(Guid groupID, Guid roleID) | |
| 184 | + { | |
| 185 | + return m_dataContext.Table<ApplicationRoleSecurityGroup>().QueryRecordCount(new RecordRestriction("SecurityGroupID={0} AND ApplicationRoleID={1}", groupID, roleID)) > 0; | |
| 186 | + } | |
| 187 | + | |
| 188 | + /// <summary> | |
| 189 | + /// Adds group to a role. | |
| 190 | + /// </summary> | |
| 191 | + /// <param name="groupID">Group ID value.</param> | |
| 192 | + /// <param name="roleID">Role ID value.</param> | |
| 193 | + /// <returns><c>true</c> if group was added; otherwise <c>false</c>.</returns> | |
| 194 | + [AuthorizeHubRole("Administrator")] | |
| 195 | + public bool AddGroupToRole(Guid groupID, Guid roleID) | |
| 196 | + { | |
| 197 | + // Nothing to do if group is already in role | |
| 198 | + if (GroupIsInRole(groupID, roleID)) | |
| 199 | + return false; | |
| 200 | + | |
| 201 | + return m_dataContext.Table<ApplicationRoleSecurityGroup>().AddNewRecord(new ApplicationRoleSecurityGroup | |
| 202 | + { | |
| 203 | + ApplicationRoleID = roleID, | |
| 204 | + SecurityGroupID = groupID | |
| 205 | + }) > 0; | |
| 206 | + } | |
| 207 | + | |
| 208 | + /// <summary> | |
| 209 | + /// Removes group from a role. | |
| 210 | + /// </summary> | |
| 211 | + /// <param name="groupID">Group ID value.</param> | |
| 212 | + /// <param name="roleID">Role ID value.</param> | |
| 213 | + /// <returns><c>true</c> if group was removed; otherwise <c>false</c>.</returns> | |
| 214 | + [AuthorizeHubRole("Administrator")] | |
| 215 | + public bool RemoveGroupFromRole(Guid groupID, Guid roleID) | |
| 216 | + { | |
| 217 | + // Nothing to do if group is not currently in role | |
| 218 | + if (!GroupIsInRole(groupID, roleID)) | |
| 219 | + return false; | |
| 220 | + | |
| 221 | + return m_dataContext.Table<ApplicationRoleSecurityGroup>().DeleteRecord(new RecordRestriction("SecurityGroupID={0} AND ApplicationRoleID={1}", groupID, roleID)) > 0; | |
| 222 | + } | |
| 223 | + | |
| 224 | + /// <summary> | |
| 225 | + /// Gets SID for a given group name. | |
| 226 | + /// </summary> | |
| 227 | + /// <param name="groupName">Group name to convert to SID.</param> | |
| 228 | + /// <returns>SID for a given group name.</returns> | |
| 229 | + public string GroupNameToSID(string groupName) | |
| 230 | + { | |
| 231 | + return UserInfo.GroupNameToSID(groupName ?? ""); | |
| 232 | + } | |
| 233 | + | |
| 234 | + /// <summary> | |
| 235 | + /// Determines if SID is for a group. | |
| 236 | + /// </summary> | |
| 237 | + /// <param name="sid">Security identifier to test.</param> | |
| 238 | + /// <returns><c>true</c>if <paramref name="sid"/> is for a group; otherwise, <c>false</c>.</returns> | |
| 239 | + [HubMethodName("isGroupSID")] | |
| 240 | + public bool IsGroupSID(string sid) | |
| 241 | + { | |
| 242 | + return UserInfo.IsGroupSID(sid); | |
| 243 | + } | |
| 244 | + | |
| 245 | + /// <summary> | |
| 246 | + /// Gets account name for a given SID. | |
| 247 | + /// </summary> | |
| 248 | + /// <param name="sid">SID to convert to a account name.</param> | |
| 249 | + /// <returns>Account name for a given SID.</returns> | |
| 250 | + [HubMethodName("sidToAccountName")] | |
| 251 | + public string SIDToAccountName(string sid) | |
| 252 | + { | |
| 253 | + return UserInfo.SIDToAccountName(sid ?? ""); | |
| 254 | + } | |
| 255 | + | |
| 256 | + #region [ Static ] | |
| 257 | + | |
| 258 | + // Static Fields | |
| 259 | + | |
| 260 | + /// <summary> | |
| 261 | + /// Gets current default Node ID for security. | |
| 262 | + /// </summary> | |
| 263 | + public static readonly Guid DefaultNodeID; | |
| 264 | + | |
| 265 | + // Static Constructor | |
| 266 | + static SecurityHub() | |
| 267 | + { | |
| 268 | + CategorizedSettingsElementCollection systemSettings = ConfigurationFile.Current.Settings["systemSettings"]; | |
| 269 | + | |
| 270 | + // Retrieve default NodeID | |
| 271 | + DefaultNodeID = Guid.Parse(systemSettings["NodeID"].Value.ToNonNullString(Guid.NewGuid().ToString())); | |
| 272 | + | |
| 273 | + // Determine whether the node exists in the database and create it if it doesn't | |
| 274 | + using (AdoDataConnection connection = new AdoDataConnection("systemSettings")) | |
| 275 | + { | |
| 276 | + const string NodeCountFormat = "SELECT COUNT(*) FROM Node"; | |
| 277 | + const string NodeInsertFormat = "INSERT INTO Node(Name, Description, Enabled) VALUES('Default', 'Default node', 1)"; | |
| 278 | + const string NodeUpdateFormat = "UPDATE Node SET ID = {0}"; | |
| 279 | + | |
| 280 | + int nodeCount = connection.ExecuteScalar<int?>(NodeCountFormat) ?? 0; | |
| 281 | + | |
| 282 | + if (nodeCount == 0) | |
| 283 | + { | |
| 284 | + connection.ExecuteNonQuery(NodeInsertFormat); | |
| 285 | + connection.ExecuteNonQuery(NodeUpdateFormat, connection.Guid(DefaultNodeID)); | |
| 286 | + } | |
| 287 | + } | |
| 288 | + } | |
| 289 | + | |
| 290 | + #endregion | |
| 291 | + } | |
| 292 | +} | |
| @@ -0,0 +1,370 @@ | ||
| 1 | +@******************************************************************************************************* | |
| 2 | +// Groups.cshtml - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 03/03/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//*****************************************************************************************************@ | |
| 23 | +@using GSF.Data.Model | |
| 24 | +@using GSF.Security.Model | |
| 25 | +@using GSF.Web.Model | |
| 26 | +@section StyleSheets{ | |
| 27 | + <style> | |
| 28 | + .account-feedback-wrapper { | |
| 29 | + position: relative; | |
| 30 | + display: table | |
| 31 | + } | |
| 32 | + | |
| 33 | + .account-feedback-message { | |
| 34 | + position: absolute; | |
| 35 | + vertical-align: middle; | |
| 36 | + display: table-cell; | |
| 37 | + left: 10px; | |
| 38 | + width: 425px; | |
| 39 | + display: none | |
| 40 | + } | |
| 41 | + | |
| 42 | + .account-feedback-icon { | |
| 43 | + font-size: large; | |
| 44 | + vertical-align: middle; | |
| 45 | + margin-top: -5px | |
| 46 | + } | |
| 47 | + </style> | |
| 48 | +} | |
| 49 | +@{ | |
| 50 | + DataContext dataContext = ViewBag.DataContext; | |
| 51 | + | |
| 52 | + // Define column headers, use null for field name to make column non-sortable | |
| 53 | + ViewBag.HeaderColumns = new[] | |
| 54 | + { // { "Field", "Label", "Classes" } | |
| 55 | + new[] { null, "Group Name", "text-center valign-middle" }, | |
| 56 | + new[] { null, "Is AD Group", "text-center" }, | |
| 57 | + new[] { "CreatedOn", "Date Added", "text-center" }, | |
| 58 | + new[] { null, "Group Roles", "text-center valign-middle" } | |
| 59 | + }; | |
| 60 | + | |
| 61 | + ViewBag.BodyRows = BodyRows().ToString(); | |
| 62 | + ViewBag.AddNewEditDialog = AddNewEditDialog(dataContext).ToString(); | |
| 63 | +} | |
| 64 | +@helper BodyRows() | |
| 65 | +{ | |
| 66 | + <td width="65%" class="text-center valign-middle" nowrap><button type="button" class="btn btn-link" data-bind="text: AccountName(), attr: {title: Name}, click: $parent.viewPageRecord"></button></td> | |
| 67 | + <td width="10%" class="text-center valign-middle"><input type="checkbox" id="checkEnabled" data-bind="checked: useADAuthentication(AccountName())" disabled></td> | |
| 68 | + <td width="10%" class="text-center valign-middle" data-bind="text: CreatedOn.formatDate(DateFormat)"></td> | |
| 69 | + <td width="10%" class="text-center valign-middle"><button type="button" class="btn btn-default btn-sm" data-bind="click: openRolesEditor.bind($data), enable: $parent.canEdit() && $parent.dataHubIsConnected()">Roles...</button></td> | |
| 70 | + <td width="5%" class="text-center valign-middle" nowrap> | |
| 71 | + <button type="button" class="btn btn-xs" data-bind="click: $parent.editPageRecord, enable: $parent.canEdit() && $parent.dataHubIsConnected()"><span class="glyphicon glyphicon-pencil"></span></button> | |
| 72 | + <button type="button" class="btn btn-xs" data-bind="click: $parent.removePageRecord, enable: $parent.canDelete() && $parent.dataHubIsConnected()"><span class="glyphicon glyphicon-remove"></span></button> | |
| 73 | + </td> | |
| 74 | +} | |
| 75 | +@helper AccountValidationFeedback() | |
| 76 | +{ | |
| 77 | + <div class="pull-right account-feedback-wrapper"> | |
| 78 | + <span id="resolvingAccount" class="account-feedback-message"><span class="glyphicon glyphicon-refresh glyphicon-spin account-feedback-icon"></span> <em class="small">Resolving account details...</em></span> | |
| 79 | + <span id="accountValid" class="account-feedback-message"><span class="glyphicon glyphicon-ok-circle account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'green'}"></span> <em class="small">Resolved account name</em></span> | |
| 80 | + <span id="accountInvalid" class="account-feedback-message"><span class="glyphicon glyphicon-remove-circle account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'red'}"></span> <em class="small">Cannot resolve account name</em></span> | |
| 81 | + <span id="accountUnknown" class="account-feedback-message"><span class="glyphicon glyphicon-warning-sign account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'orange'}"></span> <em class="small">Valid account name is not a group</em></span> | |
| 82 | + </div> | |
| 83 | +} | |
| 84 | +@helper AddNewEditDialog(DataContext dataContext) | |
| 85 | +{ | |
| 86 | + <div class="col-md-12"> | |
| 87 | + @Html.Raw(dataContext.AddInputField<SecurityGroup>("Name", fieldLabel: $"Name{AccountValidationFeedback().ToString().Trim()}", customDataBinding: "attr: {'acct-valid': validateAccount($data)}", initialFocus: true)) | |
| 88 | + <div class="panel panel-default" data-bind="css: {'panel-readonly': $root.recordMode()===RecordMode.View, 'panel-primary': UseDBAuthentication}"> | |
| 89 | + <div class="panel-heading"> | |
| 90 | + <div class="row"> | |
| 91 | + <div class="col-md-8"> | |
| 92 | + <div class="row"> | |
| 93 | + <div class="col-md-4"> | |
| 94 | + <label class="radio-inline"><input type="radio" name="radioUseDBAuthentication" data-bind="checkedValue: false, checked: UseDBAuthentication, attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}"> Active Directory Group</label> | |
| 95 | + </div> | |
| 96 | + <div class="col-md-8"> | |
| 97 | + <label class="radio-inline"><input type="radio" name="radioUseDBAuthentication" data-bind="checkedValue: true, checked: UseDBAuthentication, attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}"> Database Group</label> | |
| 98 | + </div> | |
| 99 | + </div> | |
| 100 | + </div> | |
| 101 | + <div class="col-md-4"> | |
| 102 | + </div> | |
| 103 | + </div> | |
| 104 | + </div> | |
| 105 | + <div class="panel-body" data-bind="visible: UseDBAuthentication"> | |
| 106 | + @Html.Raw(dataContext.AddTextAreaField<SecurityGroup>("Description", 4, dependencyFieldName: "UseDBAuthentication")) | |
| 107 | + </div> | |
| 108 | + </div> | |
| 109 | + </div> | |
| 110 | +} | |
| 111 | +@Html.Partial("PagedViewModel") | |
| 112 | +<div id="editRolesDialog" class="modal fade" role="dialog"> | |
| 113 | + <div class="modal-dialog"> | |
| 114 | + <div class="modal-content"> | |
| 115 | + <div class="modal-header"> | |
| 116 | + <button type="button" class="close" data-dismiss="modal">×</button> | |
| 117 | + <h4 class="modal-title">Edit Roles for <span id="currentGroupName"></span>:</h4> | |
| 118 | + </div> | |
| 119 | + <div class="modal-body"> | |
| 120 | + <div class="modal-body auto-height text-center"> | |
| 121 | + <form role="form"> | |
| 122 | + <input type="hidden" id="currentGroupID" /> | |
| 123 | + <div class="center-block"> | |
| 124 | + <div id="loadingRoles">Loading role assignments <span class="glyphicon glyphicon-refresh glyphicon-spin"></span><hr class="full-break" /></div> | |
| 125 | + <table class="table-condensed table-center"> | |
| 126 | + @foreach (ApplicationRole role in dataContext.Table<ApplicationRole>().QueryRecords("Name", new RecordRestriction("NodeID={0}", Model.Global.NodeID))) { | |
| 127 | + <tr> | |
| 128 | + <td class="text-left valign-middle"> | |
| 129 | + <div class="checkbox" style="margin: 0"> | |
| 130 | + <label for="@role.ID"><input type="checkbox" id="@role.ID"> @role.Description (@role.Name)</label> | |
| 131 | + </div> | |
| 132 | + </td> | |
| 133 | + </tr> | |
| 134 | + } | |
| 135 | + </table> | |
| 136 | + <br /> | |
| 137 | + <div class="text-center"> | |
| 138 | + <a href="@Url.Action("Help", "Main")#Roles" target="_blank">@Model.Global.ApplicationName Role Definitions</a> | |
| 139 | + </div> | |
| 140 | + </div> | |
| 141 | + </form> | |
| 142 | + </div> | |
| 143 | + </div> | |
| 144 | + <div class="modal-footer"> | |
| 145 | + <button type="submit" class="btn btn-primary" data-dismiss="modal" id="saveRolesButton" data-bind="enable: canEdit()">Save</button> | |
| 146 | + <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> | |
| 147 | + </div> | |
| 148 | + </div> | |
| 149 | + </div> | |
| 150 | +</div> | |
| 151 | +@section Scripts { | |
| 152 | + <script> | |
| 153 | + "use strict"; | |
| 154 | + | |
| 155 | + @Html.Raw(dataContext.RenderViewModelConfiguration<SecurityGroup>(ViewBag)) | |
| 156 | + var AccountState = { | |
| 157 | + Invalid: 0, | |
| 158 | + Valid: 1, | |
| 159 | + Unknown: 2, | |
| 160 | + Resolving: 3 | |
| 161 | + } | |
| 162 | + | |
| 163 | + function lookupAccountName(record) { | |
| 164 | + if (viewModel.dataHubIsConnected()) { | |
| 165 | + securityHub.sidToAccountName(record.Name).done(function (accountName) { | |
| 166 | + record.AccountName(accountName); | |
| 167 | + }); | |
| 168 | + } | |
| 169 | + } | |
| 170 | + | |
| 171 | + function validateAccount(observableRecord) { | |
| 172 | + showAccountNameValidityFeedback(AccountState.Resolving); | |
| 173 | + | |
| 174 | + if (observableRecord.UseDBAuthentication == null || observableRecord.UseDBAuthentication()) { | |
| 175 | + hideAccountNameValidityFeedback(); | |
| 176 | + } | |
| 177 | + else { | |
| 178 | + if (viewModel.dataHubIsConnected()) { | |
| 179 | + const accountName = observableRecord.Name(); | |
| 180 | + | |
| 181 | + if (isEmpty(accountName)) { | |
| 182 | + showAccountNameValidityFeedback(AccountState.Invalid); | |
| 183 | + } else { | |
| 184 | + securityHub.groupNameToSID(accountName).done(function (sid) { | |
| 185 | + if (accountName !== sid && accountName.countOccurrences("\\") < 2) { | |
| 186 | + if (accountName.startsWith("NT AUTHORITY")) { | |
| 187 | + // This is a valid group source for things like "Authenticated Users", but is | |
| 188 | + // not technically a "group" - since SID validated, we report a valid account | |
| 189 | + showAccountNameValidityFeedback(AccountState.Valid); | |
| 190 | + } else { | |
| 191 | + securityHub.isGroupSID(sid).done(function (isGroup) { | |
| 192 | + showAccountNameValidityFeedback(isGroup ? AccountState.Valid : AccountState.Unknown); | |
| 193 | + }); | |
| 194 | + } | |
| 195 | + } else { | |
| 196 | + showAccountNameValidityFeedback(AccountState.Invalid); | |
| 197 | + } | |
| 198 | + }); | |
| 199 | + } | |
| 200 | + } | |
| 201 | + else { | |
| 202 | + hideAccountNameValidityFeedback(); | |
| 203 | + } | |
| 204 | + } | |
| 205 | + | |
| 206 | + return undefined; | |
| 207 | + } | |
| 208 | + | |
| 209 | + var messages = []; | |
| 210 | + var nextMessageName = undefined; | |
| 211 | + var showingMessage = false; | |
| 212 | + messages.push("resolvingAccount"); | |
| 213 | + messages.push("accountValid"); | |
| 214 | + messages.push("accountInvalid"); | |
| 215 | + messages.push("accountUnknown"); | |
| 216 | + | |
| 217 | + function showFeedbackMessage(messageName) { | |
| 218 | + if (showingMessage) { | |
| 219 | + nextMessageName = messageName; | |
| 220 | + } else { | |
| 221 | + showingMessage = true; | |
| 222 | + | |
| 223 | + hideAccountNameValidityFeedback(messageName); | |
| 224 | + | |
| 225 | + if (messageName == null) | |
| 226 | + showingMessage = false; | |
| 227 | + else | |
| 228 | + $("#" + messageName).fadeIn(function () { | |
| 229 | + showingMessage = false; | |
| 230 | + | |
| 231 | + if (nextMessageName != undefined) { | |
| 232 | + messageName = nextMessageName; | |
| 233 | + nextMessageName = undefined; | |
| 234 | + showFeedbackMessage(messageName); | |
| 235 | + } | |
| 236 | + }); | |
| 237 | + } | |
| 238 | + } | |
| 239 | + | |
| 240 | + function hideAccountNameValidityFeedback(messageName) { | |
| 241 | + for (let i = 0; i < messages.length; i++) { | |
| 242 | + if (messages[i] !== messageName) | |
| 243 | + $("#" + messages[i]).hide(); | |
| 244 | + } | |
| 245 | + } | |
| 246 | + | |
| 247 | + function showAccountNameValidityFeedback(accountState) { | |
| 248 | + switch (accountState) { | |
| 249 | + case AccountState.Valid: | |
| 250 | + showFeedbackMessage("accountValid"); | |
| 251 | + break; | |
| 252 | + case AccountState.Invalid: | |
| 253 | + showFeedbackMessage("accountInvalid"); | |
| 254 | + break; | |
| 255 | + case AccountState.Unknown: | |
| 256 | + showFeedbackMessage("accountUnknown"); | |
| 257 | + break; | |
| 258 | + case AccountState.Resolving: | |
| 259 | + showFeedbackMessage("resolvingAccount"); | |
| 260 | + break; | |
| 261 | + } | |
| 262 | + } | |
| 263 | + | |
| 264 | + function useADAuthentication(accountName) | |
| 265 | + { | |
| 266 | + const groupName = notNull(accountName); | |
| 267 | + | |
| 268 | + // Just deriving if group is AD based by delimiters in group name | |
| 269 | + if (groupName.length > 0) | |
| 270 | + return (groupName.indexOf("\\") > 0 || groupName.indexOf("@@") > 0 || groupName.startsWith("S-")); | |
| 271 | + | |
| 272 | + return true; // Default to AD group | |
| 273 | + } | |
| 274 | + | |
| 275 | + $(viewModel).on("pageRecordsQueried", function (event, records) { | |
| 276 | + // Add a virtual observable field called "AccountName" that will resolve SID values to account names | |
| 277 | + for (var i = 0; i < records.length; i++) { | |
| 278 | + records[i].AccountName = ko.observable("..."); | |
| 279 | + lookupAccountName(records[i]); | |
| 280 | + } | |
| 281 | + }); | |
| 282 | + | |
| 283 | + $(viewModel).on("newRecord", function (event, newRecord) { | |
| 284 | + // Make sure new records get virtual fields | |
| 285 | + newRecord.AccountName = ko.observable(""); | |
| 286 | + }); | |
| 287 | + | |
| 288 | + $(viewModel).on("derivingObservableRecord", function (event, observableRecord) { | |
| 289 | + // Load human readable account name into Name field | |
| 290 | + if (observableRecord.AccountName) | |
| 291 | + observableRecord.Name(observableRecord.AccountName()); | |
| 292 | + | |
| 293 | + // Create a virtual UseDBAuthentication field | |
| 294 | + observableRecord.UseDBAuthentication = ko.observable(!useADAuthentication(notNull(observableRecord.Name()))); | |
| 295 | + }); | |
| 296 | + | |
| 297 | + $(viewModel).on("derivingJSRecord", function (event, observableRecord, promises) { | |
| 298 | + const isADGroup = !observableRecord.UseDBAuthentication(); | |
| 299 | + | |
| 300 | + // Remove virtual observable fields before serialization for record updates | |
| 301 | + delete observableRecord.UseDBAuthentication; | |
| 302 | + delete observableRecord.AccountName; | |
| 303 | + | |
| 304 | + // Push promise to convert group name back to SID before updating record | |
| 305 | + if (viewModel.dataHubIsConnected() && isADGroup) { | |
| 306 | + promises.push($.Deferred(function (deferred) { | |
| 307 | + securityHub.groupNameToSID(observableRecord.Name()).done(function (sid) { | |
| 308 | + observableRecord.Name(sid); | |
| 309 | + deferred.resolve(); | |
| 310 | + }); | |
| 311 | + }).promise()); | |
| 312 | + } | |
| 313 | + }); | |
| 314 | + | |
| 315 | + function openRolesEditor(record) { | |
| 316 | + $("#currentGroupID").text(record.ID); | |
| 317 | + $("#currentGroupName").text(record.AccountName()); | |
| 318 | + $("#editRolesDialog input:checkbox").prop("checked", false); | |
| 319 | + | |
| 320 | + if (viewModel.dataHubIsConnected()) { | |
| 321 | + securityHub.queryApplicationRoles().done(function (roles) { | |
| 322 | + var evaluations = roles.length; | |
| 323 | + | |
| 324 | + if (evaluations > 0) | |
| 325 | + $("#loadingRoles").show(); | |
| 326 | + | |
| 327 | + for (var i = 0; i < roles.length; i++) { | |
| 328 | + securityHub.groupIsInRole(record.ID, roles[i].ID).done(function (roleID) { | |
| 329 | + return function (result) { | |
| 330 | + $("#" + roleID).prop("checked", result); | |
| 331 | + | |
| 332 | + evaluations--; | |
| 333 | + | |
| 334 | + if (evaluations <= 0) | |
| 335 | + $("#loadingRoles").hide(); | |
| 336 | + }; | |
| 337 | + }(roles[i].ID)); | |
| 338 | + } | |
| 339 | + $("#editRolesDialog").modal("show"); | |
| 340 | + }); | |
| 341 | + } | |
| 342 | + } | |
| 343 | + | |
| 344 | + $(function () { | |
| 345 | + $("#editRolesDialog").modal({ show: false, backdrop: "static" }); | |
| 346 | + | |
| 347 | + $("#saveRolesButton").click(function () { | |
| 348 | + if (viewModel.dataHubIsConnected()) { | |
| 349 | + const groupID = $("#currentGroupID").text(); | |
| 350 | + const roles = $("#editRolesDialog input:checkbox"); | |
| 351 | + | |
| 352 | + for (var i = 0; i < roles.length; i++) { | |
| 353 | + const element = $(roles[i]); | |
| 354 | + const roleID = element.attr("id"); | |
| 355 | + | |
| 356 | + if (element.prop("checked")) { | |
| 357 | + securityHub.addGroupToRole(groupID, roleID).fail(function (error) { | |
| 358 | + showErrorMessage(error); | |
| 359 | + }); | |
| 360 | + } else { | |
| 361 | + securityHub.removeGroupFromRole(groupID, roleID).fail(function (error) { | |
| 362 | + showErrorMessage(error); | |
| 363 | + }); | |
| 364 | + } | |
| 365 | + } | |
| 366 | + } | |
| 367 | + }); | |
| 368 | + }); | |
| 369 | + </script> | |
| 370 | +} | |
| @@ -0,0 +1,381 @@ | ||
| 1 | +@******************************************************************************************************* | |
| 2 | +// Users.cshtml - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 03/03/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//*****************************************************************************************************@ | |
| 23 | +@using GSF.Data.Model | |
| 24 | +@using GSF.Security.Model | |
| 25 | +@using GSF.Web.Model | |
| 26 | +@section StyleSheets{ | |
| 27 | + <style> | |
| 28 | + .account-feedback-wrapper { | |
| 29 | + position: relative; | |
| 30 | + display: table | |
| 31 | + } | |
| 32 | + | |
| 33 | + .account-feedback-message { | |
| 34 | + position: absolute; | |
| 35 | + vertical-align: middle; | |
| 36 | + display: table-cell; | |
| 37 | + left: 10px; | |
| 38 | + width: 425px; | |
| 39 | + display: none | |
| 40 | + } | |
| 41 | + | |
| 42 | + .account-feedback-icon { | |
| 43 | + font-size: large; | |
| 44 | + vertical-align: middle; | |
| 45 | + margin-top: -5px | |
| 46 | + } | |
| 47 | + </style> | |
| 48 | +} | |
| 49 | +@{ | |
| 50 | + DataContext dataContext = Model.DataContext; | |
| 51 | + | |
| 52 | + // Define column headers, use null for field name to make column non-sortable | |
| 53 | + ViewBag.HeaderColumns = new[] | |
| 54 | + { // { "Field", "Label", "Classes" } | |
| 55 | + new[] { null, "User Name", "text-center valign-middle" }, | |
| 56 | + new[] { "UseADAuthentication", "Is AD User", "text-center" }, | |
| 57 | + new[] { "CreatedOn", "Date Added", "text-center" }, | |
| 58 | + new[] { null, "User Roles", "text-center valign-middle" } | |
| 59 | + }; | |
| 60 | + | |
| 61 | + ViewBag.BodyRows = BodyRows().ToString(); | |
| 62 | + ViewBag.AddNewEditDialog = AddNewEditDialog(dataContext).ToString(); | |
| 63 | +} | |
| 64 | +@helper BodyRows() | |
| 65 | +{ | |
| 66 | + <td width="65%" class="text-center valign-middle" nowrap><button type="button" class="btn btn-link" data-bind="text: AccountName(), attr: {title: Name}, click: $parent.viewPageRecord"></button></td> | |
| 67 | + <td width="10%" class="text-center valign-middle"><input type="checkbox" id="checkEnabled" data-bind="checked: UseADAuthentication" disabled></td> | |
| 68 | + <td width="10%" class="text-center valign-middle" data-bind="text: CreatedOn.formatDate(DateFormat)"></td> | |
| 69 | + <td width="10%" class="text-center valign-middle"><button type="button" class="btn btn-default btn-sm" data-bind="click: openRolesEditor.bind($data), enable: $parent.canEdit() && $parent.dataHubIsConnected()">Roles...</button></td> | |
| 70 | + <td width="5%" class="text-center valign-middle" nowrap> | |
| 71 | + <button type="button" class="btn btn-xs" data-bind="click: $parent.editPageRecord, enable: $parent.canEdit() && $parent.dataHubIsConnected()"><span class="glyphicon glyphicon-pencil"></span></button> | |
| 72 | + <button type="button" class="btn btn-xs" data-bind="click: $parent.removePageRecord, enable: $parent.canDelete() && $parent.dataHubIsConnected()"><span class="glyphicon glyphicon-remove"></span></button> | |
| 73 | + </td> | |
| 74 | +} | |
| 75 | +@helper AccountValidationFeedback() | |
| 76 | +{ | |
| 77 | + <div class="pull-right account-feedback-wrapper"> | |
| 78 | + <span id="resolvingAccount" class="account-feedback-message"><span class="glyphicon glyphicon-refresh glyphicon-spin account-feedback-icon"></span> <em class="small">Resolving account details...</em></span> | |
| 79 | + <span id="accountValid" class="account-feedback-message"><span class="glyphicon glyphicon-ok-circle account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'green'}"></span> <em class="small">Resolved account name</em></span> | |
| 80 | + <span id="accountInvalid" class="account-feedback-message"><span class="glyphicon glyphicon-remove-circle account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'red'}"></span> <em class="small">Cannot resolve account name</em></span> | |
| 81 | + <span id="accountUnknown" class="account-feedback-message"><span class="glyphicon glyphicon-warning-sign account-feedback-icon" data-bind="style: {color: $root.recordMode()===RecordMode.View ? 'gray' : 'orange'}"></span> <em class="small">Valid account name is not a user or Active Directory access is limited</em></span> | |
| 82 | + </div> | |
| 83 | +} | |
| 84 | +@helper AddNewEditDialog(DataContext dataContext) | |
| 85 | +{ | |
| 86 | + <div class="col-md-12"> | |
| 87 | + @Html.Raw(dataContext.AddInputField<UserAccount>("Name", fieldLabel: $"Name{AccountValidationFeedback().ToString().Trim()}", customDataBinding: "attr: {'acct-valid': validateAccount($data)}", initialFocus: true)) | |
| 88 | + <div class="panel panel-default" data-bind="css: {'panel-readonly': $root.recordMode()===RecordMode.View, 'panel-primary': UseDBAuthentication}"> | |
| 89 | + <div class="panel-heading"> | |
| 90 | + @* Adding actual UseADAuthentication field check-box, but hidden, to pick-up any model based field initializers *@ | |
| 91 | + <div style="display: none"> | |
| 92 | + @Html.Raw(dataContext.AddCheckBoxField<UserAccount>("UseADAuthentication")) | |
| 93 | + </div> | |
| 94 | + @* Adding radio buttons bound to virtual UseDBAuthentication that models negated state of UseADAuthentication field for simplified dependency field initialization *@ | |
| 95 | + <div class="row"> | |
| 96 | + <div class="col-md-8"> | |
| 97 | + <div class="row"> | |
| 98 | + <div class="col-md-4"> | |
| 99 | + <label class="radio-inline"><input type="radio" name="radioUseDBAuthentication" data-bind="checkedValue: false, checked: UseDBAuthentication, attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}"> Active Directory Group</label> | |
| 100 | + </div> | |
| 101 | + <div class="col-md-8"> | |
| 102 | + <label class="radio-inline"><input type="radio" name="radioUseDBAuthentication" data-bind="checkedValue: true, checked: UseDBAuthentication, attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}"> Database Group</label> | |
| 103 | + </div> | |
| 104 | + </div> | |
| 105 | + </div> | |
| 106 | + <div class="col-md-4"> | |
| 107 | + </div> | |
| 108 | + </div> | |
| 109 | + </div> | |
| 110 | + <div class="panel-body" data-bind="visible: UseDBAuthentication"> | |
| 111 | + <div class="row"> | |
| 112 | + <div class="col-md-6"> | |
| 113 | + @Html.Raw(dataContext.AddInputField<UserAccount>("Password", "password", dependencyFieldName: "UseDBAuthentication")) | |
| 114 | + @Html.Raw(dataContext.AddInputField<UserAccount>("FirstName", dependencyFieldName: "UseDBAuthentication")) | |
| 115 | + @Html.Raw(dataContext.AddInputField<UserAccount>("LastName", dependencyFieldName: "UseDBAuthentication")) | |
| 116 | + </div> | |
| 117 | + <div class="col-md-6"> | |
| 118 | + @Html.Raw(dataContext.AddInputField<UserAccount>("Phone", dependencyFieldName: "UseDBAuthentication")) | |
| 119 | + @Html.Raw(dataContext.AddInputField<UserAccount>("Email", dependencyFieldName: "UseDBAuthentication")) | |
| 120 | + @Html.Raw(dataContext.AddDateField<UserAccount>("ChangePasswordOn", dependencyFieldName: "UseDBAuthentication")) | |
| 121 | + @{ | |
| 122 | + Model.AddPageDefinedFieldInitialization("Password", "\"\"", | |
| 123 | + /* RegEx Pattern: */ Model.Global.PasswordRequirementsRegex, | |
| 124 | + /* Error Message: */ Model.Global.PasswordRequirementsError); | |
| 125 | + Model.AddPageDefinedFieldInitialization("ChangePasswordOn", "(new Date()).addDays(90)"); | |
| 126 | + } | |
| 127 | + </div> | |
| 128 | + </div> | |
| 129 | + </div> | |
| 130 | + </div> | |
| 131 | + <div class="pull-right"> | |
| 132 | + @Html.Raw(dataContext.AddCheckBoxField<UserAccount>("LockedOut")) | |
| 133 | + </div> | |
| 134 | + </div> | |
| 135 | +} | |
| 136 | +@Html.Partial("PagedViewModel") | |
| 137 | +<div id="editRolesDialog" class="modal fade" role="dialog"> | |
| 138 | + <div class="modal-dialog"> | |
| 139 | + <div class="modal-content"> | |
| 140 | + <div class="modal-header"> | |
| 141 | + <button type="button" class="close" data-dismiss="modal">×</button> | |
| 142 | + <h4 class="modal-title">Edit Roles for <span id="currentUserName"></span>:</h4> | |
| 143 | + </div> | |
| 144 | + <div class="modal-body"> | |
| 145 | + <div class="modal-body auto-height text-center"> | |
| 146 | + <form role="form"> | |
| 147 | + <input type="hidden" id="currentUserID" /> | |
| 148 | + <div class="center-block"> | |
| 149 | + <div id="loadingRoles">Loading role assignments <span class="glyphicon glyphicon-refresh glyphicon-spin"></span><hr class="full-break" /></div> | |
| 150 | + <table class="table-condensed table-center"> | |
| 151 | + @foreach (ApplicationRole role in dataContext.Table<ApplicationRole>().QueryRecords("Name", new RecordRestriction("NodeID={0}", Model.Global.NodeID))) { | |
| 152 | + <tr> | |
| 153 | + <td class="text-left valign-middle"> | |
| 154 | + <div class="checkbox" style="margin: 0"> | |
| 155 | + <label for="@role.ID"><input type="checkbox" id="@role.ID"> @role.Description (@role.Name)</label> | |
| 156 | + </div> | |
| 157 | + </td> | |
| 158 | + </tr> | |
| 159 | + } | |
| 160 | + </table> | |
| 161 | + <br /> | |
| 162 | + <div class="text-center"> | |
| 163 | + <a href="@Url.Action("Help", "Main")#Roles" target="_blank">@Model.Global.ApplicationName Role Definitions</a> | |
| 164 | + </div> | |
| 165 | + </div> | |
| 166 | + </form> | |
| 167 | + </div> | |
| 168 | + </div> | |
| 169 | + <div class="modal-footer"> | |
| 170 | + <button type="submit" class="btn btn-primary" data-dismiss="modal" id="saveRolesButton" data-bind="enable: canEdit()">Save</button> | |
| 171 | + <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> | |
| 172 | + </div> | |
| 173 | + </div> | |
| 174 | + </div> | |
| 175 | +</div> | |
| 176 | +@section Scripts { | |
| 177 | + <script> | |
| 178 | + "use strict"; | |
| 179 | + | |
| 180 | + @Html.Raw(dataContext.RenderViewModelConfiguration<UserAccount>(ViewBag)) | |
| 181 | + var AccountState = { | |
| 182 | + Invalid: 0, | |
| 183 | + Valid: 1, | |
| 184 | + Unknown: 2, | |
| 185 | + Resolving: 3 | |
| 186 | + } | |
| 187 | + | |
| 188 | + function lookupAccountName(record) { | |
| 189 | + if (viewModel.dataHubIsConnected()) { | |
| 190 | + securityHub.sidToAccountName(record.Name).done(function(accountName) { | |
| 191 | + record.AccountName(accountName); | |
| 192 | + }); | |
| 193 | + } | |
| 194 | + } | |
| 195 | + | |
| 196 | + function validateAccount(observableRecord) { | |
| 197 | + showAccountNameValidityFeedback(AccountState.Resolving); | |
| 198 | + | |
| 199 | + if (observableRecord.UseDBAuthentication == null || observableRecord.UseDBAuthentication()) { | |
| 200 | + hideAccountNameValidityFeedback(); | |
| 201 | + } else { | |
| 202 | + if (viewModel.dataHubIsConnected()) { | |
| 203 | + const accountName = observableRecord.Name(); | |
| 204 | + | |
| 205 | + if (isEmpty(accountName)) { | |
| 206 | + showAccountNameValidityFeedback(AccountState.Invalid); | |
| 207 | + } else { | |
| 208 | + securityHub.userNameToSID(accountName).done(function(sid) { | |
| 209 | + if (accountName !== sid && accountName.countOccurrences("\\") < 2) { | |
| 210 | + securityHub.isUserSID(sid).done(function(isUser) { | |
| 211 | + showAccountNameValidityFeedback(isUser ? AccountState.Valid : AccountState.Unknown); | |
| 212 | + }); | |
| 213 | + } else { | |
| 214 | + showAccountNameValidityFeedback(AccountState.Invalid); | |
| 215 | + } | |
| 216 | + }); | |
| 217 | + } | |
| 218 | + } else { | |
| 219 | + hideAccountNameValidityFeedback(); | |
| 220 | + } | |
| 221 | + } | |
| 222 | + | |
| 223 | + return undefined; | |
| 224 | + } | |
| 225 | + | |
| 226 | + var messages = []; | |
| 227 | + var nextMessageName = undefined; | |
| 228 | + var showingMessage = false; | |
| 229 | + messages.push("resolvingAccount"); | |
| 230 | + messages.push("accountValid"); | |
| 231 | + messages.push("accountInvalid"); | |
| 232 | + messages.push("accountUnknown"); | |
| 233 | + | |
| 234 | + function showFeedbackMessage(messageName) { | |
| 235 | + if (showingMessage) { | |
| 236 | + nextMessageName = messageName; | |
| 237 | + } else { | |
| 238 | + showingMessage = true; | |
| 239 | + | |
| 240 | + hideAccountNameValidityFeedback(messageName); | |
| 241 | + | |
| 242 | + if (messageName == null) | |
| 243 | + showingMessage = false; | |
| 244 | + else | |
| 245 | + $("#" + messageName).fadeIn(function () { | |
| 246 | + showingMessage = false; | |
| 247 | + | |
| 248 | + if (nextMessageName != undefined) { | |
| 249 | + messageName = nextMessageName; | |
| 250 | + nextMessageName = undefined; | |
| 251 | + showFeedbackMessage(messageName); | |
| 252 | + } | |
| 253 | + }); | |
| 254 | + } | |
| 255 | + } | |
| 256 | + | |
| 257 | + function hideAccountNameValidityFeedback(messageName) { | |
| 258 | + for (let i = 0; i < messages.length; i++) { | |
| 259 | + if (messages[i] !== messageName) | |
| 260 | + $("#" + messages[i]).hide(); | |
| 261 | + } | |
| 262 | + } | |
| 263 | + | |
| 264 | + function showAccountNameValidityFeedback(accountState) { | |
| 265 | + switch (accountState) { | |
| 266 | + case AccountState.Valid: | |
| 267 | + showFeedbackMessage("accountValid"); | |
| 268 | + break; | |
| 269 | + case AccountState.Invalid: | |
| 270 | + showFeedbackMessage("accountInvalid"); | |
| 271 | + break; | |
| 272 | + case AccountState.Unknown: | |
| 273 | + showFeedbackMessage("accountUnknown"); | |
| 274 | + break; | |
| 275 | + case AccountState.Resolving: | |
| 276 | + showFeedbackMessage("resolvingAccount"); | |
| 277 | + break; | |
| 278 | + } | |
| 279 | + } | |
| 280 | + | |
| 281 | + $(viewModel).on("pageRecordsQueried", function(event, records) { | |
| 282 | + // Add a virtual observable field called "AccountName" that will resolve SID values to account names | |
| 283 | + for (var i = 0; i < records.length; i++) { | |
| 284 | + records[i].AccountName = ko.observable("..."); | |
| 285 | + lookupAccountName(records[i]); | |
| 286 | + } | |
| 287 | + }); | |
| 288 | + | |
| 289 | + $(viewModel).on("newRecord", function(event, newRecord) { | |
| 290 | + // Make sure new records get virtual field | |
| 291 | + newRecord.AccountName = ko.observable(""); | |
| 292 | + }); | |
| 293 | + | |
| 294 | + $(viewModel).on("derivingObservableRecord", function(event, observableRecord) { | |
| 295 | + // Create a virtual UseDBAuthentication field that is negated state of actual UseADAuthentication field | |
| 296 | + observableRecord.UseDBAuthentication = ko.observable(!observableRecord.UseADAuthentication()); | |
| 297 | + | |
| 298 | + // Load human readable account name into Name field | |
| 299 | + if (observableRecord.AccountName) | |
| 300 | + observableRecord.Name(observableRecord.AccountName()); | |
| 301 | + | |
| 302 | + @* Cannot decrypt a password hash, have to clear it and force re-entry *@ | |
| 303 | + if (!observableRecord.UseADAuthentication()) | |
| 304 | + observableRecord.Password(null); | |
| 305 | + }); | |
| 306 | + | |
| 307 | + $(viewModel).on("derivingJSRecord", function(event, observableRecord, promises) { | |
| 308 | + // Set state of actual field to be negated state of virtual field | |
| 309 | + observableRecord.UseADAuthentication(!observableRecord.UseDBAuthentication()); | |
| 310 | + | |
| 311 | + // Remove virtual observable fields before serialization for record updates | |
| 312 | + delete observableRecord.UseDBAuthentication; | |
| 313 | + delete observableRecord.AccountName; | |
| 314 | + | |
| 315 | + // Push promise to convert user name back to SID before updating record | |
| 316 | + if (viewModel.dataHubIsConnected() && observableRecord.UseADAuthentication()) { | |
| 317 | + promises.push($.Deferred(function(deferred) { | |
| 318 | + securityHub.userNameToSID(observableRecord.Name()).done(function(sid) { | |
| 319 | + observableRecord.Name(sid); | |
| 320 | + deferred.resolve(); | |
| 321 | + }); | |
| 322 | + }).promise()); | |
| 323 | + } | |
| 324 | + }); | |
| 325 | + | |
| 326 | + function openRolesEditor(record) { | |
| 327 | + $("#currentUserID").text(record.ID); | |
| 328 | + $("#currentUserName").text(record.AccountName()); | |
| 329 | + $("#editRolesDialog input:checkbox").prop("checked", false); | |
| 330 | + | |
| 331 | + if (viewModel.dataHubIsConnected()) { | |
| 332 | + securityHub.queryApplicationRoles().done(function(roles) { | |
| 333 | + var evaluations = roles.length; | |
| 334 | + | |
| 335 | + if (evaluations > 0) | |
| 336 | + $("#loadingRoles").show(); | |
| 337 | + | |
| 338 | + for (var i = 0; i < roles.length; i++) { | |
| 339 | + securityHub.userIsInRole(record.ID, roles[i].ID).done(function(roleID) { | |
| 340 | + return function(result) { | |
| 341 | + $("#" + roleID).prop("checked", result); | |
| 342 | + | |
| 343 | + evaluations--; | |
| 344 | + | |
| 345 | + if (evaluations <= 0) | |
| 346 | + $("#loadingRoles").hide(); | |
| 347 | + }; | |
| 348 | + }(roles[i].ID)); | |
| 349 | + } | |
| 350 | + $("#editRolesDialog").modal("show"); | |
| 351 | + }); | |
| 352 | + } | |
| 353 | + } | |
| 354 | + | |
| 355 | + $(function() { | |
| 356 | + $("#editRolesDialog").modal({ show: false, backdrop: "static" }); | |
| 357 | + | |
| 358 | + $("#saveRolesButton").click(function() { | |
| 359 | + if (viewModel.dataHubIsConnected()) { | |
| 360 | + const userID = $("#currentUserID").text(); | |
| 361 | + const roles = $("#editRolesDialog input:checkbox"); | |
| 362 | + | |
| 363 | + for (var i = 0; i < roles.length; i++) { | |
| 364 | + const element = $(roles[i]); | |
| 365 | + const roleID = element.attr("id"); | |
| 366 | + | |
| 367 | + if (element.prop("checked")) { | |
| 368 | + securityHub.addUserToRole(userID, roleID).fail(function(error) { | |
| 369 | + showErrorMessage(error); | |
| 370 | + }); | |
| 371 | + } else { | |
| 372 | + securityHub.removeUserFromRole(userID, roleID).fail(function(error) { | |
| 373 | + showErrorMessage(error); | |
| 374 | + }); | |
| 375 | + } | |
| 376 | + } | |
| 377 | + } | |
| 378 | + }); | |
| 379 | + }); | |
| 380 | + </script> | |
| 381 | +} | |
| @@ -0,0 +1,35 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// NamespaceDoc.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2012, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://www.opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 03/14/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System.Runtime.CompilerServices; | |
| 25 | + | |
| 26 | +namespace GSF.Web.Templating | |
| 27 | +{ | |
| 28 | + /// <summary> | |
| 29 | + /// Contains classes that provide template based functionality using Razor syntax. | |
| 30 | + /// </summary> | |
| 31 | + [CompilerGenerated] | |
| 32 | + class NamespaceDoc | |
| 33 | + { | |
| 34 | + } | |
| 35 | +} | |
| @@ -0,0 +1,461 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// RazorView.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 01/13/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.Collections.Generic; | |
| 26 | +using System.IO; | |
| 27 | +using System.Net.Http; | |
| 28 | +using System.Reflection; | |
| 29 | +using System.Threading.Tasks; | |
| 30 | +using System.Web; | |
| 31 | +using System.Web.Hosting; | |
| 32 | +using GSF.Collections; | |
| 33 | +using GSF.Configuration; | |
| 34 | +using GSF.IO; | |
| 35 | +using GSF.Web.Model; | |
| 36 | +using RazorEngine; | |
| 37 | +using RazorEngine.Configuration; | |
| 38 | +using RazorEngine.Templating; | |
| 39 | + | |
| 40 | +// ReSharper disable StaticMemberInGenericType | |
| 41 | +namespace GSF.Web.Templating | |
| 42 | +{ | |
| 43 | + /// <summary> | |
| 44 | + /// Defines resolutions modes for Razor view template files. | |
| 45 | + /// </summary> | |
| 46 | + public enum RazorViewResolutionMode | |
| 47 | + { | |
| 48 | + /// <summary> | |
| 49 | + /// Resolves template files from a path. | |
| 50 | + /// </summary> | |
| 51 | + ResolvePath, | |
| 52 | + /// <summary> | |
| 53 | + /// Resolves template files from a path and watches for changes. | |
| 54 | + /// </summary> | |
| 55 | + /// <remarks> | |
| 56 | + /// This should only be used in debug mode. | |
| 57 | + /// </remarks> | |
| 58 | + WatchingResolvePath, | |
| 59 | + /// <summary> | |
| 60 | + /// Resolves template files from an embedded resource. | |
| 61 | + /// </summary> | |
| 62 | + EmbeddedResource | |
| 63 | + } | |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// Defines a language constraint for a <see cref="RazorView{TLanguage}"/>. | |
| 67 | + /// </summary> | |
| 68 | + public abstract class LanguageContraint | |
| 69 | + { | |
| 70 | + /// <summary> | |
| 71 | + /// Gets target language for the <see cref="LanguageContraint"/> implementation. | |
| 72 | + /// </summary> | |
| 73 | + public abstract Language TargetLanguage | |
| 74 | + { | |
| 75 | + get; | |
| 76 | + } | |
| 77 | + | |
| 78 | + /// <summary> | |
| 79 | + /// Gets resolution mode of <see cref="RazorView{TLanguage}"/>. | |
| 80 | + /// </summary> | |
| 81 | + public virtual RazorViewResolutionMode ResolutionMode => RazorViewResolutionMode.ResolvePath; | |
| 82 | + } | |
| 83 | + | |
| 84 | + /// <summary> | |
| 85 | + /// Defines a C# based language constraint. | |
| 86 | + /// </summary> | |
| 87 | + public class CSharp : LanguageContraint | |
| 88 | + { | |
| 89 | + /// <summary> | |
| 90 | + /// Gets target language for the <see cref="LanguageContraint"/> implementation - C#. | |
| 91 | + /// </summary> | |
| 92 | + public override Language TargetLanguage => Language.CSharp; | |
| 93 | + } | |
| 94 | + | |
| 95 | + /// <summary> | |
| 96 | + /// Defines a C# based language constraint in debug mode. | |
| 97 | + /// </summary> | |
| 98 | + public class CSharpDebug : CSharp | |
| 99 | + { | |
| 100 | + /// <summary> | |
| 101 | + /// Gets resolution mode of <see cref="RazorView{TLanguage}"/> - watching resolve path for debugging operations. | |
| 102 | + /// </summary> | |
| 103 | + public override RazorViewResolutionMode ResolutionMode => RazorViewResolutionMode.WatchingResolvePath; | |
| 104 | + } | |
| 105 | + | |
| 106 | + /// <summary> | |
| 107 | + /// Defines a C# based language constraint for embedded resources. | |
| 108 | + /// </summary> | |
| 109 | + public class CSharpEmbeddedResource : CSharp | |
| 110 | + { | |
| 111 | + /// <summary> | |
| 112 | + /// Gets resolution mode of <see cref="RazorView{TLanguage}"/> - embedded resources. | |
| 113 | + /// </summary> | |
| 114 | + public override RazorViewResolutionMode ResolutionMode => RazorViewResolutionMode.EmbeddedResource; | |
| 115 | + } | |
| 116 | + | |
| 117 | + /// <summary> | |
| 118 | + /// Defines a Visual Basic based language constraint. | |
| 119 | + /// </summary> | |
| 120 | + public class VisualBasic : LanguageContraint | |
| 121 | + { | |
| 122 | + /// <summary> | |
| 123 | + /// Gets target language for the <see cref="LanguageContraint"/> implementation - Visual Basic. | |
| 124 | + /// </summary> | |
| 125 | + public override Language TargetLanguage => Language.VisualBasic; | |
| 126 | + } | |
| 127 | + | |
| 128 | + /// <summary> | |
| 129 | + /// Defines a Visual Basic based language constraint in debug mode. | |
| 130 | + /// </summary> | |
| 131 | + public class VisualBasicDebug : VisualBasic | |
| 132 | + { | |
| 133 | + /// <summary> | |
| 134 | + /// Gets resolution mode of <see cref="RazorView{TLanguage}"/> - watching resolve path for debugging operations. | |
| 135 | + /// </summary> | |
| 136 | + public override RazorViewResolutionMode ResolutionMode => RazorViewResolutionMode.WatchingResolvePath; | |
| 137 | + } | |
| 138 | + | |
| 139 | + /// <summary> | |
| 140 | + /// Defines a Visual Basic based language constraint for embedded resources. | |
| 141 | + /// </summary> | |
| 142 | + public class VisualBasicEmbeddedResource : CSharp | |
| 143 | + { | |
| 144 | + /// <summary> | |
| 145 | + /// Gets resolution mode of <see cref="RazorView{TLanguage}"/> - embedded resources. | |
| 146 | + /// </summary> | |
| 147 | + public override RazorViewResolutionMode ResolutionMode => RazorViewResolutionMode.EmbeddedResource; | |
| 148 | + } | |
| 149 | + | |
| 150 | + /// <summary> | |
| 151 | + /// Defines a view class for data context based Razor template implementations. | |
| 152 | + /// </summary> | |
| 153 | + /// <typeparam name="TLanguage"><see cref="LanguageContraint"/> type to use for Razor views.</typeparam> | |
| 154 | + public class RazorView<TLanguage> where TLanguage : LanguageContraint, new() | |
| 155 | + { | |
| 156 | + #region [ Members ] | |
| 157 | + | |
| 158 | + // Fields | |
| 159 | + private readonly DynamicViewBag m_viewBag = new DynamicViewBag(); | |
| 160 | + private Dictionary<string, string> m_parameters; | |
| 161 | + | |
| 162 | + #endregion | |
| 163 | + | |
| 164 | + #region [ Constructors ] | |
| 165 | + | |
| 166 | + /// <summary> | |
| 167 | + /// Creates a new <see cref="RazorView{TLanguage}"/>. | |
| 168 | + /// </summary> | |
| 169 | + public RazorView() | |
| 170 | + { | |
| 171 | + } | |
| 172 | + | |
| 173 | + /// <summary> | |
| 174 | + /// Creates a new <see cref="RazorView{TLanguage}"/> with the specified exception handler. | |
| 175 | + /// </summary> | |
| 176 | + /// <param name="templateName">Name of template file, typically a .cshtml or .vbhtml file.</param> | |
| 177 | + /// <param name="exceptionHandler">Delegate to handle exceptions, if any.</param> | |
| 178 | + public RazorView(string templateName, Action<Exception> exceptionHandler = null) | |
| 179 | + { | |
| 180 | + TemplateName = templateName; | |
| 181 | + ExceptionHandler = exceptionHandler; | |
| 182 | + } | |
| 183 | + | |
| 184 | + /// <summary> | |
| 185 | + /// Creates a new <see cref="RazorView{TLanguage}"/> with the specified parameters. | |
| 186 | + /// </summary> | |
| 187 | + /// <param name="templateName">Name of template file, typically a .cshtml or .vbhtml file.</param> | |
| 188 | + /// <param name="model">Reference to model to use when rendering template.</param> | |
| 189 | + /// <param name="modelType">Type of <paramref name="model"/>.</param> | |
| 190 | + /// <param name="exceptionHandler">Delegate to handle exceptions, if any.</param> | |
| 191 | + public RazorView(string templateName, object model, Type modelType, Action<Exception> exceptionHandler = null) : this(templateName, exceptionHandler) | |
| 192 | + { | |
| 193 | + Model = model; | |
| 194 | + ModelType = modelType; | |
| 195 | + } | |
| 196 | + | |
| 197 | + #endregion | |
| 198 | + | |
| 199 | + #region [ Properties ] | |
| 200 | + | |
| 201 | + /// <summary> | |
| 202 | + /// Gets or sets name of template file. | |
| 203 | + /// </summary> | |
| 204 | + public string TemplateName | |
| 205 | + { | |
| 206 | + get; set; | |
| 207 | + } | |
| 208 | + | |
| 209 | + /// <summary> | |
| 210 | + /// Gets or sets reference to model to use when rendering template. | |
| 211 | + /// </summary> | |
| 212 | + public object Model | |
| 213 | + { | |
| 214 | + get; set; | |
| 215 | + } | |
| 216 | + | |
| 217 | + /// <summary> | |
| 218 | + /// Gets or sets type of <see cref="Model"/>. | |
| 219 | + /// </summary> | |
| 220 | + public Type ModelType | |
| 221 | + { | |
| 222 | + get; set; | |
| 223 | + } | |
| 224 | + | |
| 225 | + /// <summary> | |
| 226 | + /// Gets or sets delegate used to handle exceptions. | |
| 227 | + /// </summary> | |
| 228 | + public Action<Exception> ExceptionHandler | |
| 229 | + { | |
| 230 | + get; set; | |
| 231 | + } | |
| 232 | + | |
| 233 | + /// <summary> | |
| 234 | + /// Gets reference to view bag used when rendering template. | |
| 235 | + /// </summary> | |
| 236 | + public dynamic ViewBag => m_viewBag; | |
| 237 | + | |
| 238 | + /// <summary> | |
| 239 | + /// Gets query string parameter specified by <paramref name="key"/>. | |
| 240 | + /// </summary> | |
| 241 | + /// <param name="key">Name of query string parameter to retrieve.</param> | |
| 242 | + /// <returns>Query string parameter specified by <paramref name="key"/>.</returns> | |
| 243 | + public string this[string key] => Parameters[key]; | |
| 244 | + | |
| 245 | + /// <summary> | |
| 246 | + /// Gets a dictionary of query string parameters passed to rendered view. | |
| 247 | + /// </summary> | |
| 248 | + public Dictionary<string, string> Parameters | |
| 249 | + { | |
| 250 | + get | |
| 251 | + { | |
| 252 | + if ((object)m_parameters == null) | |
| 253 | + { | |
| 254 | + HttpRequestMessage request = ViewBag.Request; | |
| 255 | + m_parameters = HttpUtility.ParseQueryString(request.RequestUri.Query).ToDictionary(); | |
| 256 | + } | |
| 257 | + | |
| 258 | + return m_parameters; | |
| 259 | + } | |
| 260 | + } | |
| 261 | + | |
| 262 | + #endregion | |
| 263 | + | |
| 264 | + #region [ Methods ] | |
| 265 | + | |
| 266 | + /// <summary> | |
| 267 | + /// Compiles and executes view template. | |
| 268 | + /// </summary> | |
| 269 | + /// <returns>Rendered result.</returns> | |
| 270 | + public string Execute() | |
| 271 | + { | |
| 272 | + using (DataContext<TLanguage> dataContext = new DataContext<TLanguage>(exceptionHandler: ExceptionHandler)) | |
| 273 | + { | |
| 274 | + m_viewBag.AddValue("DataContext", dataContext); | |
| 275 | + return s_engineService.RunCompile(TemplateName, ModelType, Model, m_viewBag); | |
| 276 | + } | |
| 277 | + } | |
| 278 | + | |
| 279 | + /// <summary> | |
| 280 | + /// Compiles and executes view template for specified request message and post data. | |
| 281 | + /// </summary> | |
| 282 | + /// <returns>Rendered result.</returns> | |
| 283 | + public string Execute(HttpRequestMessage requestMessage, dynamic postData) | |
| 284 | + { | |
| 285 | + using (DataContext<TLanguage> dataContext = new DataContext<TLanguage>(exceptionHandler: ExceptionHandler)) | |
| 286 | + { | |
| 287 | + m_viewBag.AddValue("DataContext", dataContext); | |
| 288 | + m_viewBag.AddValue("Request", requestMessage); | |
| 289 | + | |
| 290 | + if ((object)postData == null) | |
| 291 | + { | |
| 292 | + m_viewBag.AddValue("IsPost", false); | |
| 293 | + } | |
| 294 | + else | |
| 295 | + { | |
| 296 | + m_viewBag.AddValue("IsPost", true); | |
| 297 | + m_viewBag.AddValue("PostData", postData); | |
| 298 | + } | |
| 299 | + | |
| 300 | + return s_engineService.RunCompile(TemplateName, ModelType, Model, m_viewBag); | |
| 301 | + } | |
| 302 | + } | |
| 303 | + | |
| 304 | + /// <summary> | |
| 305 | + /// Asynchronously compiles and executes view template for specified request message and post data. | |
| 306 | + /// </summary> | |
| 307 | + /// <returns>Task that will provide rendered result.</returns> | |
| 308 | + public Task ExecuteAsync(HttpRequestMessage requestMessage, dynamic postData) | |
| 309 | + { | |
| 310 | + return Task.Run(() => Execute(requestMessage, postData)); | |
| 311 | + } | |
| 312 | + | |
| 313 | + #endregion | |
| 314 | + | |
| 315 | + #region [ Static ] | |
| 316 | + | |
| 317 | + // Static Fields | |
| 318 | + private static readonly IRazorEngineService s_engineService; | |
| 319 | + | |
| 320 | + /// <summary> | |
| 321 | + /// Defines default settings path for template files. | |
| 322 | + /// </summary> | |
| 323 | + public static readonly string TemplatePath; | |
| 324 | + | |
| 325 | + // Static Constructor | |
| 326 | + static RazorView() | |
| 327 | + { | |
| 328 | + TLanguage languageType = new TLanguage(); | |
| 329 | + | |
| 330 | + // Get configured template path | |
| 331 | + CategorizedSettingsElementCollection systemSettings = ConfigurationFile.Current.Settings["systemSettings"]; | |
| 332 | + | |
| 333 | + switch (languageType.ResolutionMode) | |
| 334 | + { | |
| 335 | + case RazorViewResolutionMode.ResolvePath: | |
| 336 | + case RazorViewResolutionMode.WatchingResolvePath: | |
| 337 | + if (Common.GetApplicationType() == ApplicationType.Web) | |
| 338 | + { | |
| 339 | + systemSettings.Add("TemplatePath", "~/Views/Shared/Templates/", "Path for data context based razor field templates."); | |
| 340 | + TemplatePath = HostingEnvironment.MapPath(systemSettings["TemplatePath"].Value).EnsureEnd('/'); | |
| 341 | + } | |
| 342 | + else | |
| 343 | + { | |
| 344 | + systemSettings.Add("TemplatePath", "wwwroot/Templates/", "Path for data context based razor field templates."); | |
| 345 | + TemplatePath = FilePath.GetAbsolutePath(systemSettings["TemplatePath"].Value); | |
| 346 | + } | |
| 347 | + | |
| 348 | + if (languageType.ResolutionMode == RazorViewResolutionMode.ResolvePath) | |
| 349 | + { | |
| 350 | + s_engineService = RazorEngineService.Create(new TemplateServiceConfiguration | |
| 351 | + { | |
| 352 | + Language = languageType.TargetLanguage, | |
| 353 | + TemplateManager = new ResolvePathTemplateManager(new[] { TemplatePath }), | |
| 354 | + Debug = false | |
| 355 | + }); | |
| 356 | + } | |
| 357 | + else | |
| 358 | + { | |
| 359 | + // The watching resolve path template manager should not be used in production since | |
| 360 | + // assemblies cannot be unloaded from an AppDomain. Every time a change to a .cshtml | |
| 361 | + // file is picked up by the watcher it is compiled and loaded into the AppDomain and | |
| 362 | + // the old one cannot be removed (.NET restriction), the net result is a memory leak | |
| 363 | + InvalidatingCachingProvider cachingProvider = new InvalidatingCachingProvider(); | |
| 364 | + | |
| 365 | + s_engineService = RazorEngineService.Create(new TemplateServiceConfiguration | |
| 366 | + { | |
| 367 | + Language = languageType.TargetLanguage, | |
| 368 | + CachingProvider = cachingProvider, | |
| 369 | + TemplateManager = new WatchingResolvePathTemplateManager(new[] { TemplatePath }, cachingProvider), | |
| 370 | + Debug = true | |
| 371 | + }); | |
| 372 | + } | |
| 373 | + break; | |
| 374 | + case RazorViewResolutionMode.EmbeddedResource: | |
| 375 | + systemSettings.Add("EmbeddedTemplatePath", "GSF.Web.Templating.Views.", "Embedded name space path for data context based razor field templates."); | |
| 376 | + TemplatePath = systemSettings["EmbeddedTemplatePath"].Value.EnsureEnd('.'); | |
| 377 | + | |
| 378 | + s_engineService = RazorEngineService.Create(new TemplateServiceConfiguration | |
| 379 | + { | |
| 380 | + Language = languageType.TargetLanguage, | |
| 381 | + TemplateManager = new DelegateTemplateManager(name => | |
| 382 | + { | |
| 383 | + string resourceName = name; | |
| 384 | + | |
| 385 | + if (!resourceName.Contains(".")) | |
| 386 | + resourceName = TemplatePath + name; | |
| 387 | + | |
| 388 | + Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); | |
| 389 | + | |
| 390 | + if ((object)stream == null) | |
| 391 | + return ""; | |
| 392 | + | |
| 393 | + using (StreamReader reader = new StreamReader(stream)) | |
| 394 | + { | |
| 395 | + return reader.ReadToEnd(); | |
| 396 | + } | |
| 397 | + }), | |
| 398 | + Debug = false | |
| 399 | + }); | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + break; | |
| 404 | + default: | |
| 405 | + throw new ArgumentOutOfRangeException(""); | |
| 406 | + } | |
| 407 | + } | |
| 408 | + | |
| 409 | + /// <summary> | |
| 410 | + /// Kicks off a task to pre-compile Razor templates. | |
| 411 | + /// </summary> | |
| 412 | + /// <param name="exceptionHandler">Exception handler used to report issues, if any.</param> | |
| 413 | + public static Task PreCompile(Action<Exception> exceptionHandler = null) | |
| 414 | + { | |
| 415 | + return Task.Run(() => | |
| 416 | + { | |
| 417 | + TLanguage languageType = new TLanguage(); | |
| 418 | + | |
| 419 | + if (languageType.ResolutionMode == RazorViewResolutionMode.EmbeddedResource) | |
| 420 | + { | |
| 421 | + foreach (string fileName in Assembly.GetExecutingAssembly().GetManifestResourceNames()) | |
| 422 | + { | |
| 423 | + if (fileName.StartsWith(TemplatePath)) | |
| 424 | + { | |
| 425 | + try | |
| 426 | + { | |
| 427 | + s_engineService.Compile(fileName.Substring(TemplatePath.Length)); | |
| 428 | + } | |
| 429 | + catch (Exception ex) | |
| 430 | + { | |
| 431 | + if ((object)exceptionHandler != null) | |
| 432 | + exceptionHandler(new InvalidOperationException($"Failed to pre-compile razor template \"{fileName}\": {ex.Message}", ex)); | |
| 433 | + } | |
| 434 | + } | |
| 435 | + | |
| 436 | + } | |
| 437 | + } | |
| 438 | + else | |
| 439 | + { | |
| 440 | + string webRootFolder = FilePath.AddPathSuffix(TemplatePath); | |
| 441 | + string[] razorFiles = FilePath.GetFileList($"{webRootFolder}*.{(languageType.TargetLanguage == Language.CSharp ? "cs" : "vb")}html"); | |
| 442 | + | |
| 443 | + foreach (string fileName in razorFiles) | |
| 444 | + { | |
| 445 | + try | |
| 446 | + { | |
| 447 | + s_engineService.Compile(FilePath.GetFileName(fileName)); | |
| 448 | + } | |
| 449 | + catch (Exception ex) | |
| 450 | + { | |
| 451 | + if ((object)exceptionHandler != null) | |
| 452 | + exceptionHandler(new InvalidOperationException($"Failed to pre-compile razor template \"{fileName}\": {ex.Message}", ex)); | |
| 453 | + } | |
| 454 | + } | |
| 455 | + } | |
| 456 | + }); | |
| 457 | + } | |
| 458 | + | |
| 459 | + #endregion | |
| 460 | + } | |
| 461 | +} | |
| @@ -0,0 +1,70 @@ | ||
| 1 | +@******************************************************************************************************* | |
| 2 | +// AddCheckBoxField.cshtml - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/01/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//*****************************************************************************************************@ | |
| 23 | +@using RazorEngine.Templating | |
| 24 | +@inherits TemplateBase | |
| 25 | +@{ | |
| 26 | + string fieldName = ViewBag.FieldName; | |
| 27 | + string fieldLabel = ViewBag.FieldLabel; | |
| 28 | + string fieldID = ViewBag.FieldID; | |
| 29 | + string groupDataBinding = ViewBag.GroupDataBinding; | |
| 30 | + string labelDataBinding = ViewBag.LabelDataBinding; | |
| 31 | + string customDataBinding = ViewBag.CustomDataBinding; | |
| 32 | + string dependencyFieldName = ViewBag.DependencyFieldName; | |
| 33 | + string toolTip = ViewBag.ToolTip; | |
| 34 | + | |
| 35 | + if (!string.IsNullOrEmpty(dependencyFieldName)) | |
| 36 | + { | |
| 37 | + if (string.IsNullOrEmpty(customDataBinding)) | |
| 38 | + { | |
| 39 | + customDataBinding = string.Format("enable: {0}", dependencyFieldName); | |
| 40 | + } | |
| 41 | + | |
| 42 | + if (string.IsNullOrEmpty(labelDataBinding)) | |
| 43 | + { | |
| 44 | + labelDataBinding = string.Format("css: {{disabled: !{0}()}}", dependencyFieldName); | |
| 45 | + } | |
| 46 | + } | |
| 47 | + | |
| 48 | + if (!string.IsNullOrEmpty(groupDataBinding)) | |
| 49 | + { | |
| 50 | + groupDataBinding = string.Format(" data-bind=\"{0}\"", groupDataBinding); | |
| 51 | + } | |
| 52 | + | |
| 53 | + if (!string.IsNullOrEmpty(labelDataBinding)) | |
| 54 | + { | |
| 55 | + labelDataBinding = string.Format(" data-bind=\"{0}\"", labelDataBinding); | |
| 56 | + } | |
| 57 | + | |
| 58 | + if (!string.IsNullOrEmpty(customDataBinding)) | |
| 59 | + { | |
| 60 | + customDataBinding = string.Format(", {0}", customDataBinding); | |
| 61 | + } | |
| 62 | + | |
| 63 | + if (!string.IsNullOrEmpty(toolTip)) | |
| 64 | + { | |
| 65 | + toolTip = string.Format(" data-toggle=\"tooltip\" data-placement=\"bottom\" title=\"{0}\"", toolTip); | |
| 66 | + } | |
| 67 | +} | |
| 68 | +<div class="checkbox"@Raw(groupDataBinding)@Raw(toolTip)> | |
| 69 | + <label for="@Raw(fieldID)"@Raw(labelDataBinding)><input type="checkbox" id="@Raw(fieldID)" data-bind="checked: @Raw(fieldName), attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)"> @Raw(fieldLabel)</label> | |
| 70 | +</div> | |
| @@ -0,0 +1,109 @@ | ||
| 1 | +@******************************************************************************************************* | |
| 2 | +// AddDateField.cshtml - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/01/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//*****************************************************************************************************@ | |
| 23 | +@using RazorEngine.Templating | |
| 24 | +@inherits TemplateBase | |
| 25 | +@{ | |
| 26 | + string fieldName = ViewBag.FieldName; | |
| 27 | + bool required = ViewBag.Required; | |
| 28 | + int maxLength = ViewBag.MaxLength; | |
| 29 | + string inputType = ViewBag.InputType; | |
| 30 | + string fieldLabel = ViewBag.FieldLabel; | |
| 31 | + string fieldID = ViewBag.FieldID; | |
| 32 | + string groupDataBinding = ViewBag.GroupDataBinding; | |
| 33 | + string labelDataBinding = ViewBag.LabelDataBinding; | |
| 34 | + string requiredDataBinding = ViewBag.RequiredDataBinding; | |
| 35 | + string customDataBinding = ViewBag.CustomDataBinding; | |
| 36 | + string dependencyFieldName = ViewBag.DependencyFieldName; | |
| 37 | + string toolTip = ViewBag.ToolTip; | |
| 38 | + string inputSizeRestriction = maxLength > 0 ? string.Format(" maxlength=\"{0}\" size=\"{0}\"", maxLength) : ""; | |
| 39 | + | |
| 40 | + if (!string.IsNullOrEmpty(dependencyFieldName)) | |
| 41 | + { | |
| 42 | + if (string.IsNullOrEmpty(customDataBinding)) | |
| 43 | + { | |
| 44 | + customDataBinding = string.Format("enable: {0}", dependencyFieldName); | |
| 45 | + } | |
| 46 | + | |
| 47 | + if (string.IsNullOrEmpty(labelDataBinding)) | |
| 48 | + { | |
| 49 | + labelDataBinding = string.Format("css: {{disabled: !{0}()}}", dependencyFieldName); | |
| 50 | + } | |
| 51 | + | |
| 52 | + if (required && string.IsNullOrEmpty(requiredDataBinding)) | |
| 53 | + { | |
| 54 | + requiredDataBinding = string.Format("{0}()", dependencyFieldName); | |
| 55 | + } | |
| 56 | + } | |
| 57 | + | |
| 58 | + if (!string.IsNullOrEmpty(groupDataBinding)) | |
| 59 | + { | |
| 60 | + groupDataBinding = string.Format(" data-bind=\"{0}\"", groupDataBinding); | |
| 61 | + } | |
| 62 | + | |
| 63 | + if (!string.IsNullOrEmpty(labelDataBinding)) | |
| 64 | + { | |
| 65 | + labelDataBinding = string.Format(" data-bind=\"{0}\"", labelDataBinding); | |
| 66 | + } | |
| 67 | + | |
| 68 | + if (!string.IsNullOrEmpty(requiredDataBinding)) | |
| 69 | + { | |
| 70 | + requiredDataBinding = string.Format(" && {0}", requiredDataBinding); | |
| 71 | + } | |
| 72 | + | |
| 73 | + if (!string.IsNullOrEmpty(customDataBinding)) | |
| 74 | + { | |
| 75 | + customDataBinding = string.Format(", {0}", customDataBinding); | |
| 76 | + } | |
| 77 | + | |
| 78 | + if (!string.IsNullOrEmpty(toolTip)) | |
| 79 | + { | |
| 80 | + toolTip = string.Format(" data-toggle=\"tooltip\" data-trigger=\"focus\" data-placement=\"auto bottom\" title=\"{0}\"", toolTip); | |
| 81 | + } | |
| 82 | +} | |
| 83 | +@if (required) | |
| 84 | +{ | |
| 85 | + @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : string.Format("<div{0}>\r\n", groupDataBinding)) | |
| 86 | + <div class="form-group" data-bind="css: {'has-error': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding)), 'has-feedback': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))}"@Raw(toolTip)> | |
| 87 | + @if (fieldLabel.Length > 0) { | |
| 88 | + <label for="@Raw(fieldID)" @Raw(labelDataBinding)>@Raw(fieldLabel):</label> | |
| 89 | + } | |
| 90 | + <div class="input-group date" data-provide="datepicker"> | |
| 91 | + <input type="@Raw(inputType)" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)@(string.IsNullOrEmpty(requiredDataBinding) ? " required" : "")> | |
| 92 | + <span class="input-group-addon" data-bind="style: {'cursor': ($root.recordMode()===RecordMode.View ? 'not-allowed' : 'pointer')}"><i class="glyphicon glyphicon-calendar"></i></span> | |
| 93 | + <span class="glyphicon glyphicon-remove form-control-feedback" style="right: 36px" data-bind="visible: isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))"></span> | |
| 94 | + </div> | |
| 95 | + </div> | |
| 96 | + @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : "</div>\r\n") | |
| 97 | +} | |
| 98 | +else | |
| 99 | +{ | |
| 100 | + <div class="form-group"@Raw(groupDataBinding)@Raw(toolTip)> | |
| 101 | + @if (fieldLabel.Length > 0) { | |
| 102 | + <label for="@Raw(fieldID)" @Raw(labelDataBinding)>@Raw(fieldLabel):</label> | |
| 103 | + } | |
| 104 | + <div class="input-group date" data-provide="datepicker"> | |
| 105 | + <input type="@Raw(inputType)" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)> | |
| 106 | + <span class="input-group-addon" data-bind="style: {'cursor': ($root.recordMode()===RecordMode.View ? 'not-allowed' : 'pointer')}"><i class="glyphicon glyphicon-calendar"></i></span> | |
| 107 | + </div> | |
| 108 | + </div> | |
| 109 | +} | |
| @@ -0,0 +1,103 @@ | ||
| 1 | +@******************************************************************************************************* | |
| 2 | +// AddInputField.cshtml - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/01/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//*****************************************************************************************************@ | |
| 23 | +@using RazorEngine.Templating | |
| 24 | +@inherits TemplateBase | |
| 25 | +@{ | |
| 26 | + string fieldName = ViewBag.FieldName; | |
| 27 | + bool required = ViewBag.Required; | |
| 28 | + int maxLength = ViewBag.MaxLength; | |
| 29 | + string inputType = ViewBag.InputType; | |
| 30 | + string fieldLabel = ViewBag.FieldLabel; | |
| 31 | + string fieldID = ViewBag.FieldID; | |
| 32 | + string groupDataBinding = ViewBag.GroupDataBinding; | |
| 33 | + string labelDataBinding = ViewBag.LabelDataBinding; | |
| 34 | + string requiredDataBinding = ViewBag.RequiredDataBinding; | |
| 35 | + string customDataBinding = ViewBag.CustomDataBinding; | |
| 36 | + string dependencyFieldName = ViewBag.DependencyFieldName; | |
| 37 | + string toolTip = ViewBag.ToolTip; | |
| 38 | + string inputSizeRestriction = maxLength > 0 ? string.Format(" maxlength=\"{0}\" size=\"{0}\"", maxLength) : ""; | |
| 39 | + | |
| 40 | + if (!string.IsNullOrEmpty(dependencyFieldName)) | |
| 41 | + { | |
| 42 | + if (string.IsNullOrEmpty(customDataBinding)) | |
| 43 | + { | |
| 44 | + customDataBinding = string.Format("enable: {0}", dependencyFieldName); | |
| 45 | + } | |
| 46 | + | |
| 47 | + if (string.IsNullOrEmpty(labelDataBinding)) | |
| 48 | + { | |
| 49 | + labelDataBinding = string.Format("css: {{disabled: !{0}()}}", dependencyFieldName); | |
| 50 | + } | |
| 51 | + | |
| 52 | + if (required && string.IsNullOrEmpty(requiredDataBinding)) | |
| 53 | + { | |
| 54 | + requiredDataBinding = string.Format("{0}()", dependencyFieldName); | |
| 55 | + } | |
| 56 | + } | |
| 57 | + | |
| 58 | + if (!string.IsNullOrEmpty(groupDataBinding)) | |
| 59 | + { | |
| 60 | + groupDataBinding = string.Format(" data-bind=\"{0}\"", groupDataBinding); | |
| 61 | + } | |
| 62 | + | |
| 63 | + if (!string.IsNullOrEmpty(labelDataBinding)) | |
| 64 | + { | |
| 65 | + labelDataBinding = string.Format(" data-bind=\"{0}\"", labelDataBinding); | |
| 66 | + } | |
| 67 | + | |
| 68 | + if (!string.IsNullOrEmpty(requiredDataBinding)) | |
| 69 | + { | |
| 70 | + requiredDataBinding = string.Format(" && {0}", requiredDataBinding); | |
| 71 | + } | |
| 72 | + | |
| 73 | + if (!string.IsNullOrEmpty(customDataBinding)) | |
| 74 | + { | |
| 75 | + customDataBinding = string.Format(", {0}", customDataBinding); | |
| 76 | + } | |
| 77 | + | |
| 78 | + if (!string.IsNullOrEmpty(toolTip)) | |
| 79 | + { | |
| 80 | + toolTip = string.Format(" data-toggle=\"tooltip\" data-trigger=\"focus\" data-placement=\"auto bottom\" title=\"{0}\"", toolTip); | |
| 81 | + } | |
| 82 | +} | |
| 83 | +@if (required) | |
| 84 | +{ | |
| 85 | + @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : string.Format("<div{0}>\r\n", groupDataBinding)) | |
| 86 | + <div class="form-group" data-bind="css: {'has-error': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding)), 'has-feedback': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))}"@Raw(toolTip)> | |
| 87 | + @if (fieldLabel.Length > 0) { | |
| 88 | + <label for="@Raw(fieldID)"@Raw(labelDataBinding)>@Raw(fieldLabel):</label> | |
| 89 | + } | |
| 90 | + <input type="@Raw(inputType)" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)@(string.IsNullOrEmpty(requiredDataBinding) ? " required" : "")> | |
| 91 | + <span class="glyphicon glyphicon-remove form-control-feedback" data-bind="visible: isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))"></span> | |
| 92 | + </div> | |
| 93 | + @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : "</div>\r\n") | |
| 94 | +} | |
| 95 | +else | |
| 96 | +{ | |
| 97 | + <div class="form-group"@Raw(groupDataBinding)@Raw(toolTip)> | |
| 98 | + @if (fieldLabel.Length > 0) { | |
| 99 | + <label for="@Raw(fieldID)"@Raw(labelDataBinding)>@Raw(fieldLabel):</label> | |
| 100 | + } | |
| 101 | + <input type="@Raw(inputType)" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)> | |
| 102 | + </div> | |
| 103 | +} | |
| @@ -0,0 +1,86 @@ | ||
| 1 | +@******************************************************************************************************* | |
| 2 | +// AddSelectField.cshtml - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/01/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//*****************************************************************************************************@ | |
| 23 | +@using System.Collections.Generic | |
| 24 | +@using RazorEngine.Templating | |
| 25 | +@inherits TemplateBase | |
| 26 | +@{ | |
| 27 | + string fieldName = ViewBag.FieldName; | |
| 28 | + bool required = ViewBag.Required; | |
| 29 | + string fieldLabel = ViewBag.FieldLabel; | |
| 30 | + string fieldID = ViewBag.FieldID; | |
| 31 | + string groupDataBinding = ViewBag.GroupDataBinding; | |
| 32 | + string labelDataBinding = ViewBag.LabelDataBinding; | |
| 33 | + string customDataBinding = ViewBag.CustomDataBinding; | |
| 34 | + string dependencyFieldName = ViewBag.DependencyFieldName; | |
| 35 | + string toolTip = ViewBag.ToolTip; | |
| 36 | + string optionDataBinding = ViewBag.OptionDataBinding; | |
| 37 | + Dictionary<string, string> options = ViewBag.Options; | |
| 38 | + | |
| 39 | + if (!string.IsNullOrEmpty(dependencyFieldName)) | |
| 40 | + { | |
| 41 | + if (string.IsNullOrEmpty(customDataBinding)) | |
| 42 | + { | |
| 43 | + customDataBinding = string.Format("enable: {0}", dependencyFieldName); | |
| 44 | + } | |
| 45 | + | |
| 46 | + if (string.IsNullOrEmpty(labelDataBinding)) | |
| 47 | + { | |
| 48 | + labelDataBinding = string.Format("css: {{disabled: !{0}()}}", dependencyFieldName); | |
| 49 | + } | |
| 50 | + } | |
| 51 | + | |
| 52 | + if (!string.IsNullOrEmpty(groupDataBinding)) | |
| 53 | + { | |
| 54 | + groupDataBinding = string.Format(" data-bind=\"{0}\"", groupDataBinding); | |
| 55 | + } | |
| 56 | + | |
| 57 | + if (!string.IsNullOrEmpty(labelDataBinding)) | |
| 58 | + { | |
| 59 | + labelDataBinding = string.Format(" data-bind=\"{0}\"", labelDataBinding); | |
| 60 | + } | |
| 61 | + | |
| 62 | + if (!string.IsNullOrEmpty(customDataBinding)) | |
| 63 | + { | |
| 64 | + customDataBinding = string.Format(", {0}", customDataBinding); | |
| 65 | + } | |
| 66 | + | |
| 67 | + if (!string.IsNullOrEmpty(toolTip)) | |
| 68 | + { | |
| 69 | + toolTip = string.Format(" data-toggle=\"tooltip\" data-placement=\"bottom\" title=\"{0}\"", toolTip); | |
| 70 | + } | |
| 71 | + | |
| 72 | + if (!string.IsNullOrEmpty(optionDataBinding)) | |
| 73 | + { | |
| 74 | + optionDataBinding = string.Format(" data-bind=\"{0}\"", optionDataBinding); | |
| 75 | + } | |
| 76 | +} | |
| 77 | +<div class="form-group"@Raw(groupDataBinding)@Raw(toolTip)> | |
| 78 | + @if (fieldLabel.Length > 0) { | |
| 79 | + <label for="@Raw(fieldID)" @Raw(labelDataBinding)>@Raw(fieldLabel):</label> | |
| 80 | + } | |
| 81 | + <select class="form-control" id="@Raw(fieldID)" data-bind="value: @Raw(fieldName), valueAllowUnset: @((!required).ToString().ToLower()), attr: {'disabled': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)"> | |
| 82 | + @foreach (KeyValuePair<string, string> item in options) { | |
| 83 | + <option value="@item.Key"@Raw(optionDataBinding)>@item.Value</option> | |
| 84 | + } | |
| 85 | + </select> | |
| 86 | +</div> | |
| @@ -0,0 +1,103 @@ | ||
| 1 | +@******************************************************************************************************* | |
| 2 | +// AddTextAreaField.cshtml - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | |
| 9 | +// not use this file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/01/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//*****************************************************************************************************@ | |
| 23 | +@using RazorEngine.Templating | |
| 24 | +@inherits TemplateBase | |
| 25 | +@{ | |
| 26 | + string fieldName = ViewBag.FieldName; | |
| 27 | + bool required = ViewBag.Required; | |
| 28 | + int maxLength = ViewBag.MaxLength; | |
| 29 | + int rows = ViewBag.Rows; | |
| 30 | + string fieldLabel = ViewBag.FieldLabel; | |
| 31 | + string fieldID = ViewBag.FieldID; | |
| 32 | + string groupDataBinding = ViewBag.GroupDataBinding; | |
| 33 | + string labelDataBinding = ViewBag.LabelDataBinding; | |
| 34 | + string requiredDataBinding = ViewBag.RequiredDataBinding; | |
| 35 | + string customDataBinding = ViewBag.CustomDataBinding; | |
| 36 | + string dependencyFieldName = ViewBag.DependencyFieldName; | |
| 37 | + string toolTip = ViewBag.ToolTip; | |
| 38 | + string inputSizeRestriction = maxLength > 0 ? string.Format(" maxlength=\"{0}\" size=\"{0}\"", maxLength) : ""; | |
| 39 | + | |
| 40 | + if (!string.IsNullOrEmpty(dependencyFieldName)) | |
| 41 | + { | |
| 42 | + if (string.IsNullOrEmpty(customDataBinding)) | |
| 43 | + { | |
| 44 | + customDataBinding = string.Format("enable: {0}", dependencyFieldName); | |
| 45 | + } | |
| 46 | + | |
| 47 | + if (string.IsNullOrEmpty(labelDataBinding)) | |
| 48 | + { | |
| 49 | + labelDataBinding = string.Format("css: {{disabled: !{0}()}}", dependencyFieldName); | |
| 50 | + } | |
| 51 | + | |
| 52 | + if (required && string.IsNullOrEmpty(requiredDataBinding)) | |
| 53 | + { | |
| 54 | + requiredDataBinding = string.Format("{0}()", dependencyFieldName); | |
| 55 | + } | |
| 56 | + } | |
| 57 | + | |
| 58 | + if (!string.IsNullOrEmpty(groupDataBinding)) | |
| 59 | + { | |
| 60 | + groupDataBinding = string.Format(" data-bind=\"{0}\"", groupDataBinding); | |
| 61 | + } | |
| 62 | + | |
| 63 | + if (!string.IsNullOrEmpty(labelDataBinding)) | |
| 64 | + { | |
| 65 | + labelDataBinding = string.Format(" data-bind=\"{0}\"", labelDataBinding); | |
| 66 | + } | |
| 67 | + | |
| 68 | + if (!string.IsNullOrEmpty(requiredDataBinding)) | |
| 69 | + { | |
| 70 | + requiredDataBinding = string.Format(" && {0}", requiredDataBinding); | |
| 71 | + } | |
| 72 | + | |
| 73 | + if (!string.IsNullOrEmpty(customDataBinding)) | |
| 74 | + { | |
| 75 | + customDataBinding = string.Format(", {0}", customDataBinding); | |
| 76 | + } | |
| 77 | + | |
| 78 | + if (!string.IsNullOrEmpty(toolTip)) | |
| 79 | + { | |
| 80 | + toolTip = string.Format(" data-toggle=\"tooltip\" data-trigger=\"focus\" data-placement=\"auto bottom\" title=\"{0}\"", toolTip); | |
| 81 | + } | |
| 82 | +} | |
| 83 | +@if (required) | |
| 84 | +{ | |
| 85 | + @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : string.Format("<div{0}>\r\n", groupDataBinding)) | |
| 86 | + <div class="form-group" data-bind="css: {'has-error': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding)), 'has-feedback': isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))}"@Raw(toolTip)> | |
| 87 | + @if (fieldLabel.Length > 0) { | |
| 88 | + <label for="@Raw(fieldID)" @Raw(labelDataBinding)>@Raw(fieldLabel):</label> | |
| 89 | + } | |
| 90 | + <textarea rows="@rows" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)@(string.IsNullOrEmpty(requiredDataBinding) ? " required" : "")></textarea> | |
| 91 | + <span class="glyphicon glyphicon-remove form-control-feedback" data-bind="visible: isEmpty(@(Raw(fieldName))())@(Raw(requiredDataBinding))"></span> | |
| 92 | + </div> | |
| 93 | + @Raw(string.IsNullOrEmpty(groupDataBinding) ? "" : "</div>\r\n") | |
| 94 | +} | |
| 95 | +else | |
| 96 | +{ | |
| 97 | + <div class="form-group"@Raw(groupDataBinding)@Raw(toolTip)> | |
| 98 | + @if (fieldLabel.Length > 0) { | |
| 99 | + <label for="@Raw(fieldID)" @Raw(labelDataBinding)>@Raw(fieldLabel):</label> | |
| 100 | + } | |
| 101 | + <textarea rows="@rows" class="form-control" data-bind="textInput: @Raw(fieldName), attr: {'readonly': ($root.recordMode()===RecordMode.View ? true : undefined)}@Raw(customDataBinding)" id="@Raw(fieldID)"@Raw(inputSizeRestriction)></textarea> | |
| 102 | + </div> | |
| 103 | +} | |
| @@ -0,0 +1,102 @@ | ||
| 1 | +//****************************************************************************************************** | |
| 2 | +// WebExtensions.cs - Gbtc | |
| 3 | +// | |
| 4 | +// Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | |
| 5 | +// | |
| 6 | +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | |
| 7 | +// the NOTICE file distributed with this work for additional information regarding copyright ownership. | |
| 8 | +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this | |
| 9 | +// file except in compliance with the License. You may obtain a copy of the License at: | |
| 10 | +// | |
| 11 | +// http://opensource.org/licenses/MIT | |
| 12 | +// | |
| 13 | +// Unless agreed to in writing, the subject software distributed under the License is distributed on an | |
| 14 | +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | |
| 15 | +// License for the specific language governing permissions and limitations. | |
| 16 | +// | |
| 17 | +// Code Modification History: | |
| 18 | +// ---------------------------------------------------------------------------------------------------- | |
| 19 | +// 02/17/2016 - J. Ritchie Carroll | |
| 20 | +// Generated original version of source code. | |
| 21 | +// | |
| 22 | +//****************************************************************************************************** | |
| 23 | + | |
| 24 | +using System; | |
| 25 | +using System.Collections.Generic; | |
| 26 | +using System.Linq; | |
| 27 | +using System.Net.Http; | |
| 28 | +using System.Web; | |
| 29 | +using GSF.Collections; | |
| 30 | + | |
| 31 | +namespace GSF.Web | |
| 32 | +{ | |
| 33 | + /// <summary> | |
| 34 | + /// Defines extension methods useful for web applications. | |
| 35 | + /// </summary> | |
| 36 | + public static class WebExtensions | |
| 37 | + { | |
| 38 | + /// <summary> | |
| 39 | + /// Performs JavaScript encoding on given string. | |
| 40 | + /// </summary> | |
| 41 | + /// <param name="text">The string to be encoded.</param> | |
| 42 | + public static string JavaScriptEncode(this string text) | |
| 43 | + { | |
| 44 | + return HttpUtility.JavaScriptStringEncode(text.ToNonNullString()); | |
| 45 | + } | |
| 46 | + | |
| 47 | + /// <summary> | |
| 48 | + /// Performs HTML encoding on the given string. | |
| 49 | + /// </summary> | |
| 50 | + /// <param name="text">The string to be encoded.</param> | |
| 51 | + public static string HtmlEncode(this string text) | |
| 52 | + { | |
| 53 | + return HttpUtility.HtmlEncode(text.ToNonNullString()); | |
| 54 | + } | |
| 55 | + | |
| 56 | + /// <summary> | |
| 57 | + /// Performs URL encoding on the given string. | |
| 58 | + /// </summary> | |
| 59 | + /// <param name="text">The string to be encoded.</param> | |
| 60 | + public static string UrlEncode(this string text) | |
| 61 | + { | |
| 62 | + return HttpUtility.UrlEncode(text.ToNonNullString()); | |
| 63 | + } | |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// Gets query parameters for current request message | |
| 67 | + /// </summary> | |
| 68 | + /// <param name="request"></param> | |
| 69 | + /// <returns></returns> | |
| 70 | + public static Dictionary<string, string> QueryParameters(this HttpRequestMessage request) | |
| 71 | + { | |
| 72 | + return request.GetQueryNameValuePairs().ToDictionary(kvp => kvp.Key, kvp => kvp.Value); | |
| 73 | + } | |
| 74 | + | |
| 75 | + /// <summary> | |
| 76 | + /// Corrects script alignment with a desired number of forward spaces. | |
| 77 | + /// </summary> | |
| 78 | + /// <param name="script">Script text.</param> | |
| 79 | + /// <param name="spaces">Desired forward spaces.</param> | |
| 80 | + /// <returns>Script with corrected forward alignment.</returns> | |
| 81 | + public static string FixForwardSpacing(this string script, int spaces = 4) | |
| 82 | + { | |
| 83 | + Tuple<string, int>[] linesAndLengths = script | |
| 84 | + .Split(new[] { Environment.NewLine }, StringSplitOptions.None) | |
| 85 | + .Select(line => new Tuple<string, int>(line, line.Length - line.TrimStart(' ').Length)) | |
| 86 | + .ToArray(); | |
| 87 | + | |
| 88 | + int minLength = linesAndLengths | |
| 89 | + .Select(lineAndLength => (int?)lineAndLength.Item2) | |
| 90 | + .Where(length => length.GetValueOrDefault() > 0) | |
| 91 | + .Min() ?? 0; | |
| 92 | + | |
| 93 | + string forwardSpacing = new string(' ', spaces); | |
| 94 | + | |
| 95 | + return linesAndLengths | |
| 96 | + .Select(lineAndLength => lineAndLength.Item2 > 0 ? | |
| 97 | + $"{forwardSpacing}{(lineAndLength.Item2 > minLength ? lineAndLength.Item1.Substring(minLength) : lineAndLength.Item1)}" : | |
| 98 | + lineAndLength.Item1.ToNonNullNorEmptyString()) | |
| 99 | + .ToDelimitedString(Environment.NewLine); | |
| 100 | + } | |
| 101 | + } | |
| 102 | +} | |
| @@ -0,0 +1,20 @@ | ||
| 1 | +<?xml version="1.0" encoding="utf-8"?> | |
| 2 | +<packages> | |
| 3 | + <package id="Microsoft.AspNet.Mvc" version="5.2.3" targetFramework="net46" /> | |
| 4 | + <package id="Microsoft.AspNet.Razor" version="3.2.3" targetFramework="net46" /> | |
| 5 | + <package id="Microsoft.AspNet.SignalR" version="2.2.0" targetFramework="net46" /> | |
| 6 | + <package id="Microsoft.AspNet.SignalR.Core" version="2.2.0" targetFramework="net46" /> | |
| 7 | + <package id="Microsoft.AspNet.SignalR.JS" version="2.2.0" targetFramework="net46" /> | |
| 8 | + <package id="Microsoft.AspNet.SignalR.SystemWeb" version="2.2.0" targetFramework="net46" /> | |
| 9 | + <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net46" /> | |
| 10 | + <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net46" /> | |
| 11 | + <package id="Microsoft.AspNet.WebPages" version="3.2.3" targetFramework="net46" /> | |
| 12 | + <package id="Microsoft.Owin" version="2.1.0" targetFramework="net46" /> | |
| 13 | + <package id="Microsoft.Owin.Host.SystemWeb" version="2.1.0" targetFramework="net46" /> | |
| 14 | + <package id="Microsoft.Owin.Security" version="2.1.0" targetFramework="net46" /> | |
| 15 | + <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net46" /> | |
| 16 | + <package id="Newtonsoft.Json" version="6.0.4" targetFramework="net46" /> | |
| 17 | + <package id="Owin" version="1.0" targetFramework="net46" /> | |
| 18 | + <package id="RazorEngine" version="3.7.7" targetFramework="net46" /> | |
| 19 | + <package id="System.Net.Http" version="2.0.20126.16343" targetFramework="net46" /> | |
| 20 | +</packages> | |