Adding the Reports feature back.

This commit is contained in:
Daniel Stolt 2015-07-09 21:49:02 +01:00
parent 95eecd30b9
commit ad4a702f7c
22 changed files with 542 additions and 0 deletions

View File

@ -223,6 +223,11 @@
<Compile Include="Navigation\ViewModels\MenuItemEntry.cs" />
<Compile Include="Navigation\ViewModels\NavigationManagementViewModel.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Reports\AdminMenu.cs" />
<Compile Include="Reports\Controllers\AdminController.cs" />
<Compile Include="Reports\Routes.cs" />
<Compile Include="Reports\ViewModels\DisplayReportViewModel.cs" />
<Compile Include="Reports\ViewModels\ReportsAdminIndexViewModel.cs" />
<Compile Include="Scheduling\Migrations.cs" />
<Compile Include="Scheduling\Models\ScheduledTaskRecord.cs" />
<Compile Include="Scheduling\Services\ScheduledTaskManager.cs" />
@ -310,6 +315,9 @@
<Content Include="Navigation\Styles\navigation-admin.css" />
<Content Include="Navigation\Styles\images\menu.navigation.png" />
<Content Include="Navigation\Styles\menu.navigation-admin.css" />
<Content Include="Reports\Module.txt" />
<Content Include="Reports\Styles\images\menu.reports.png" />
<Content Include="Reports\Styles\menu.reports-admin.css" />
<Content Include="Settings\Module.txt" />
<Content Include="Settings\Styles\admin.css" />
<Content Include="Settings\Styles\images\menu.settings.png" />
@ -557,6 +565,11 @@
<ItemGroup>
<Content Include="Common\Views\EditorTemplates\Flavor.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Reports\Styles\Web.config" />
<Content Include="Reports\Views\Admin\Display.cshtml" />
<Content Include="Reports\Views\Admin\Index.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@ -0,0 +1,17 @@
using Orchard.Localization;
using Orchard.Security;
using Orchard.UI.Navigation;
namespace Orchard.Core.Reports {
public class AdminMenu : INavigationProvider {
public Localizer T { get; set; }
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder.AddImageSet("reports")
.Add(T("Reports"), "12",
menu => menu.Add(T("View"), "0", item => item.Action("Index", "Admin", new { area = "Reports" })
.Permission(StandardPermissions.SiteOwner)));
}
}
}

View File

@ -0,0 +1,42 @@
using System.Linq;
using System.Web.Mvc;
using Orchard.Core.Reports.ViewModels;
using Orchard.Localization;
using Orchard.Reports.Services;
using Orchard.Security;
namespace Orchard.Core.Reports.Controllers {
public class AdminController : Controller {
private readonly IReportsManager _reportsManager;
public AdminController(
IOrchardServices services,
IReportsManager reportsManager) {
Services = services;
_reportsManager = reportsManager;
T = NullLocalizer.Instance;
}
public IOrchardServices Services { get; set; }
public Localizer T { get; set; }
public ActionResult Index() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to list reports")))
return new HttpUnauthorizedResult();
var model = new ReportsAdminIndexViewModel { Reports = _reportsManager.GetReports().ToList() };
return View(model);
}
public ActionResult Display(int id) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to display report")))
return new HttpUnauthorizedResult();
var model = new DisplayReportViewModel { Report = _reportsManager.Get(id) };
return View(model);
}
}
}

View File

@ -0,0 +1,9 @@
Name: Reports
AntiForgery: enabled
Author: The Orchard Team
Website: http://orchardproject.net
Version: 1.9.1
OrchardVersion: 1.9
Description: The dashboard module is providing the reports screen of the application.
FeatureDescription: Reports management (deprecated).
Category: Core

View File

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.Mvc.Routes;
namespace Orchard.Core.Reports {
public class Routes : IRouteProvider {
public void GetRoutes(ICollection<RouteDescriptor> routes) {
foreach (var routeDescriptor in GetRoutes())
routes.Add(routeDescriptor);
}
public IEnumerable<RouteDescriptor> GetRoutes() {
return new[] {
new RouteDescriptor {
Priority = -5,
Route = new Route(
"Admin/Reports",
new RouteValueDictionary {
{"area", "Reports"},
{"controller", "Admin"},
{"action", "Index"}
},
new RouteValueDictionary(),
new RouteValueDictionary {
{"area", "Reports"}
},
new MvcRouteHandler())
}
};
}
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
</staticContent>
<handlers accessPolicy="Script,Read">
<!--
iis7 - for any request to a file exists on disk, return it via native http module.
accessPolicy 'Script' is to allow for a managed 404 page.
-->
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
</handlers>
</system.webServer>
</configuration>

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

View File

@ -0,0 +1,6 @@
.navicon-reports {
background-image:url(images/menu.reports.png) !important;
}
.navicon-reports:hover {
background-position:0 -30px !important;
}

View File

@ -0,0 +1,7 @@
using Orchard.Reports;
namespace Orchard.Core.Reports.ViewModels {
public class DisplayReportViewModel {
public Report Report { get; set; }
}
}

View File

@ -0,0 +1,8 @@
using System.Collections.Generic;
using Orchard.Reports;
namespace Orchard.Core.Reports.ViewModels {
public class ReportsAdminIndexViewModel {
public IList<Report> Reports { get; set; }
}
}

View File

@ -0,0 +1,39 @@
@model DisplayReportViewModel
@using Orchard.Core.Reports.ViewModels;
@{ Layout.Title = T("Display Report").ToString(); }
@using(Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<fieldset>
<table class="items" summary="@T("This is a table of the reports in your application")">
<colgroup>
<col id="Col1" />
<col id="Col2" />
<col id="Col3" />
<col id="Col4" />
</colgroup>
<thead>
<tr>
<th scope="col">@T("Type")</th>
<th scope="col">@T("Message")</th>
<th scope="col">@T("Date")</th>
<th scope="col"></th>
</tr>
</thead>
@foreach (var reportEntry in Model.Report.Entries) {
<tr>
<td>
@reportEntry.Type
</td>
<td>
@reportEntry.Message
</td>
<td>
@reportEntry.Utc.ToLocalTime().ToShortDateString() @reportEntry.Utc.ToLocalTime().ToShortTimeString()
</td>
</tr>
}
</table>
</fieldset>
}

View File

@ -0,0 +1,39 @@
@model ReportsAdminIndexViewModel
@using Orchard.Core.Reports.ViewModels;
@{ Layout.Title = T("Reports").ToString(); }
@using(Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<fieldset>
<table class="items" summary="@T("This is a table of the reports in your application")">
<colgroup>
<col id="Col1" />
<col id="Col2" />
<col id="Col3" />
<col id="Col4" />
</colgroup>
<thead>
<tr>
<th scope="col">@T("Name")</th>
<th scope="col">@T("Title")</th>
<th scope="col">@T("Date")</th>
<th scope="col"></th>
</tr>
</thead>
@foreach (var report in Model.Reports) {
<tr>
<td>
@Html.ActionLink(report.ActivityName, "Display", new {id = report.ReportId})
</td>
<td>
@report.Title
</td>
<td>
@report.Utc.ToLocalTime().ToShortDateString() @report.Utc.ToLocalTime().ToShortTimeString()
</td>
</tr>
}
</table>
</fieldset>
}

View File

@ -149,6 +149,15 @@
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Compile Include="Reports\Report.cs" />
<Compile Include="Reports\ReportEntry.cs" />
<Compile Include="Reports\ReportExtentions.cs" />
<Compile Include="Reports\Services\IReportsCoordinator.cs" />
<Compile Include="Reports\Services\IReportsManager.cs" />
<Compile Include="Reports\Services\IReportsPersister.cs" />
<Compile Include="Reports\Services\ReportsCoordinator.cs" />
<Compile Include="Reports\Services\ReportsManager.cs" />
<Compile Include="Reports\Services\ReportsPersister.cs" />
<Compile Include="Security\IMembershipValidationService.cs" />
<Compile Include="Localization\Services\ILocalizationStreamParser.cs" />
<Compile Include="Localization\Services\LocalizationStreamParser.cs" />

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
namespace Orchard.Reports {
public class Report {
public Report() {
Entries = new List<ReportEntry>();
}
public IList<ReportEntry> Entries { get; set;}
public int ReportId { get; set; }
public string Title { get; set; }
public string ActivityName { get; set; }
public DateTime Utc { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace Orchard.Reports {
public enum ReportEntryType {
Information,
Warning,
Error
}
public class ReportEntry {
public ReportEntryType Type { get; set; }
public string Message { get; set; }
public DateTime Utc { get; set; }
}
}

View File

@ -0,0 +1,35 @@
using Orchard.Reports;
using Orchard.Reports.Services;
public static class ReportExtentions {
/// <summary>
/// Adds a new report entry of type information to a report that was previously registered.
/// </summary>
/// <seealso cref="Register()"/>
/// <param name="reportKey">Key, i.e. technical name of the report. Should be the same as the one used when registering the report.</param>
/// <param name="message">The message to include in the entry.</param>
public static void Information(this IReportsCoordinator reportCoordinator, string reportKey, string message) {
reportCoordinator.Add(reportKey, ReportEntryType.Information, message);
}
/// <summary>
/// Adds a new report entry of type warning to a report that was previously registered.
/// </summary>
/// <seealso cref="Register()"/>
/// <param name="reportKey">Key, i.e. technical name of the report. Should be the same as the one used when registering the report.</param>
/// <param name="message">The message to include in the entry.</param>
public static void Warning(this IReportsCoordinator reportCoordinator, string reportKey, string message) {
reportCoordinator.Add(reportKey, ReportEntryType.Warning, message);
}
/// <summary>
/// Adds a new report entry of type error to a report that was previously registered.
/// </summary>
/// <seealso cref="Register()"/>
/// <param name="reportKey">Key, i.e. technical name of the report. Should be the same as the one used when registering the report.</param>
/// <param name="message">The message to include in the entry.</param>
public static void Error(this IReportsCoordinator reportCoordinator, string reportKey, string message) {
reportCoordinator.Add(reportKey, ReportEntryType.Error, message);
}
}

View File

@ -0,0 +1,30 @@
namespace Orchard.Reports.Services {
/// <summary>
/// Exposes a simplified interface for creating reports. Reports provide user-accessible log-like functionality.
/// </summary>
/// <remarks>
/// <see cref="Orchard.Reports.Services.IReportsManager"/> can be used too to create reports directly.
/// </remarks>
public interface IReportsCoordinator : IDependency {
/// <summary>
/// Adds a new report entry to a report that was previously registered.
/// </summary>
/// <remarks>
/// Entries can be only added to a report that was previously registered through Register().
/// </remarks>
/// <seealso cref="Register()"/>
/// <param name="reportKey">Key, i.e. technical name of the report. Should be the same as the one used when registering the report.</param>
/// <param name="type">Type of the entry.</param>
/// <param name="message">The message to include in the entry.</param>
void Add(string reportKey, ReportEntryType type, string message);
/// <summary>
/// Registers a new report so entries can be added to it.
/// </summary>
/// <param name="reportKey">Key, i.e. technical name of the report.</param>
/// <param name="activityName">Name of the activity the report is about (e.g. "Upgrade").</param>
/// <param name="title">A title better describing what the report is about (e.g. "Migrating routes of Pages, Blog Posts").</param>
/// <returns>The report's numerical ID.</returns>
int Register(string reportKey, string activityName, string title);
}
}

View File

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace Orchard.Reports.Services {
/// <summary>
/// Service for handling reports. Reports provide user-accessible log-like functionality.
/// </summary>
/// <remarks>
/// You can use <see cref="Orchard.Reports.Services.IReportsCoordinator"/> to create reports through a simplified interface.
/// </remarks>
public interface IReportsManager : ISingletonDependency {
void Add(int reportId, ReportEntryType type, string message);
int CreateReport(string title, string activityName);
Report Get(int reportId);
IEnumerable<Report> GetReports();
void Flush();
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Orchard.Reports.Services {
/// <summary>
/// Defines a service that can be used to persist reports.
/// </summary>
/// <remarks>
/// Implementations of this interface are commonly used from <see cref="Orchard.Reports.Services.IReportsManager"/> implementations.
/// </remarks>
public interface IReportsPersister : IDependency {
IEnumerable<Report> Fetch();
void Save(IEnumerable<Report> reports);
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using Orchard.Logging;
namespace Orchard.Reports.Services {
public class ReportsCoordinator : IReportsCoordinator, IDisposable {
private readonly IReportsManager _reportsManager;
private readonly IDictionary<string, int> _reports;
public ReportsCoordinator(IReportsManager reportsManager) {
_reportsManager = reportsManager;
Logger = NullLogger.Instance;
_reports = new Dictionary<string, int>();
}
public ILogger Logger { get; set; }
public void Dispose() {
_reportsManager.Flush();
}
public void Add(string reportKey, ReportEntryType type, string message) {
if(!_reports.ContainsKey(reportKey)) {
// ignore message if no corresponding report
return;
}
_reportsManager.Add(_reports[reportKey], type, message);
}
public int Register(string reportKey, string activityName, string title) {
int reportId = _reportsManager.CreateReport(title, activityName);
_reports.Add(reportKey, reportId);
return reportId;
}
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Logging;
namespace Orchard.Reports.Services {
public class ReportsManager : IReportsManager {
private readonly IReportsPersister _reportsPersister;
private List<Report> _reports;
private static readonly object _synLock = new object();
private bool _isDirty;
public ReportsManager(IReportsPersister reportsPersister) {
_reportsPersister = reportsPersister;
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public void Add(int reportId, ReportEntryType type, string message) {
lock ( _synLock ) {
LoadReports();
_isDirty = true;
var report = Get(reportId);
if(report == null) {
return;
}
report.Entries.Add(new ReportEntry {Message = message, Type = type, Utc = DateTime.UtcNow});
}
}
public int CreateReport(string title, string activityName) {
lock ( _synLock ) {
LoadReports();
_isDirty = true;
var reportId = _reports.Count == 0 ? 1 : _reports.Max(r => r.ReportId) + 1;
var report = new Report {ActivityName = activityName, ReportId = reportId, Title = title, Utc = DateTime.UtcNow};
_reports.Add(report);
return reportId;
}
}
public Report Get(int reportId) {
lock(_synLock) {
LoadReports();
return _reports.Where(r => r.ReportId == reportId).FirstOrDefault();
}
}
public IEnumerable<Report> GetReports() {
lock ( _synLock ) {
LoadReports();
return _reports.ToList();
}
}
public void Flush() {
if ( _reports == null || !_isDirty) {
return;
}
lock ( _synLock ) {
_reportsPersister.Save(_reports);
_isDirty = false;
}
}
private void LoadReports() {
if(_reports == null) {
_reports = _reportsPersister.Fetch().ToList();
}
}
}
}

View File

@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Xml;
using System.Xml.Linq;
using Orchard.Environment.Configuration;
using Orchard.FileSystems.AppData;
using System.IO;
namespace Orchard.Reports.Services {
public class ReportsPersister : IReportsPersister {
private readonly IAppDataFolder _appDataFolder;
private readonly ShellSettings _shellSettings;
private readonly string _reportsFileName;
private readonly DataContractSerializer _dataContractSerializer;
private readonly object _synLock = new object();
public ReportsPersister(IAppDataFolder appDataFolder, ShellSettings shellSettings) {
_appDataFolder = appDataFolder;
_shellSettings = shellSettings;
_dataContractSerializer = new DataContractSerializer(typeof(Report), new [] { typeof(ReportEntry) });
_reportsFileName = Path.Combine(Path.Combine("Sites", _shellSettings.Name), "reports.dat");
}
public IEnumerable<Report> Fetch() {
lock ( _synLock ) {
if ( !_appDataFolder.FileExists(_reportsFileName) ) {
yield break;
}
var text = _appDataFolder.ReadFile(_reportsFileName);
var xmlDocument = XDocument.Parse(text);
var rootNode = xmlDocument.Root;
if (rootNode == null) {
yield break;
}
foreach (var reportNode in rootNode.Elements()) {
var reader = new StringReader(reportNode.Value);
using (var xmlReader = XmlReader.Create(reader)) {
yield return (Report) _dataContractSerializer.ReadObject(xmlReader, true);
}
}
}
}
public void Save(IEnumerable<Report> reports) {
lock ( _synLock ) {
var xmlDocument = new XDocument();
xmlDocument.Add(new XElement("Reports"));
foreach (var report in reports) {
var reportNode = new XElement("Report");
var writer = new StringWriter();
using (var xmlWriter = XmlWriter.Create(writer)) {
_dataContractSerializer.WriteObject(xmlWriter, report);
}
reportNode.Value = writer.ToString();
xmlDocument.Root.Add(reportNode);
}
var saveWriter = new StringWriter();
xmlDocument.Save(saveWriter);
_appDataFolder.CreateFile(_reportsFileName, saveWriter.ToString());
}
}
}
}