Files
Saxonite/Workspace/SaxoniteCoreWebOnline/Online/Controllers/ActivityController-Ajax.cs
T
andy 9f22b522df v1.3.0 Restructure Saxonite workspace and upgrade to .NET 10 (#1)
Move Saxonite’s projects into a dedicated `Workspace` folder and align the solution with .NET 10 and the current Common workspace. Update encryption integration, test project structure and coverage, and repository guidance and upgrade notes.

- Relocate application and library projects while retaining Git rename history.
- Split tests into Admin, Online, and Shared projects; add representative behavioral and serialization tests.
- Update Common integration, application version, and repository documentation.

Application version: 1.3.0

Reviewed-on: #1
Co-authored-by: andy <andy@sarand.com>
2026-10-16 18:24:16 +01:00

454 lines
19 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Dbas;
using Sarand.Net.Standard.Base.Utils;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Core.Web.Online.App;
using Saxonite.Net.Core.Web.Online.Models;
using Saxonite.Net.Standard.Base.App;
using Saxonite.Net.Standard.Base.Bus;
using Saxonite.Net.Standard.Base.Consoles;
using Saxonite.Net.Standard.Base.Mgr;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
// Online Activity Controller - Ajax actions only.
//=======================================================================
public partial class ActivityController : SaxController_Online
{
//-------------------------------------------------------------------
// Construction.
public ActivityController(SaxLocaliser localiser, ILogger<HomeController> logger) :
base(localiser, logger)
{ }
//-------------------------------------------------------------------
// Data definitions.
/// <summary>Activity details for home page list.</summary>
private class ActivityResponseData
{
public short ItemType { get; set; }
public String ItemTypeClass { get; set; }
public String ItemId { get; set; }
public String Organisation { get; set; }
public String OrganisationLogoUrl { get; set; }
public String Author { get; set; }
public String Caption { get; set; }
public String Blurb { get; set; }
public String ThumbnailUrl { get; set; }
public String Sport { get; set; }
public String Region { get; set; }
public int? Reaction { get; set; } = null;
public String PublishedOnFmt { get; set; }
}
/// <summary>Article activity info. </summary>
private class ActivityResponseData_Article
{
public ActivityResponseData_Article(SaxController_Online controller, SaxModel_Online model, SaxArticle article, SaxOrganisation organisation)
{
Organisation = organisation?.Name ?? null;
if (controller.ItemDataMgr.ItemHasActiveImage(organisation?.GetItemId() ?? null, Sax.ImageType.OrganisationLogo))
OrganisationLogoUrl = controller.GetOrganisationLogoLink(organisation.Id, false);
Author = model.GetUserAccountDisplayName(article.UserAccountIdAuthor);
Headline = article.Headline;
Summary = article.Summary;
Sport = model.GetSportCaption(article.SportIdPrimary);
Region = model.GetRegionCaption(article.RegionIdPrimary);
PublishedOnFmt = model.Localiser.FormatDate(article.PublishedAt);
SaxArticle.ContentData content = SaxArticle.ExtractContent(article.BodyData);
foreach (SaxArticle.ContentData.SegmentData segment in content.Segments)
Content.Segments.Add(new ContentData.SegmentData(controller, segment));
}
public String Organisation { get; set; }
public String OrganisationLogoUrl { get; set; }
public String Author { get; set; }
public String Headline { get; set; }
public String Summary { get; set; }
public String Sport { get; set; }
public String Region { get; set; }
public String PublishedOnFmt { get; set; }
public class ContentData
{
public class SegmentData
{
public SegmentData(SaxController_Online controller, SaxArticle.ContentData.SegmentData segment)
{
if (segment.Body.IsValid())
{
Body = new BodyData();
Body.Html = segment.Body.Html;
}
else if (segment.Image.IsValid())
{
Image = new ImageData();
Image.Link = controller.GetItemImageLink(segment.Image.ItemImageId);
}
}
public class BodyData
{
public String Html { get; set; }
}
public BodyData Body { get; }
public class ImageData
{
public String Link { get; set; }
}
public ImageData Image { get; }
}
public List<SegmentData> Segments { get; } = new List<SegmentData>();
}
public ContentData Content { get; } = new ContentData();
}
/// <summary>Event activity info. </summary>
private class ActivityResponseData_Event
{
public ActivityResponseData_Event(SaxController_Online controller, SaxModel_Online model, SaxOrganisationEvent organisationEvent, SaxOrganisation organisation)
{
Organisation = model.GetOrganisationName(organisationEvent.OrganisationId);
if (controller.ItemDataMgr.ItemHasActiveImage(organisation?.GetItemId() ?? null, Sax.ImageType.OrganisationLogo))
OrganisationLogoUrl = controller.GetOrganisationLogoLink(organisation.Id, false);
EventAt = model.FormatDateForDisplay(organisationEvent.EventAt);
if (organisationEvent.EventAt.Hour > 0 || organisationEvent.EventAt.Minute > 0)
EventAt += DateUtil.Format(organisationEvent.EventAt, " HH:ss");
Summary = organisationEvent.Summary;
Details = organisationEvent.Details;
Sport = model.GetSportCaption(organisationEvent.SportIdPrimary);
Region = model.GetRegionCaption(organisationEvent.RegionIdCountry);
PublishedOnFmt = model.Localiser.FormatDate(organisationEvent.PublishedAt);
}
public String Organisation { get; set; }
public String OrganisationLogoUrl { get; set; }
public String EventAt { get; set; }
public String Summary { get; set; }
public String Details { get; set; }
public String Sport { get; set; }
public String Region { get; set; }
public String PublishedOnFmt { get; set; }
}
/// <summary>Opportunity activity info. </summary>
private class ActivityResponseData_Opportunity
{
public ActivityResponseData_Opportunity(SaxController_Online controller, SaxModel_Online model, SaxOrganisationOpportunity opportunity, SaxOrganisation organisation)
{
Organisation = model.GetOrganisationName(opportunity.OrganisationId);
if (controller.ItemDataMgr.ItemHasActiveImage(organisation?.GetItemId() ?? null, Sax.ImageType.OrganisationLogo))
OrganisationLogoUrl = controller.GetOrganisationLogoLink(organisation.Id, false);
Position = opportunity.Position;
Outline = opportunity.Outline;
Sport = model.GetSportCaption(opportunity.SportIdPrimary);
Region = model.GetRegionCaption(opportunity.RegionIdPrimary);
PublishedOnFmt = model.Localiser.FormatDate(opportunity.PublishedAt);
}
public String Organisation { get; set; }
public String OrganisationLogoUrl { get; set; }
public String Position { get; set; }
public String Outline { get; set; }
public String Sport { get; set; }
public String Region { get; set; }
public String PublishedOnFmt { get; set; }
}
//-------------------------------------------------------------------
// Activities
//-------------------------------------------------------------------
private class AjaxResponse_ActivityList : AjaxResponse_Base
{
public List<ActivityResponseData> Activities { get; } = new List<ActivityResponseData>();
}
//-------------------------------------------------------------------
[HttpPost]
[Route("Activity/Ajax/GetList")]
[Route("Activity/Ajax/GetActivityList")]
[Route("Activity/Ajax/GetActivities")]
public IActionResult Ajax_GetActivityList(Model_Activity model)
{
String fn = GetType().Name + ".Ajax_GetActivityList";
String messageTitle = Localiser["activity.caption.activities"];
AjaxResponse_ActivityList responseData = new AjaxResponse_ActivityList();
// Validate session and referrer.
if (!ValidReferer) return Forbid();
if (!ValidUserSession) return GetAjaxResult_InvalidSession(responseData);
RefreshModel(model);
SaxSessionData_Online sessionData = SessionData_Online;
// User context.
SysId userAccountId = SessionUserAccountId;
SaxActivityConsole.IndexContext indexContext = new SaxActivityConsole.IndexContext(userAccountId);
if (SysId.IsValid(userAccountId))
indexContext.UserAccountIdsOwner.Add(userAccountId);
SysId organisationId = model.DecryptId(model.Filter.OrganisationId);
if (SysId.IsValid(organisationId))
indexContext.OrganisationIdsOwner.Add(organisationId);
// Period.
indexContext.EffectiveTo = DateTime.Today;
// Search filter is dissected to extract regions and sports.
String searchFilter = model.Filter.Search.TrimAll();
List<SysId> sportIds = ExtractSearchFilterSports(ref searchFilter);
List<SysId> regionIds = ExtractSearchFilterRegions(ref searchFilter);
// Sport and region combinations.
if (sportIds.IsEmpty())
{
// No sports extractd from search filter - default to user's sports.
foreach (SysId sportId in sessionData.Profile.Sports.Select(item => item.SportId))
{
if (sportIds.Contains(sportId)) continue;
sportIds.AddId(sportId);
// Include child sports.
sportIds.AddIds(model.GetChildSports_Active(sportId).Select(item => item.Id));
model.GetSportCaption(sportId);
}
}
if (regionIds.IsEmpty())
{
// No regions extracted from search filter - default to user's region(s).
regionIds.AddId(sessionData.User.RegionIdProvince);
if (regionIds.IsEmpty())
regionIds.AddId(sessionData.User.RegionIdCountry);
}
foreach (SysId sportId in sportIds)
{
foreach (SysId regionId in regionIds)
indexContext.SportRegionIdsIncluded.Add(new SaxActivityIndex.SportRegionIds(sportId, regionId));
}
// Populate activities.
List<ItemId> activityItemIds = new List<ItemId>();
SaxActivityConsole.IndexResults indexResults = ActivityConsole.QueryIndex(indexContext);
foreach (SaxActivityConsole.IndexResults.ActivityData itemData in indexResults.Items)
{
activityItemIds.Add(itemData.ItemId);
ActivityResponseData activityData = new ActivityResponseData();
activityData.ItemType = itemData.ItemId.Type;
if (itemData.ItemId.Type == (int)Sax.DataItemType.Article)
activityData.ItemTypeClass = "Article";
else if (itemData.ItemId.Type == (int)Sax.DataItemType.OrganisationEvent)
activityData.ItemTypeClass = "Event";
else if (itemData.ItemId.Type == (int)Sax.DataItemType.OrganisationOpportunity)
activityData.ItemTypeClass = "Opportunity";
// ToDo Mask activity index Id with cross-reference stored in session data.
activityData.ItemId = model.EncryptId(itemData.ItemId.Id);
// Organisation.
activityData.Organisation = itemData.Organisation;
// Organisation Logo URL.
if (SysId.IsValid(itemData.OrganisationId) && itemData.OrganisationHasLogo)
activityData.OrganisationLogoUrl = GetOrganisationLogoLink(itemData.OrganisationId, false);
// Author.
activityData.Author = itemData.Author;
// Thumbnail image URL.
if (SysId.IsValid(itemData.ItemImageIdThumbnail))
activityData.ThumbnailUrl = GetItemImageLink(itemData.ItemImageIdThumbnail);
// Activity details.
activityData.Caption = itemData.Caption;
activityData.Blurb = itemData.Blurb;
activityData.Sport = Localiser.GetSportName(itemData.SportId, itemData.SportName);
activityData.Region = Localiser.GetRegionName(itemData.RegionId, itemData.RegionName);
if ((int)itemData.Reaction > 0)
activityData.Reaction = (int)itemData.Reaction;
activityData.PublishedOnFmt = Localiser.FormatDate(itemData.EffectiveOn);
responseData.Activities.Add(activityData);
}
if (!activityItemIds.IsEmpty())
ActivityConsole.RecordUserActivitiesListed(userAccountId, activityItemIds, DbUpdateInfo);
// Complete response.
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
/// <summary>Extract Sports from activity search filter.</summary>
private List<SysId> ExtractSearchFilterSports(ref string searchFilter)
{
List<SysId> sportIds = new List<SysId>();
if (StringUtil.IsEmpty(searchFilter)) return sportIds;
foreach (SaxSport.Data sport in DataCacheConsole.GetActiveSports())
{
Regex reSport = new Regex("\\b" + Regex.Escape(sport.Name) + "\\b", RegexOptions.IgnoreCase);
Match match = reSport.Match(searchFilter);
if (match.Success)
{
sportIds.Add(sport.Id);
searchFilter = reSport.Replace(searchFilter, " ").TrimAll();
}
}
return sportIds;
}
/// <summary>Extract Regions from activity search filter.</summary>
private List<SysId> ExtractSearchFilterRegions(ref string searchFilter)
{
List<SysId> regionIds = new List<SysId>();
if (StringUtil.IsEmpty(searchFilter)) return regionIds;
foreach (SaxRegion.Data region in DataCacheConsole.GetActiveRegions())
{
Regex reRegion = new Regex("\\b" + Regex.Escape(region.Name) + "\\b", RegexOptions.IgnoreCase);
Match match = reRegion.Match(searchFilter);
if (match.Success)
{
regionIds.Add(region.Id);
searchFilter = reRegion.Replace(searchFilter, " ").TrimAll();
}
}
return regionIds;
}
//-------------------------------------------------------------------
// Activity Info
//-------------------------------------------------------------------
private class AjaxResponse_ActivityInfo : AjaxResponse_Base
{
public ActivityResponseData_Article Article { get; set; }
public ActivityResponseData_Event Event { get; set; }
public ActivityResponseData_Opportunity Opportunity { get; set; }
}
//-------------------------------------------------------------------
[HttpPost]
[Route("Activity/Ajax/GetInfo")]
[Route("Activity/Ajax/GetItemInfo")]
[Route("Activity/Ajax/GetActivityInfo")]
[Route("Activity/Ajax/GetActivity")]
public IActionResult Ajax_GetActivityInfo(Model_Activity model)
{
String fn = GetType().Name + ".Ajax_GetActivityInfo";
String messageTitle = Localiser["activity.caption.activity"];
AjaxResponse_ActivityInfo responseData = new AjaxResponse_ActivityInfo();
// Validate session and referrer.
if (!ValidReferer) return Forbid();
if (!ValidUserSession) return GetAjaxResult_InvalidSession(responseData);
RefreshModel(model);
SaxSessionData_Online sessionData = SessionData_Online;
// Record disposable items as they are created so all can be disposed when needed.
using (DisposableCollection<IDisposable> disposableItems = new DisposableCollection<IDisposable>())
{
// Identify activity.
Sax.DataItemType itemType = (Sax.DataItemType)StringUtil.ToShort(model.ActivityItemType);
SysId itemId = model.ExtractSysId(model.ActivityItemId);
if (itemType <= 0 || !SysId.IsValid(itemId))
{
SaLogger.Warn(fn + " Activity type/Id inncorrectly specified." + GetLogSuffix());
return Json(responseData);
}
// Retrieve activity details.
if (itemType == Sax.DataItemType.Article)
{
// Article.
SaxArticle article = ActivityMgr.GetArticleById(itemId);
disposableItems.Add(article);
if (article == null)
{
SaLogger.Warn(fn + " Article not found for Activity Id: " + itemId.ToString() + "." + GetLogSuffix());
return Json(responseData);
}
SaxOrganisation organisation = AccountMgr.GetOrganisationById(article.OrganisationId);
disposableItems.Add(organisation);
responseData.Article = new ActivityResponseData_Article(this, model, article, organisation);
}
else if (itemType == Sax.DataItemType.OrganisationEvent)
{
// Organisation Event.
SaxOrganisationEvent organisationEvent = ActivityMgr.GetOrganisationEventById(itemId);
disposableItems.Add(organisationEvent);
if (organisationEvent == null)
{
SaLogger.Warn(fn + " Event not found for Activity Id: " + itemId.ToString() + "." + GetLogSuffix());
return Json(responseData);
}
SaxOrganisation organisation = AccountMgr.GetOrganisationById(organisationEvent.OrganisationId);
disposableItems.Add(organisation);
responseData.Event = new ActivityResponseData_Event(this, model, organisationEvent, organisation);
}
else if (itemType == Sax.DataItemType.OrganisationOpportunity)
{
// Organisation Opportunity.
SaxOrganisationOpportunity opportunity = ActivityMgr.GetOrganisationOpportunityById(itemId);
disposableItems.Add(opportunity);
if (opportunity == null)
{
SaLogger.Warn(fn + " Opportunity not found for Activity Id: " + itemId.ToString() + "." + GetLogSuffix());
return Json(responseData);
}
SaxOrganisation organisation = AccountMgr.GetOrganisationById(opportunity.OrganisationId);
disposableItems.Add(organisation);
responseData.Opportunity = new ActivityResponseData_Opportunity(this, model, opportunity, organisation);
}
// Record activity viewing.
if (!ActivityConsole.RecordUserActivityViewed(SessionUserAccountId, new ItemId((short)itemType, itemId), DbUpdateInfo))
{
SaLogger.Warn(fn + " Failed to record Activity Viewing: " + ActivityConsole.ErrorStateMessage + "." + GetLogSuffix());
return Json(responseData);
}
}
// Complete response.
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
// Activity Reaction.
//-------------------------------------------------------------------
private class AjaxResponse_ActivityReaction : AjaxResponse_Base
{
public int Reaction { get; set; }
}
//-------------------------------------------------------------------
[HttpPost]
[Route("Activity/Ajax/React")]
[Route("Activity/Ajax/Reaction")]
public IActionResult Ajax_SetActivityReaction(Model_Activity model)
{
String fn = GetType().Name + ".Ajax_SetActivityReaction";
String messageTitle = Localiser["activity.caption.activity"];
AjaxResponse_ActivityReaction responseData = new AjaxResponse_ActivityReaction();
// Validate session and referrer.
if (!ValidReferer) return Forbid();
if (!ValidUserSession) return GetAjaxResult_InvalidSession(responseData);
RefreshModel(model);
SaxSessionData_Online sessionData = SessionData_Online;
// Identify activity.
Sax.DataItemType itemType = (Sax.DataItemType)StringUtil.ToShort(model.ActivityItemType);
SysId itemId = model.ExtractSysId(model.ActivityItemId);
if (itemType <= 0 || !SysId.IsValid(itemId))
{
SaLogger.Warn(fn + " Activity type/Id inncorrectly specified." + GetLogSuffix());
return Json(responseData);
}
// Record activity reaction.
if (!ActivityConsole.RecordUserActivityReaction(SessionUserAccountId, new ItemId((short)itemType, itemId), model.GetActivityReaction(), DbUpdateInfo))
{
SaLogger.Warn(fn + " Failed to record Activity Reaction: " + ActivityConsole.ErrorStateMessage + "." + GetLogSuffix());
return Json(responseData);
}
// Complete response.
responseData.Reaction = (int)model.GetActivityReaction();
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
}
//=======================================================================
}