Reports module

- Stores service management reports
- Use IReportsCoordinator to write message to the same report through multiple components
- Views to display reports

--HG--
branch : dev
This commit is contained in:
Sebastien Ros 2010-07-09 17:56:58 -07:00
parent f397d25d66
commit a9dc5abd50
24 changed files with 515 additions and 5 deletions

View File

@ -82,11 +82,16 @@
<Compile Include="Contents\ViewModels\EditItemViewModel.cs" />
<Compile Include="Contents\ViewModels\ListContentsViewModel.cs" />
<Compile Include="Contents\ViewModels\ListContentTypesViewModel.cs" />
<Compile Include="Reports\AdminMenu.cs" />
<Compile Include="Reports\Controllers\AdminController.cs" />
<Compile Include="Reports\Routes.cs" />
<Compile Include="Localization\Controllers\AdminController.cs" />
<Compile Include="Localization\DataMigrations\LocalizationDataMigration.cs" />
<Compile Include="Localization\ViewModels\ContentTranslationsViewModel.cs" />
<Compile Include="Localization\Drivers\LocalizationDriver.cs" />
<Compile Include="Navigation\DataMigrations\NavigationDataMigration.cs" />
<Compile Include="Reports\ViewModels\DisplayReportViewModel.cs" />
<Compile Include="Reports\ViewModels\ReportsAdminIndexViewModel.cs" />
<Compile Include="Routable\Controllers\ItemController.cs" />
<Compile Include="Routable\Drivers\RoutableDriver.cs" />
<Compile Include="Routable\Handlers\RoutableHandler.cs" />
@ -234,6 +239,9 @@
<Content Include="Contents\Views\EditorTemplates\Items\Contents.Item.ascx" />
<Content Include="Contents\Views\Item\Preview.aspx" />
<Content Include="Contents\Views\Item\Display.aspx" />
<Content Include="Reports\Module.txt" />
<Content Include="Reports\Views\Admin\Display.aspx" />
<Content Include="Reports\Views\Admin\Index.aspx" />
<Content Include="Localization\Module.txt" />
<Content Include="Localization\Views\Admin\Translate.ascx" />
<Content Include="Localization\Views\DisplayTemplates\Parts\Localization.ContentTranslations.SummaryAdmin.ascx" />
@ -300,6 +308,7 @@
<Content Include="Contents\Views\Web.config" />
<Content Include="Routable\Views\Web.config" />
<Content Include="Localization\Views\Web.config" />
<Content Include="Reports\Views\Web.config" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />

View File

@ -0,0 +1,15 @@
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.Add(T("Site Configuration"), "0",
menu => menu.Add(T("Reports"), "0", item => item.Action("Index", "Admin", new { area = "Reports" }).Permission(StandardPermissions.AccessAdminPanel)));
}
}
}

View File

@ -0,0 +1,28 @@
using System.Linq;
using System.Web.Mvc;
using Orchard.Core.Reports.ViewModels;
using Orchard.Mvc.ViewModels;
using Orchard.Reports.Services;
namespace Orchard.Core.Reports.Controllers {
public class AdminController : Controller {
private readonly IReportsManager _reportsManager;
public AdminController(IReportsManager reportsManager) {
_reportsManager = reportsManager;
}
public ActionResult Index() {
var model = new ReportsAdminIndexViewModel { Reports = _reportsManager.GetReports().ToList() };
return View(model);
}
public ActionResult Display(int id) {
var model = new DisplayReportViewModel { Report = _reportsManager.Get(id) };
return View(model);
}
}
}

View File

@ -0,0 +1,11 @@
name: Reports
antiforgery: enabled
author: The Orchard Team
website: http://orchardproject.net
version: 0.1
orchardversion: 0.1.2010.0312
description: The dashboard module is providing the reports screen of the application.
features:
Dashboard:
Description: Reports management.
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,9 @@
using System.Collections.Generic;
using Orchard.Mvc.ViewModels;
using Orchard.Reports;
namespace Orchard.Core.Reports.ViewModels {
public class DisplayReportViewModel : BaseViewModel {
public Report Report { get; set; }
}
}

View File

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

View File

@ -0,0 +1,40 @@
<%@ Page Language="C#" Inherits="Orchard.Mvc.ViewPage<DisplayReportViewModel>" %>
<%@ Import Namespace="Orchard.Core.Reports.ViewModels"%>
<h1><%: Html.TitleForPage(T("Display Report").ToString())%></h1>
<% 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,40 @@
<%@ Page Language="C#" Inherits="Orchard.Mvc.ViewPage<ReportsAdminIndexViewModel>" %>
<%@ Import Namespace="Orchard.Core.Reports.ViewModels"%>
<h1><%: Html.TitleForPage(T("Manage Reports").ToString())%></h1>
<% 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(Html.Encode(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

@ -0,0 +1,34 @@
<?xml version="1.0"?>
<configuration>
<system.web>
<httpHandlers>
<add path="*" verb="*"
type="System.Web.HttpNotFoundHandler"/>
</httpHandlers>
<!--
Enabling request validation in view pages would cause validation to occur
after the input has already been processed by the controller. By default
MVC performs request validation before a controller processes the input.
To change this behavior apply the ValidateInputAttribute to a
controller or action.
-->
<pages
validateRequest="false"
pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<controls>
<add assembly="System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.Mvc" tagPrefix="mvc" />
</controls>
</pages>
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<handlers>
<remove name="BlockViewHandler"/>
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler"/>
</handlers>
</system.webServer>
</configuration>

View File

@ -21,6 +21,7 @@ using Orchard.Environment.Descriptor.Models;
using Orchard.Indexing;
using Orchard.Localization;
using Orchard.Localization.Services;
using Orchard.Reports.Services;
using Orchard.Security;
using Orchard.Settings;
using Orchard.Themes;
@ -68,6 +69,7 @@ namespace Orchard.Setup.Services {
"Common",
"Contents",
"Dashboard",
"Reports",
"Feeds",
"HomePage",
"Navigation",
@ -111,6 +113,9 @@ namespace Orchard.Setup.Services {
using (var environment = new StandaloneEnvironment(bootstrapLifetimeScope)) {
var schemaBuilder = new SchemaBuilder(environment.Resolve<IDataMigrationInterpreter>() );
var reportsCoordinator = environment.Resolve<IReportsCoordinator>();
reportsCoordinator.Register("Data Migration", "Setup", "Orchard installation");
schemaBuilder.CreateTable("Orchard_Framework_DataMigrationRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())

View File

@ -8,7 +8,9 @@ using Orchard.Data.Migration.Records;
using Orchard.Data.Migration.Schema;
using Orchard.Environment.Extensions;
using Orchard.Environment.State;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.Reports.Services;
namespace Orchard.Data.Migration {
/// <summary>
@ -19,21 +21,24 @@ namespace Orchard.Data.Migration {
private readonly IRepository<DataMigrationRecord> _dataMigrationRepository;
private readonly IExtensionManager _extensionManager;
private readonly IDataMigrationInterpreter _interpreter;
private readonly IReportsCoordinator _reportsCoordinator;
public DataMigrationManager(
IEnumerable<IDataMigration> dataMigrations,
IRepository<DataMigrationRecord> dataMigrationRepository,
IExtensionManager extensionManager,
IDataMigrationInterpreter interpreter
IDataMigrationInterpreter interpreter,
IReportsCoordinator reportsCoordinator
) {
_dataMigrations = dataMigrations;
_dataMigrationRepository = dataMigrationRepository;
_extensionManager = extensionManager;
_interpreter = interpreter;
_reportsCoordinator = reportsCoordinator;
Logger = NullLogger.Instance;
}
public Localizer T { get; set; }
public ILogger Logger { get; set; }
public IEnumerable<string> GetFeaturesThatNeedUpdate() {
@ -78,7 +83,8 @@ namespace Orchard.Data.Migration {
}
public void Update(string feature){
Logger.Information("Updating {0}", feature);
Logger.Information("Updating feature: {0}", feature);
// proceed with dependent features first, whatever the module it's in
var dependencies = ShellStateCoordinator.OrderByDependencies(_extensionManager.AvailableExtensions()

View File

@ -9,7 +9,9 @@ using NHibernate.SqlTypes;
using Orchard.Data.Migration.Schema;
using Orchard.Data.Providers;
using Orchard.Environment.Configuration;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.Reports.Services;
namespace Orchard.Data.Migration.Interpreters {
public class DefaultDataMigrationInterpreter : AbstractDataMigrationInterpreter, IDataMigrationInterpreter {
@ -20,6 +22,7 @@ namespace Orchard.Data.Migration.Interpreters {
private readonly List<string> _sqlStatements;
private readonly IDataServicesProviderFactory _dataServicesProviderFactory;
private readonly ISessionFactoryHolder _sessionFactoryHolder;
private readonly IReportsCoordinator _reportsCoordinator;
private const char Space = ' ' ;
@ -28,13 +31,15 @@ namespace Orchard.Data.Migration.Interpreters {
ISessionLocator sessionLocator,
IEnumerable<ICommandInterpreter> commandInterpreters,
IDataServicesProviderFactory dataServicesProviderFactory,
ISessionFactoryHolder sessionFactoryHolder) {
ISessionFactoryHolder sessionFactoryHolder,
IReportsCoordinator reportsCoordinator) {
_shellSettings = shellSettings;
_commandInterpreters = commandInterpreters;
_session = sessionLocator.For(typeof(DefaultDataMigrationInterpreter));
_sqlStatements = new List<string>();
_dataServicesProviderFactory = dataServicesProviderFactory;
_sessionFactoryHolder = sessionFactoryHolder;
_reportsCoordinator = reportsCoordinator;
Logger = NullLogger.Instance;
@ -44,6 +49,7 @@ namespace Orchard.Data.Migration.Interpreters {
}
public ILogger Logger { get; set; }
public Localizer T { get; set; }
public IEnumerable<string> SqlStatements {
get { return _sqlStatements; }
@ -318,6 +324,8 @@ namespace Orchard.Data.Migration.Interpreters {
command.CommandText = sqlStatement;
command.ExecuteNonQuery();
}
_reportsCoordinator.Information("Data Migration", String.Format("Executing SQL Query: {0}", sqlStatement));
}
_sqlStatements.Clear();

View File

@ -54,7 +54,7 @@ namespace Orchard.Localization.Services {
.Where(x => x != null)
.OrderByDescending(x => x.Priority);
if (requestCulture.Count() < 1)
if ( requestCulture.Count() < 1 )
return String.Empty;
foreach (var culture in requestCulture) {

View File

@ -408,6 +408,15 @@
<Compile Include="Environment\Extensions\Compilers\IProjectFileParser.cs" />
<Compile Include="Indexing\MetaDataExtensions.cs" />
<Compile Include="Localization\Commands\CultureCommands.cs" />
<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="UI\Admin\Notification\NotificationFilter.cs" />
<Compile Include="Data\Migration\DataMigrationNotificationProvider.cs" />
<Compile Include="UI\Admin\Notification\INotificationManager.cs" />

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using Orchard.Localization;
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,16 @@
using System;
using Orchard.Localization;
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,16 @@
using Orchard.Localization;
using Orchard.Reports;
using Orchard.Reports.Services;
public static class ReportExtentions {
public static void Information(this IReportsCoordinator reportCoordinator, string reportKey, string message) {
reportCoordinator.Add(reportKey, ReportEntryType.Information, message);
}
public static void Warning(this IReportsCoordinator reportCoordinator, string reportKey, string message) {
reportCoordinator.Add(reportKey, ReportEntryType.Warning, message);
}
public static void Error(this IReportsCoordinator reportCoordinator, string reportKey, string message) {
reportCoordinator.Add(reportKey, ReportEntryType.Error, message);
}
}

View File

@ -0,0 +1,8 @@
using Orchard.Localization;
namespace Orchard.Reports.Services {
public interface IReportsCoordinator : IDependency {
void Add(string reportKey, ReportEntryType type, string message);
void Register(string reportKey, string activityName, string title);
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Orchard.Reports.Services {
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,8 @@
using System.Collections.Generic;
namespace Orchard.Reports.Services {
public interface IReportsPersister : IDependency {
IEnumerable<Report> Fetch();
void Save(IEnumerable<Report> reports);
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using Orchard.Localization;
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 void Register(string reportKey, string activityName, string title) {
_reports.Add(reportKey, _reportsManager.CreateReport(title, activityName));
}
}
}

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,69 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Xml;
using System.Xml.Linq;
using Orchard.Environment.Configuration;
using Orchard.Environment.Descriptor.Models;
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());
}
}
}
}