Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/Business/Grand.Business.Cms/Services/NewsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,72 @@ public virtual async Task<IPagedList<NewsItem>> GetAllNews(string storeId = "",
return await PagedList<NewsItem>.Create(query, pageIndex, pageSize);
}

/// <summary>
/// Gets news for store with configurable limit
/// </summary>
/// <param name="storeId">Store identifier</param>
/// <param name="pageIndex">Page index</param>
/// <param name="pageSize">Page size</param>
/// <param name="storeNewsLimit">Maximum number of news items to return (0 for unlimited)</param>
/// <param name="newsTitle">News title filter</param>
/// <returns>News items</returns>
public virtual async Task<IPagedList<NewsItem>> GetStoreNews(string storeId = "",
int pageIndex = 0, int pageSize = int.MaxValue, int storeNewsLimit = 0, string newsTitle = "")
{
var query = from p in _newsItemRepository.Table
select p;

if (!string.IsNullOrWhiteSpace(newsTitle))
query = query.Where(n => n.Title != null && n.Title.ToLower().Contains(newsTitle.ToLower()));

// Store news should only show published and current items
var utcNow = DateTime.UtcNow;
query = query.Where(n => n.Published);
query = query.Where(n => !n.StartDateUtc.HasValue || n.StartDateUtc <= utcNow);
query = query.Where(n => !n.EndDateUtc.HasValue || n.EndDateUtc >= utcNow);

// Apply ACL and store limitations for store context
if (!_accessControlConfig.IgnoreAcl)
{
var allowedCustomerGroupsIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds();
query = from p in query
where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x))
select p;
}

// Store acl
if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations)
query = from p in query
where !p.LimitedToStores || p.Stores.Contains(storeId)
select p;

query = query.OrderByDescending(n => n.CreatedOnUtc);

// Apply store news limit if specified
if (storeNewsLimit > 0)
{
// First check if the requested page would exceed our limit
var remainingItems = storeNewsLimit - (pageIndex * pageSize);
if (remainingItems <= 0)
{
// No items left for this page
return new PagedList<NewsItem>(new List<NewsItem>(), pageIndex, pageSize, 0);
}

// Adjust page size if it would exceed the limit on this page
if (pageSize > remainingItems)
{
pageSize = remainingItems;
}

// Limit the total query to the store limit and let PagedList handle pagination
var limitedQuery = query.Take(storeNewsLimit);
return await PagedList<NewsItem>.Create(limitedQuery, pageIndex, pageSize);
}

return await PagedList<NewsItem>.Create(query, pageIndex, pageSize);
}

/// <summary>
/// Inserts a news item
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/Business/Grand.Business.Core/Interfaces/Cms/INewsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,16 @@ Task<IPagedList<NewsItem>> GetAllNews(string storeId = "",
/// <param name="customerId">Customer identifier; "" to load all records</param>
/// <returns>Comments</returns>
Task<IList<NewsComment>> GetAllComments(string customerId);

/// <summary>
/// Gets news for store with configurable limit
/// </summary>
/// <param name="storeId">Store identifier</param>
/// <param name="pageIndex">Page index</param>
/// <param name="pageSize">Page size</param>
/// <param name="storeNewsLimit">Maximum number of news items to return (0 for unlimited)</param>
/// <param name="newsTitle">News title filter</param>
/// <returns>News items</returns>
Task<IPagedList<NewsItem>> GetStoreNews(string storeId = "",
int pageIndex = 0, int pageSize = int.MaxValue, int storeNewsLimit = 0, string newsTitle = "");
}
5 changes: 5 additions & 0 deletions src/Core/Grand.Domain/News/NewsSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ public class NewsSettings : ISettings
/// Gets or sets the page size for news archive
/// </summary>
public int NewsArchivePageSize { get; set; }

/// <summary>
/// Gets or sets the maximum number of news items to display in store (0 for unlimited)
/// </summary>
public int StoreNewsLimit { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ public static IEnumerable<DefaultPermission> DefaultPermissions()
StandardPermission.ManageShipments,
StandardPermission.ManageMerchandiseReturns,
StandardPermission.ManageCheckoutAttribute,
StandardPermission.ManageReports
StandardPermission.ManageReports,
StandardPermission.ManageNews
]
},

Expand Down
70 changes: 70 additions & 0 deletions src/Tests/Grand.Business.Cms.Tests/Services/NewsServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,74 @@ public async Task GetAllCommentsTest()
//Assert
Assert.IsTrue(result.Any());
}

[TestMethod]
public async Task GetStoreNews_NoLimit_ReturnsAllPublishedNews()
{
//Arrange
await _repository.InsertAsync(new NewsItem { Published = true, Title = "News 1" });
await _repository.InsertAsync(new NewsItem { Published = true, Title = "News 2" });
await _repository.InsertAsync(new NewsItem { Published = false, Title = "News 3" }); // Should be excluded
//Act
var result = await _newsService.GetStoreNews("", 0, 10, 0); // No limit (storeNewsLimit = 0)
//Assert
Assert.AreEqual(2, result.Count); // Only published news
Assert.IsTrue(result.All(x => x.Published));
}

[TestMethod]
public async Task GetStoreNews_WithLimit_ReturnsLimitedNews()
{
//Arrange
for (int i = 1; i <= 10; i++)
{
await _repository.InsertAsync(new NewsItem { Published = true, Title = $"News {i}" });
}
//Act
var result = await _newsService.GetStoreNews("", 0, 10, 5); // Limit to 5 items
//Assert
Assert.AreEqual(5, result.Count);
Assert.AreEqual(5, result.TotalCount); // Total should respect the limit
}

[TestMethod]
public async Task GetStoreNews_WithLimitAndPagination_ReturnsCorrectPage()
{
//Arrange
for (int i = 1; i <= 15; i++)
{
await _repository.InsertAsync(new NewsItem { Published = true, Title = $"News {i}" });
}
//Act - Request page 2 (index 1) with page size 5, but limit to 8 total items
var result = await _newsService.GetStoreNews("", 1, 5, 8); // Page 2, size 5, limit 8
//Assert
Assert.AreEqual(3, result.Count); // Should have 3 items (8 total - 5 on first page = 3 remaining)
}

[TestMethod]
public async Task GetStoreNews_PageExceedsLimit_ReturnsEmptyResult()
{
//Arrange
for (int i = 1; i <= 10; i++)
{
await _repository.InsertAsync(new NewsItem { Published = true, Title = $"News {i}" });
}
//Act - Request page 3 (index 2) with page size 5, but limit to 8 total items
var result = await _newsService.GetStoreNews("", 2, 5, 8); // Page 3, size 5, limit 8 (no items left)
//Assert
Assert.AreEqual(0, result.Count);
}

[TestMethod]
public async Task GetStoreNews_ExcludesUnpublishedNews()
{
//Arrange
await _repository.InsertAsync(new NewsItem { Published = true, Title = "Published News" });
await _repository.InsertAsync(new NewsItem { Published = false, Title = "Unpublished News" });
//Act
var result = await _newsService.GetStoreNews("", 0, 10, 0);
//Assert
Assert.AreEqual(1, result.Count);
Assert.AreEqual("Published News", result.First().Title);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,14 @@
<span asp-validation-for="NewsSettings.NewsArchivePageSize"></span>
</div>
</div>
<div class="form-group">
<div class="col-8 col-md-4 col-sm-4 text-right">
<admin-label asp-for="NewsSettings.StoreNewsLimit" class="control-label"/>
</div>
<div class="col-4 col-md-8 col-sm-8">
<admin-input asp-for="NewsSettings.StoreNewsLimit"/>
<span asp-validation-for="NewsSettings.StoreNewsLimit"></span>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ public class NewsItemListModel : BaseModel
[GrandResourceDisplayName("Admin.Content.News.NewsItems.List.SearchStore")]
public string SearchStoreId { get; set; }

[GrandResourceDisplayName("Admin.Content.News.NewsItems.List.SearchTitle")]
public string SearchNewsTitle { get; set; }

public IList<SelectListItem> AvailableStores { get; set; } = new List<SelectListItem>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ public class NewsSettingsModel : BaseModel

[GrandResourceDisplayName("Admin.Settings.News.NewsArchivePageSize")]
public int NewsArchivePageSize { get; set; }

[GrandResourceDisplayName("Admin.Settings.News.StoreNewsLimit")]
public int StoreNewsLimit { get; set; }
}

public class KnowledgebaseSettingsModel : BaseModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public NewsViewModelService(INewsService newsService,
public virtual async Task<(IEnumerable<NewsItemModel> newsItemModels, int totalCount)> PrepareNewsItemModel(
NewsItemListModel model, int pageIndex, int pageSize)
{
var news = await _newsService.GetAllNews(model.SearchStoreId, pageIndex - 1, pageSize, true, true);
var news = await _newsService.GetAllNews(model.SearchStoreId, pageIndex - 1, pageSize, true, true, model.SearchNewsTitle);
return (news.Select(x =>
{
var m = x.ToModel(_dateTimeService);
Expand Down
38 changes: 38 additions & 0 deletions src/Web/Grand.Web.Store/Areas/Store/Views/News/Create.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@model NewsItemModel
@{
//page title
ViewBag.Title = Loc["Admin.Content.News.NewsItems.AddNew"];
Layout = Constants.LayoutStore;
}
<form asp-area="@Constants.AreaStore" asp-controller="News" asp-action="Create" method="post">

<div class="row">
<div class="col-md-12">
<div class="x_panel light form-fit">
<div class="x_title">
<div class="caption">
<i class="fa fa-hacker-news"></i>
@Loc["Admin.Content.News.NewsItems.AddNew"]
<small>
<i class="fa fa-arrow-circle-left"></i>@Html.ActionLink(Loc["Admin.Content.News.NewsItems.BackToList"], "List")
</small>
</div>
<div class="actions">
<div class="btn-group btn-group-devided">
<button class="btn btn-success" type="submit" name="save">
<i class="fa fa-check"></i> @Loc["Admin.Common.Save"]
</button>
<button class="btn btn-success" type="submit" name="save-continue">
<i class="fa fa-check-circle"></i> @Loc["Admin.Common.SaveContinue"]
</button>
<vc:admin-widget widget-zone="news_details_buttons" additional-data="null"/>
</div>
</div>
</div>
<div class="x_content form">
<partial name="Partials/CreateOrUpdate" model="Model"/>
</div>
</div>
</div>
</div>
</form>
46 changes: 46 additions & 0 deletions src/Web/Grand.Web.Store/Areas/Store/Views/News/Edit.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@model NewsItemModel
@{
//page title
ViewBag.Title = Loc["Admin.Content.News.NewsItems.EditNewsItemDetails"];
Layout = Constants.LayoutStore;
}
<form asp-area="@Constants.AreaStore" asp-controller="News" asp-action="Edit" method="post">

<div class="row">
<div class="col-md-12">
<div class="x_panel light form-fit">
<div class="x_title">
<div class="caption">
<i class="fa fa-hacker-news"></i>
@Loc["Admin.Content.News.NewsItems.EditNewsItemDetails"] - @Model.Title
<small>
<i class="fa fa-arrow-circle-left"></i>@Html.ActionLink(Loc["Admin.Content.News.NewsItems.BackToList"], "List")
</small>
</div>
<div class="actions">
<div class="btn-group btn-group-devided">
<button type="button" onclick="window.open('@Url.RouteUrl("NewsItem", new { Model.SeName })','_blank');" class="btn purple">
<i class="fa fa-eye"></i>
@Loc["Admin.Common.Preview"]
</button>
<button class="btn btn-success" type="submit" name="save">
<i class="fa fa-check"></i> @Loc["Admin.Common.Save"]
</button>
<button class="btn btn-success" type="submit" name="save-continue">
<i class="fa fa-check-circle"></i> @Loc["Admin.Common.SaveContinue"]
</button>
<span id="newsitem-delete" class="btn red">
<i class="fa fa-trash-o"></i> @Loc["Admin.Common.Delete"]
</span>
<vc:admin-widget widget-zone="news_details_buttons" additional-data="Model"/>
</div>
</div>
</div>
<div class="x_content form">
<partial name="Partials/CreateOrUpdate" model="Model"/>
</div>
</div>
</div>
</div>
</form>
<admin-delete-confirmation button-id="newsitem-delete"/>
Loading