Try our conversational search powered by Generative AI!

Reporting on all changes made by editors

Vote:
 

Hello,

I'm currently on EPiServer DXC and am trying to find a way to make the change log that is found under "Admin -> Tools" more usable when viewed.

The problem I am currently facing is that any change in the change log shows the block with an ID reference to it, but doesn't specify where this block currently sits, you have to manually search for the ID and look from there. This isn't a sustainable solution as with a large number of edits happening each week, it would consume a fair amount of time.

I am thinking along the lines of using the ID provided to locate the block and then find the parents of said block, but do not know how to impliment this. I have spent days searching for guides, but they all seem to be for older versions, using depreicated code (EPiServer.dll Chnagelog) or methods that aren't seemingly present in the newest version.

The only place where the changelog sits appears to be in the CMS.zip folder under _protected, which would auto update and overwrite any changes I make whenever an update is pushed out.

How would I go about doing this?

#197888
Oct 16, 2018 18:02
Vote:
 

Hi Joseph,

As far as I can see, if this is what you want to do you shouldn't be looking to replace the current plugin ASPX -- but should instead register a new plugin.

Creating a plug-in in Episerver is pretty easy, and in this case we can reuse as much as possible. I just put this together this example in Quicksilver and it seems to work (although undoubtedly the code could use some clean-up).

First, create a class inheriting ViewChangeLog (EPiServer.UI.Admin):

using System.Linq;
using EPiServer.Core;
using EPiServer.DataAbstraction.Activities;
using EPiServer.Editor;
using EPiServer.PlugIn;
using EPiServer.ServiceLocation;
using EPiServer.UI.Admin;

namespace EPiServer.Reference.Commerce.Site
{
    [GuiPlugIn(Area = PlugInArea.AdminMenu, Description = "", DisplayName = "Change Log w/ Parents", LanguagePath = "/admin/viewchangelogwithparent", SortIndex = 500, Url = "~/ViewChangeLogWithParent.aspx")]
    public class ViewChangeLogWithParent : ViewChangeLog
    {
        private readonly IContentLoader _contentLoader;

        public ViewChangeLogWithParent()
        {
            _contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
        }

        /// <summary>
        /// Returns a formatted edit link to the parent content.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns>A formattted link to the parent, if there is one.</returns>
        public string GetParent(object item)
        {
            var activity = item as Activity;

            if (activity == null)
            {
                return string.Empty;
            }

            var extendedData = activity.ExtendedData;

            if (extendedData.Count == 0)
            {
                return string.Empty;
            }

            string complexReference = extendedData.FirstOrDefault(x => x.Key.Equals("ContentLink")).Value;

            if (string.IsNullOrEmpty(complexReference))
            {
                return string.Empty;
            }

            var contentLink = new ContentReference(complexReference);

            if (ContentReference.IsNullOrEmpty(contentLink))
            {
                return string.Empty;
            }

            IContent content;

            if (!_contentLoader.TryGet(contentLink, out content))
            {
                return string.Empty;
            }

            if (ContentReference.IsNullOrEmpty(content.ParentLink))
            {
                return string.Empty;
            }

            IContent parentContent;

            if (!_contentLoader.TryGet(contentLink, out parentContent))
            {
                return string.Empty;
            }

            return $"<a href=\"{PageEditing.GetEditUrl(parentContent.ContentLink)}\" target=\"_blank\">{parentContent.Name} - {parentContent.ContentLink.ID.ToString()}</a>";
        }
    }
}

All I've done here is decorated it with the GuiPlugInAttribute this allows me to specify a DisplayName, LanguagePath (so you can specify a custom heading) and URL for the ASPX. It's Area is also set to be the same as the ViewChangeLog. Finally, I added a method to get the parent edit URL from the Activity.

Next I added a ViewChangeLogWithParent.aspx:

<%@ Page Language="C#" AutoEventWireup="True" CodeBehind="ViewChangeLogWithParent.aspx.cs"
    Inherits="EPiServer.Reference.Commerce.Site.ViewChangeLogWithParent" %>
<%@ Register TagPrefix="EPiServerUI" Namespace="EPiServer.UI.WebControls" Assembly="EPiServer.UI, Version=11.7.0.0, Culture=neutral, PublicKeyToken=8fe83dea738b45b7" %>
<asp:content runat="server" contentplaceholderid="MainRegion">
    <!-- Removed form and Javascript from here to keep things relevant -->

    <table class="epi-default epi-marginVertical" cellpadding="0">
        <tr>
            <th>
                <EPiServer:Translate runat="server" Text="/admin/viewchangelog/colheadingsequencenumber" />
            </th>
            <th>
                <EPiServer:Translate runat="server" Text="/admin/viewchangelog/colheadingchangedate" />
            </th>
            <th>
                <EPiServer:Translate runat="server" Text="/admin/viewchangelog/colheadingcategory" />
            </th>
            <th>
                <EPiServer:Translate runat="server" Text="/admin/viewchangelog/colheadingaction" />
            </th>
            <th>
                <EPiServer:Translate runat="server" Text="/admin/viewchangelog/colheadingChangedby" />
            </th>
            <th>
                Parent
            </th>
            <th>
                <EPiServer:Translate runat="server" Text="/admin/viewchangelog/colheadingdata" />
            </th>
        </tr>
        <tbody>
        <asp:Repeater ID="DataRepeater" runat="server">
            <ItemTemplate>
                <tr>
                    <td>
                        <%# DataBinder.Eval(Container.DataItem, "ID") %>
                    </td>
                    <td>
                        <%# DataBinder.Eval(Container.DataItem, "Created") %>
                    </td>
                    <td>
                        <%# GetActivityTypeName(Container.DataItem) %>
                    </td>
                    <td>
                        <%# GetActionName(Container.DataItem) %>
                    </td>
                    <td>
                        <%#: DataBinder.Eval(Container.DataItem, "ChangedBy") %>
                    </td>
                    <td>
                        <%# GetParent(Container.DataItem) %>
                    </td>
                    <td>
                        <%# GetExtendedData(Container.DataItem) %>
                    </td>
                </tr>
            </ItemTemplate>
        </asp:Repeater>
        </tbody>
    </table>
    <div>
        <EPiServerUI:ToolButton ID="PrevButton" OnClick="PrevButton_Click" runat="server" SkinID="ArrowLeft" Text="<%$ Resources: EPiServer, admin/viewchangelog/previous %>" ToolTip="<%$ Resources: EPiServer, admin/viewchangelog/previous %>" />
        <EPiServerUI:ToolButton ID="NextButton" OnClick="NextButton_Click" runat="server" SkinID="ArrowRight" Text="<%$ Resources: EPiServer, admin/viewchangelog/next %>" ToolTip="<%$ Resources: EPiServer, admin/viewchangelog/next %>" />
    </div>
</asp:content>

This is 95% the same as the Episerver ViewChangeLog.aspx, except I've added a column calling GetParent(Container.DataItem). I also changed the translations so they use the same ones as ViewChangeLog (meaning the LanguagePath specified above should only be used to add the heading). Finally, updated the CodeBehind and Inherits.

This works at this point and should just require adding a heading translation.

Just because this can be done doesn't necessarily mean it's a great idea. This data also isn't been written to the activity repository (which drives the change log) but merely added to the view. Could a custom report (which will show in the Episerver Report Center) not fulfill requirements?

#197903
Edited, Oct 17, 2018 4:42
Vote:
 

Hi Jake,

Thank you so much for the code, it makes much more sense that I should make a plugin based on the changelog rather than attempt to edit the existing one. I'm going to go away and try setting this up in my local environment. I don't think it should be an issue that it doesn't write to the activity log as it only needs to appear in the view for reporting purposes (showing which pages have had changes done to them), but thank you for warning me of this as I probably wouldn't have realised this otherwise.

I have looked into custom reports using the reporting features, but I could only seem to have them reporting on the pages that have had changes directly to them (moving blocks on the page, renaming, meta data changes etc.) not the blocks on said pages. I also spent some time reading guides for this but couldn't seem to find anything as to how I could get it to report on block changes, as such I felt that using the changelog which already reported with the granularity I was looking for would be an easier basis to work with.

Joseph

#197927
Oct 17, 2018 12:03
Vote:
 

Great - hope it works out.

I misread the question a little, so I was returning the parent of the content. You'll probably want to use the IContentRepository.GetReferencesToContent opposed to using the ContentLoader to get the parent.

/Jake

#197936
Oct 17, 2018 15:28
Vote:
 

Hi Jake,

I've implemented this into my local site and got it working a treat. Thanks again for the code you've provided, made the creation of the custom element incredibly easy.

I then renamed GetParent() to GetItem() as it was providing me with a quick reference to the changed block and used it as a base for another field in the changelog. This new field used GetParent(), which was initially a duplication of your initial code, but using the IContentRepository as you suggested.

While getting it to print a list of all parents of the block was easy, it also applied to aany page that was altered, listing 100's of parents that happened to link to it in our structure (mega nav page). Validation to prevent this proved a little tricky, in the end I used IContentTypeRepository to assemble a list of content types and put all "page" types into a blacklist. Using this I could then take your "extendedData" IDictionary and take the ContentTypeID of the current block/page and compare it to the blacklist. This allowed me to only return the parents when the item being shown wasn't a page. I have put my method and ctor below, it is fairly messy, but works as intended.

 

    public partial class ViewChangeLogWithParent : ViewChangeLog
    {

        private readonly IContentLoader _contentLoader;
        private readonly IContentRepository _contentRepository;
        private readonly IContentTypeRepository _contentTypeRepository;
        private static List<int> _blacklist;


        public ViewChangeLogWithParent()
        {
            _contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
            _contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
            _contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
            _blacklist = new List<int>(); //declare blacklist as a list
            var types = _contentTypeRepository.List(); //assign a list of all content types
            List<ContentType> alltypes = types.ToList(); //convert from IEnum to List

            for (int i = 0; i < alltypes.Count; i++) //for each content type
            {
                if (alltypes[i].FullName.Contains("Page")) //if the content type is for a page
                {
                    _blacklist.Add(alltypes[i].ID); //add the contenttype ID to the blacklist
                }
            }
        }
//GetItem() goes in here, code is above in the thread as GetParent()
public string GetParent(object item)
        {

            if (!(item is Activity activity))
            {
                return string.Empty;
            }

            var extendedData = activity.ExtendedData;

            if (extendedData.Count == 0)
            {
                return string.Empty;
            }

            extendedData.TryGetValue("ContentTypeID", out string pagetype); //get ContentTypeID from IDictionary
            Int32.TryParse(pagetype, out int pagetypeid); //convert to int to compare with blacklist
            if (_blacklist.IndexOf(pagetypeid) == -1) //check if contentID matches any ID in the blacklist, if it doesn't continue running the code
            {
                //only blocks from beyond this point
                string complexReference = extendedData.FirstOrDefault(x => x.Key.Equals("ContentLink")).Value;

            if (string.IsNullOrEmpty(complexReference))
            {
                return string.Empty;
            }

            var contentLink = new ContentReference(complexReference);

            if (ContentReference.IsNullOrEmpty(contentLink))
            {
                return string.Empty;
            }


            if (!_contentLoader.TryGet(contentLink, out IContent content))
            {
                return string.Empty;
            }

            if (ContentReference.IsNullOrEmpty(content.ParentLink))
            {
                return string.Empty;
            }

            IEnumerable<ReferenceInformation> parentinfo = _contentRepository.GetReferencesToContent(contentLink, true);

            List<ReferenceInformation> parent = parentinfo.ToList();
            
            List<string> output = new List<string>();

                for (int i = 0; i < parent.Count; i++) //for each parent, assemble a hyperlinked string
                {
                    output.Add(string.Format("<a href=\"{0}\" target=\"_blank\">{1} - {2}</a><br />", PageEditing.GetEditUrl(parent[i].OwnerID), parent[i].OwnerName, parent[i].OwnerID));
                }

                return string.Join(",", output.ToArray()); //turn list to array, then to string and return
            }
            else
            {
                //if content is a page
                return string.Empty;
            }
        }
#198542
Oct 30, 2018 17:55
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.