Last active
May 12, 2019 19:55
-
-
Save PaulGruffyddAmaze/69cbe6ed1020d85b39898e57cc9f6094 to your computer and use it in GitHub Desktop.
PoC for getting lists of popular content from Episerver Profile Store
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ScheduledPlugIn(DisplayName = "Index Recent Page Hits", DefaultEnabled = true, IntervalLength = 1, IntervalType = EPiServer.DataAbstraction.ScheduledIntervalType.Hours)] | |
public class IndexRecentHits : ScheduledJobBase | |
{ | |
//Settings | |
private readonly string _apiRootUrl = ConfigurationManager.AppSettings["episerver:profiles.ProfileApiBaseUrl"]; | |
private readonly string _appKey = ConfigurationManager.AppSettings["episerver:profiles.ProfileApiSubscriptionKey"]; | |
private readonly string _eventUrl = "/api/v1.0/trackevents/"; | |
private readonly string _timeWindow = ConfigurationManager.AppSettings["RecentHours"] ?? "24"; | |
private readonly int _resultsPerPage = 1000; | |
private Dictionary<string, int> _recentHits = new Dictionary<string, int>(); | |
private bool _stopSignaled; | |
private static DynamicDataStoreFactory _dataStoreFactory; | |
private static IContentLoader _contentLoader; | |
public IndexRecentHits(DynamicDataStoreFactory dataStoreFactory, IContentLoader contentLoader) | |
{ | |
_dataStoreFactory = dataStoreFactory; | |
_contentLoader = contentLoader; | |
} | |
public IndexRecentHits() | |
{ | |
IsStoppable = true; | |
} | |
/// <summary> | |
/// Called when a user clicks on Stop for a manually started job, or when ASP.NET shuts down. | |
/// </summary> | |
public override void Stop() | |
{ | |
_stopSignaled = true; | |
} | |
/// <summary> | |
/// Called when a scheduled job executes | |
/// </summary> | |
/// <returns>A status message to be stored in the database log and visible from admin mode</returns> | |
public override string Execute() | |
{ | |
//Call OnStatusChanged to periodically notify progress of job for manually started jobs | |
OnStatusChanged(String.Format("Beginning processing of recent hits")); | |
var totalProcessed = 0; | |
var errorCount = 0; | |
//Get the recent hit counts | |
if (!int.TryParse(_timeWindow, out int recentHours)) | |
{ | |
recentHours = 24; | |
} | |
var fromDate = DateTime.Now.AddHours(0 - recentHours).ToUniversalTime().ToString("o"); | |
// Set up the request | |
var request = GetTrackingRequest($"EventType eq epiPageView and EventTime gt {fromDate}", _resultsPerPage); | |
ProcessEventResults(1, request); | |
if (_stopSignaled) | |
{ | |
return "Execution was cancelled by user"; | |
} | |
var store = _dataStoreFactory.CreateStore(typeof(RecentHit)); | |
store.DeleteAll(); | |
foreach (var hit in _recentHits) | |
{ | |
if (_stopSignaled) | |
{ | |
return "Execution was cancelled by user"; | |
} | |
try | |
{ | |
var keyParts = hit.Key.Split('_'); | |
var page = _contentLoader.Get<SitePageData>(new Guid(keyParts.FirstOrDefault() ?? Guid.Empty.ToString())); | |
var recentHit = new RecentHit | |
{ | |
PageId = page.ContentLink.ID, | |
PageTypeId = page.ContentTypeID, | |
Parents = _contentLoader.GetAncestors(page.ContentLink).Select(x => x.ContentLink.ID).ToArray(), | |
Language = keyParts.LastOrDefault() ?? "en", | |
Hits = hit.Value | |
}; | |
store.Save(recentHit); | |
} | |
catch (Exception) | |
{ | |
errorCount++; | |
} | |
totalProcessed++; | |
if (totalProcessed.ToString().EndsWith("0")) | |
{ | |
OnStatusChanged($"Indexed {totalProcessed} of {_recentHits.Count} with {errorCount} errors"); | |
} | |
} | |
return $"Reindexed {totalProcessed} pages with {errorCount} errors"; | |
} | |
#region Private Methods | |
/// <summary> | |
/// Makes a request to ProfileStore and processes results | |
/// </summary> | |
private void ProcessEventResults(int page, RestRequest request) | |
{ | |
OnStatusChanged($"Fetching hits page {page}"); | |
if (_stopSignaled) | |
{ | |
return; | |
} | |
//Handle pagination | |
request.AddOrUpdateParameter("$skip", (page - 1) * _resultsPerPage); | |
// Execute the request to get the events matching the filter | |
var eventResponseObject = GetTrackingResponse(request); | |
foreach (var result in eventResponseObject.Items) | |
{ | |
//Add/update the hit count per event | |
var key = $"{result.Payload.Epi.ContentGuid}_{result.Payload.Epi.Language}"; | |
if (_recentHits.ContainsKey(key)) | |
{ | |
_recentHits[key]++; | |
} | |
else | |
{ | |
_recentHits.Add(key, 1); | |
} | |
} | |
//Repeat until all pages of results have been processed | |
if (eventResponseObject.Total > _resultsPerPage * page) | |
{ | |
ProcessEventResults(page + 1, request); | |
} | |
} | |
/// <summary> | |
/// Builds the ProfileStore request | |
/// </summary> | |
private RestRequest GetTrackingRequest(string filter, int resultsPerPage) | |
{ | |
var req = new RestRequest(_eventUrl, Method.GET); | |
req.AddHeader("Ocp-Apim-Subscription-Key", _appKey); | |
req.AddParameter("$top", resultsPerPage); | |
req.AddParameter("$filter", filter); | |
return req; | |
} | |
/// <summary> | |
/// Serialises the ProfileStore response into an object | |
/// </summary> | |
private TrackingObjectResponse GetTrackingResponse(RestRequest request) | |
{ | |
var client = new RestClient(_apiRootUrl); | |
var getEventResponse = client.Execute(request); | |
return JsonConvert.DeserializeObject<TrackingObjectResponse>(getEventResponse.Content); | |
} | |
#endregion | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class RecentHit : IDynamicData | |
{ | |
public Identity Id { get; set; } | |
public int PageId { get; set; } | |
public string Language { get; set; } | |
public int PageTypeId { get; set; } | |
public int[] Parents { get; set; } | |
public long Hits { get; set; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class RecentHitsUtils | |
{ | |
//Stuff to be injected | |
private DynamicDataStoreFactory _dataStoreFactory; | |
private IContentTypeRepository _contentTypeRepository; | |
private IContentLoader _contentLoader; | |
#region Constructors | |
public RecentHitsUtils(ISynchronizedObjectInstanceCache cache, DynamicDataStoreFactory dataStoreFactory, IContentTypeRepository contentTypeRepository, IContentLoader contentLoader) | |
{ | |
_dataStoreFactory = dataStoreFactory; | |
_contentTypeRepository = contentTypeRepository; | |
_contentLoader = contentLoader; | |
} | |
#endregion | |
#region Public Methods | |
public IEnumerable<T> GetPopularPages<T>(ContentReference ancestor, string language, int numberOfResults) where T : PageData | |
{ | |
var store = _dataStoreFactory.CreateStore(typeof(RecentHit)); | |
var contentTypeId = _contentTypeRepository.Load<T>().ID; | |
var hits = store.Items<RecentHit>().Where(x => x.Parents.Contains(ancestor.ID) && x.Language.Equals(language) && x.PageTypeId.Equals(contentTypeId)).OrderByDescending(x => x.Hits).Take(numberOfResults).ToList(); | |
var contentRefs = hits.Select(x => new ContentReference(x.PageId)); | |
return _contentLoader.GetItems(contentRefs, new LoaderOptions() { LanguageLoaderOption.Specific(CultureInfo.GetCultureInfo(language)) }).OfType<T>(); | |
} | |
public IEnumerable<IContent> GetPopularPages(ContentReference ancestor, string language, int numberOfResults) | |
{ | |
var store = _dataStoreFactory.CreateStore(typeof(RecentHit)); | |
var hits = store.Items<RecentHit>().Where(x => x.Parents.Contains(ancestor.ID) && x.Language.Equals(language)).OrderByDescending(x => x.Hits).Take(numberOfResults); | |
var contentRefs = hits.Select(x => new ContentReference(x.PageId)); | |
return _contentLoader.GetItems(contentRefs, new LoaderOptions() { LanguageLoaderOption.Specific(CultureInfo.GetCultureInfo(language)) }); | |
} | |
#endregion | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class TrackingObjectResponse | |
{ | |
public int Total { get; set; } | |
public int Count { get; set; } | |
public List<TrackingItem> Items { get; set; } | |
} | |
public class TrackingItem | |
{ | |
public Payload Payload { get; set; } | |
} | |
public class Payload | |
{ | |
public Epi Epi { get; set; } | |
} | |
public class Epi | |
{ | |
public string ContentGuid { get; set; } | |
public string Language { get; set; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@model TrendingArticlesViewModel | |
<h2>@Model.Heading</h2> | |
<hr /> | |
<ol> | |
@foreach (var article in Model.TrendingArticles) | |
{ | |
<li class="listResult"> | |
<h3> | |
@Html.PageLink(article) | |
</h3> | |
<p class="date">@Html.DisplayFor(x => article.StartPublish)</p> | |
<hr /> | |
</li> | |
} | |
</ol> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ContentType(DisplayName = "Trending Articles Block", GUID = "30d6ef65-d46e-4478-b66c-e54b3385e68d", Description = "")] | |
public class TrendingArticlesBlock : BlockData | |
{ | |
[CultureSpecific] | |
[Display( | |
Name = "Heading", | |
Description = "Heading for this block", | |
GroupName = SystemTabNames.Content, | |
Order = 10)] | |
public virtual string Heading { get; set; } | |
[Display( | |
Name = "No of items", | |
Description = "The number of items to show", | |
GroupName = SystemTabNames.Content, | |
Order = 20)] | |
public virtual int ItemCount { get; set; } | |
[Display( | |
Name = "Root page for items", | |
Description = "The ancestor of the content items returned", | |
GroupName = SystemTabNames.Content, | |
Order = 30)] | |
[Required] | |
public virtual PageReference Root { get; set; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class TrendingArticlesBlockController : BlockController<TrendingArticlesBlock> | |
{ | |
private RecentHitsUtils _recentHitsUtils; | |
public TrendingArticlesBlockController(RecentHitsUtils recentHitsUtils) | |
{ | |
_recentHitsUtils = recentHitsUtils; | |
} | |
public override ActionResult Index(TrendingArticlesBlock currentBlock) | |
{ | |
var lang = (currentBlock as ILocalizable).Language; | |
var trendingArticles = _recentHitsUtils.GetPopularPages<ArticlePage>(ContentReference.StartPage, lang.Name, currentBlock.ItemCount); | |
var viewModel = new TrendingArticlesViewModel | |
{ | |
Heading = currentBlock.Heading, | |
TrendingArticles = trendingArticles | |
}; | |
return PartialView(viewModel); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class TrendingArticlesViewModel | |
{ | |
public string Heading { get; set; } | |
public IEnumerable<ArticlePage> TrendingArticles { get; set; } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment