Skip to content

Instantly share code, notes, and snippets.

@tlanzer-aktion
Last active August 24, 2023 20:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tlanzer-aktion/99ff743dff939b4c815aefdbbe48847f to your computer and use it in GitHub Desktop.
Save tlanzer-aktion/99ff743dff939b4c815aefdbbe48847f to your computer and use it in GitHub Desktop.
README ProjectList Graph Extension
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using PX.Data;
using PX.Data.BQL.Fluent;
using PX.Data.BQL;
using PX.SM;
using Aktion.Common.Acumatica.ReadMe.DAC;
using System.Web.Caching;
namespace Aktion.Common.Acumatica.ReadMe.Graphs
{
/// <summary>
/// ProjectList graph extension for Customization Projects screen
/// </summary>
public class ProjectListExt : PXGraphExtension<ProjectList>
{
#region Constants
protected const string README_TEMPLATE =
"Name: {0}"
+ "\r\nDescription: {1}"
+ "\r\nAuthor: "
+ "\r\n"
+ "\r\nINSTALLATION"
+ "\r\n1) Import package.zip file"
+ "\r\n2) Publish package"
+ "\r\n"
+ "\r\nFEATURES"
+ "\r\n"
+ "\r\nREFERENCES"
+ "\r\n"
;
#endregion
#region Views
/// <summary>
/// Current project view for use in read.me dialog
/// </summary>
public SelectFrom<CustProject>
.Where<CustProject.projID.IsNotNull
.And<CustProject.projID.IsEqual<CustProject.projID.FromCurrent>>>.View ReadMeProject;
/// <summary>
/// Custom objects view for current project
/// </summary>
public SelectFrom<CustObject>
.Where<CustObject.projectID.IsEqual<CustProject.projID.FromCurrent>>.View ProjectObjects;
/// <summary>
/// File reference header view for searching for read.me file name
/// </summary>
public SelectFrom<UploadFile>
.Where<UploadFile.name.Contains<@P.AsString>>.View ProjectFiles;
/// <summary>
/// File reference revision view for searching for read.me file
/// </summary>
public SelectFrom<UploadFileRevision>
.Where<UploadFileRevision.fileID.IsEqual<@P.AsGuid>>
.OrderBy<UploadFileRevision.fileRevisionID.Desc>.View ProjectFileRevisions;
#endregion
#region Actions
/// <summary>
/// read.me toolbar action button
/// </summary>
public PXAction<CustProject> openReadMe;
/// <summary>
/// Action method for read.me button. Specify its display settings. The actual dialog popup is specified in the UI for the screen.
/// </summary>
/// <param name="adapter"></param>
/// <returns></returns>
[PXButton(DisplayOnMainToolbar = true)]
[PXUIField(DisplayName = "read.me")]
public IEnumerable OpenReadMe(PXAdapter adapter)
{
CustProject project = Base.Projects.Current;
if (project == null) return adapter.Get();
CustProjectExt projectExt = project.GetExtension<CustProjectExt>();
if (string.IsNullOrEmpty(projectExt.UsrReadMe))
{
// If read.me file exists, open and set field content
if (ReadMeFileExists(project))
{
projectExt.UsrReadMe = GetReadMeContentText(project);
}
// If file doesn't exist, default with a read.me template
else
{
projectExt.UsrReadMe = string.Format(README_TEMPLATE, project.Name, project.Description);
}
}
return adapter.Get();
}
/// <summary>
/// read.me dialog Save button
/// </summary>
public PXAction<CustProject> ReadMeSave;
/// <summary>
/// Action method for Save button. Save the read.me contents to file.
/// </summary>
[PXButton(DisplayOnMainToolbar = false)]
[PXUIField(DisplayName = "Save")]
protected void readMeSave()
{
Base.Projects.UpdateCurrent();
// Write/update read.me file to App_Data folder
SaveReadMeToFile();
// Add/update row to CustObject table
SaveReadMeToDB();
Base.Persist();
}
/// <summary>
/// read.me dialog Cancel button
/// </summary>
public PXAction<CustProject> ReadMeCancel;
[PXButton(DisplayOnMainToolbar = false)]
[PXUIField(DisplayName = "Cancel")]
protected void readMeCancel()
{
Base.Projects.Cache.Clear();
Base.Projects.ClearDialog();
}
#endregion
#region Event handlers
/// <summary>
/// CustProject RowSelected event handler
/// </summary>
protected virtual void CustProject_RowSelected(PXCache cache, PXRowSelectedEventArgs e, PXRowSelected baseMethod)
{
baseMethod?.Invoke(cache, e);
var project = (CustProject)e.Row;
if (project == null) return;
CustProjectExt projectExt = project.GetExtension<CustProjectExt>();
if (string.IsNullOrEmpty(projectExt.UsrReadMe))
{
// If read.me file exists, open and set field content, otherwise default with a template
if (ReadMeFileExists(project))
{
projectExt.UsrReadMe = GetReadMeContentText(project);
}
else
{
projectExt.UsrReadMe = string.Format(README_TEMPLATE, project.Name, project.Description);
}
}
}
#endregion
#region Local methods
/// <summary>
/// Determine if a read.me file exists for the project
/// </summary>
/// <param name="project">project data row</param>
/// <returns>whether read.me file exists</returns>
protected bool ReadMeFileExists(CustProject project)
{
// Look for the project read.me file in the project package file references
string filePath = GetProjectReadMePath(project);
return File.Exists(filePath);
}
/// <summary>
/// Build the path of a project's read.me file
/// </summary>
/// <param name="project">project data row</param>
/// <returns>read.me file path</returns>
protected string GetProjectReadMePath(CustProject project)
{
return string.Format(@"{0}\{1}_read.me", HttpContext.Current.Server.MapPath("~/App_Data"), project.Name);
}
/// <summary>
/// Build the read.me path for its project object
/// </summary>
/// <param name="project">project data row</param>
/// <returns>read.me project object path</returns>
protected string GetObjectReadMePath(CustProject project)
{
return string.Format(@"File#App_Data\{0}_read.me", project.Name);
}
/// <summary>
/// Build the read.me project object content field
/// </summary>
/// <param name="project">project data row</param>
/// <param name="fileID">file object ID</param>
/// <returns>read.me project object content</returns>
protected string GetObjectReadMeContent(CustProject project, Guid fileID)
{
return string.Format("<File AppRelativePath=\"App_Data\\{0}_read.me\" FileID=\"{1}\" />", project.Name, fileID);
}
/// <summary>
/// Build the read.me path for its upload file reference
/// </summary>
/// <param name="project">project data row</param>
/// <returns>read.me project object path</returns>
protected string GetUploadFileReadMePath(CustProject project)
{
return string.Format(@"\CstFile-{0}-App_Data-{1}_read.me", project.Name, project.Name);
}
/// <summary>
/// Pull text content out of the project read.me file
/// </summary>
/// <param name="project">project data row</param>
/// <returns>read.me file content</returns>
protected string GetReadMeContentText(CustProject project)
{
string filePath = GetProjectReadMePath(project);
return File.ReadAllText(filePath);
}
/// <summary>
/// Pull content as binary out of the project read.me file
/// </summary>
/// <param name="project">project data row</param>
/// <returns>read.me file content</returns>
protected byte[] GetReadMeContentBytes(CustProject project)
{
string filePath = GetProjectReadMePath(project);
return File.ReadAllBytes(filePath);
}
/// <summary>
/// Save customization project read.me content to the file system to be included in its package
/// </summary>
internal void SaveReadMeToFile()
{
CustProject project = Base.Projects.Current;
if (project == null) return;
CustProjectExt projectExt = project.GetExtension<CustProjectExt>();
// Write the read.me content to the web server file system. The read.me file path includes
// the project name.
File.WriteAllText(GetProjectReadMePath(project), projectExt.UsrReadMe);
}
/// <summary>
/// Save customization project read.me file as a file object for a project in order to include
/// in its package
/// </summary>
internal void SaveReadMeToDB()
{
CustProject project = Base.Projects.Current;
if (project == null) return;
// Look for the project read.me file in the project package file references
string objFileName = GetObjectReadMePath(project);
CustObject fileObject = ProjectObjects.Select().RowCast<CustObject>().FirstOrDefault(o => o.Name == objFileName);
// Search for the file reference according to its special Acumatica-formatted and delimited name
string uploadFileName = GetUploadFileReadMePath(project);
UploadFile uploadFile = ProjectFiles.SelectSingle(uploadFileName);
UploadFileRevision uploadFileRev = ProjectFileRevisions.SelectSingle(uploadFile?.FileID);
// Read file into memory
byte[] readMeFile = GetReadMeContentBytes(project);
// Save to the db
using (var tran = new PXTransactionScope())
{
Guid objectID = fileObject == null ? Guid.NewGuid() : fileObject.ObjectID.Value;
Guid fileID = uploadFile == null ? Guid.NewGuid() : uploadFile.FileID.Value;
// If the file object is not referenced by the customization, add the object
if (fileObject == null)
{
PXDatabase.Insert<CustObject>(
new PXDataFieldAssign(nameof(CustObject.objectID), PXDbType.UniqueIdentifier, objectID),
new PXDataFieldAssign(nameof(CustObject.name), PXDbType.NVarChar, objFileName),
new PXDataFieldAssign(nameof(CustObject.type), PXDbType.NVarChar, "File"),
new PXDataFieldAssign(nameof(CustObject.projectID), PXDbType.UniqueIdentifier, project.ProjID),
new PXDataFieldAssign(nameof(CustObject.content), PXDbType.NVarChar, GetObjectReadMeContent(project, fileID)),
new PXDataFieldAssign(nameof(CustObject.description), PXDbType.NVarChar, string.Format("{0} README file", project.Name)),
new PXDataFieldAssign(nameof(CustObject.isDisabled), PXDbType.Bit, false),
new PXDataFieldAssign(nameof(CustObject.createdByID), PXDbType.UniqueIdentifier, Base.Accessinfo.UserID),
new PXDataFieldAssign(nameof(CustObject.createdDateTime), PXDbType.DateTime, DateTime.Now),
new PXDataFieldAssign(nameof(CustObject.lastModifiedByID), PXDbType.UniqueIdentifier, Base.Accessinfo.UserID),
new PXDataFieldAssign(nameof(CustObject.lastModifiedDateTime), PXDbType.DateTime, DateTime.Now),
new PXDataFieldAssign(nameof(CustObject.noteID), PXDbType.UniqueIdentifier, Guid.NewGuid())
);
}
// If the file reference doesn't exist, add the file
if (uploadFile == null)
{
PXDatabase.Insert<UploadFile>(
new PXDataFieldAssign(nameof(UploadFile.fileID), PXDbType.UniqueIdentifier, fileID),
new PXDataFieldAssign(nameof(UploadFile.name), PXDbType.NVarChar, string.Format(@"Files ({0}){1}", objectID, uploadFileName)),
new PXDataFieldAssign(nameof(UploadFile.versioned), PXDbType.Bit, false),
new PXDataFieldAssign(nameof(UploadFile.lastRevisionID), PXDbType.Int, 1),
//new PXDataFieldAssign(nameof(UploadFile.primaryScreenID), PXDbType.VarChar, "SM000000"),
new PXDataFieldAssign(nameof(UploadFile.isPublic), PXDbType.Bit, false),
new PXDataFieldAssign(nameof(UploadFile.isAccessRightsFromEntities), PXDbType.Bit, true),
new PXDataFieldAssign(nameof(UploadFile.createdByID), PXDbType.UniqueIdentifier, Base.Accessinfo.UserID),
new PXDataFieldAssign(nameof(UploadFile.createdDateTime), PXDbType.DateTime, DateTime.Now),
new PXDataFieldAssign(nameof(UploadFile.noteID), PXDbType.UniqueIdentifier, Guid.NewGuid())
);
}
// If the file revision doesn't exist, add it
if (uploadFileRev == null)
{
PXDatabase.Insert<UploadFileRevision>(
new PXDataFieldAssign(nameof(UploadFileRevision.fileID), PXDbType.UniqueIdentifier, fileID),
new PXDataFieldAssign(nameof(UploadFileRevision.fileRevisionID), PXDbType.Int, 1),
new PXDataFieldAssign(nameof(UploadFileRevision.Data), PXDbType.VarBinary, readMeFile),
new PXDataFieldAssign(nameof(UploadFileRevision.size), PXDbType.Int, readMeFile.Length),
new PXDataFieldAssign(nameof(UploadFileRevision.createdByID), PXDbType.UniqueIdentifier, Base.Accessinfo.UserID),
new PXDataFieldAssign(nameof(UploadFileRevision.createdDateTime), PXDbType.DateTime, DateTime.Now)
);
}
// If the file reference exists, update the file
else if (uploadFileRev != null)
{
PXDatabase.Update<UploadFileRevision>(
new PXDataFieldRestrict(nameof(UploadFileRevision.fileID), uploadFileRev.FileID),
new PXDataFieldRestrict(nameof(UploadFileRevision.fileRevisionID), uploadFileRev.FileRevisionID),
new PXDataFieldAssign(nameof(UploadFileRevision.Data), PXDbType.VarBinary, readMeFile),
new PXDataFieldAssign(nameof(UploadFileRevision.size), PXDbType.Int, readMeFile.Length)
);
}
tran.Complete();
}
}
#endregion
}
}
@tlanzer-aktion
Copy link
Author

See article at https://community.acumatica.com/develop-customizations-288/readme-for-customization-projects-by-tony-lanzer-18318 for context.

Using a README for an Acumatica customization project is effective in documenting and tracking its purpose and features; and including it within the customization project allows it to be easily referenced and reviewed. The ultimate goal then is to package a README file in a customization project’s packaged .zip file.

To support the creation of README files within a customization project where you (1) don’t have access to the application website folders, or (2) want to create the file within Acumatica; you can create a customization project like I did that accomplishes this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment