diff --git a/src/Orchard.Core.Tests/Indexing/DefaultIndexProviderTests.cs b/src/Orchard.Core.Tests/Indexing/DefaultIndexProviderTests.cs index 443f8dad6..f92333d91 100644 --- a/src/Orchard.Core.Tests/Indexing/DefaultIndexProviderTests.cs +++ b/src/Orchard.Core.Tests/Indexing/DefaultIndexProviderTests.cs @@ -58,7 +58,11 @@ namespace Orchard.Tests.Indexing { [Test] public void IndexProviderShouldOverwriteAlreadyExistingIndex() { _provider.CreateIndex("default"); - _provider.CreateIndex("default"); + _provider.Store("default", _provider.New(1).Add("body", null)); + Assert.That(_provider.IsEmpty("default"), Is.False); + + _provider.CreateIndex("default"); + Assert.That(_provider.IsEmpty("default"), Is.True); } [Test] @@ -181,5 +185,36 @@ namespace Orchard.Tests.Indexing { Assert.That(searchBuilder.Get(2).Id, Is.EqualTo(2)); Assert.That(searchBuilder.Get(3).Id, Is.EqualTo(3)); } + + [Test] + public void ProviderShouldStoreSettings() { + _provider.CreateIndex("default"); + Assert.That(_provider.GetLastIndexUtc("default"), Is.EqualTo(DefaultIndexProvider.DefaultMinDateTime)); + + _provider.SetLastIndexUtc("default", new DateTime(2010, 1, 1, 1, 1, 1, 1)); + Assert.That(_provider.GetLastIndexUtc("default"), Is.EqualTo(new DateTime(2010, 1, 1, 1, 1, 1, 0))); + + _provider.SetLastIndexUtc("default", new DateTime(1901, 1, 1, 1, 1, 1, 1)); + Assert.That(_provider.GetLastIndexUtc("default"), Is.EqualTo(DefaultIndexProvider.DefaultMinDateTime)); + } + + [Test] + public void IsEmptyShouldBeTrueForNoneExistingIndexes() { + _provider.IsEmpty("dummy"); + Assert.That(_provider.IsEmpty("default"), Is.True); + } + + [Test] + public void IsEmptyShouldBeTrueForJustNewIndexes() { + _provider.CreateIndex("default"); + Assert.That(_provider.IsEmpty("default"), Is.True); + } + + [Test] + public void IsEmptyShouldBeFalseWhenThereIsADocument() { + _provider.CreateIndex("default"); + _provider.Store("default", _provider.New(1).Add("body", null)); + Assert.That(_provider.IsEmpty("default"), Is.False); + } } } diff --git a/src/Orchard.Core.Tests/Indexing/DefaultSearchBuilderTests.cs b/src/Orchard.Core.Tests/Indexing/DefaultSearchBuilderTests.cs index 9ffa62d6c..b28036c57 100644 --- a/src/Orchard.Core.Tests/Indexing/DefaultSearchBuilderTests.cs +++ b/src/Orchard.Core.Tests/Indexing/DefaultSearchBuilderTests.cs @@ -177,5 +177,19 @@ namespace Orchard.Tests.Indexing { Assert.That(date[0].GetDateTime("date") < date[1].GetDateTime("date"), Is.True); Assert.That(date[1].GetDateTime("date") < date[2].GetDateTime("date"), Is.True); } + + [Test] + public void ShouldEscapeSpecialChars() { + _provider.CreateIndex("default"); + _provider.Store("default", _provider.New(1).Add("body", "Orchard has been developped in C#")); + _provider.Store("default", _provider.New(2).Add("body", "Windows has been developped in C++")); + + var cs = _searchBuilder.WithField("body", "C#").Search().ToList(); + Assert.That(cs.Count(), Is.EqualTo(2)); + + var cpp = _searchBuilder.WithField("body", "C++").Search().ToList(); + Assert.That(cpp.Count(), Is.EqualTo(2)); + + } } } diff --git a/src/Orchard.Specs/Modules.feature b/src/Orchard.Specs/Modules.feature index 31482a274..2509b5ddd 100644 --- a/src/Orchard.Specs/Modules.feature +++ b/src/Orchard.Specs/Modules.feature @@ -7,15 +7,9 @@ Scenario: Installed modules are listed Given I have installed Orchard When I go to "admin/modules" Then I should see "

Installed Modules

" - And I should see "

Themes

" + And I should see "

Themes" And the status should be 200 OK -Scenario: Edit module shows its features - Given I have installed Orchard - When I go to "admin/modules/Edit/Orchard.Themes" - Then I should see "

Edit Module: Themes

" - And the status should be 200 OK - Scenario: Features of installed modules are listed Given I have installed Orchard When I go to "admin/modules/features" diff --git a/src/Orchard.Specs/MultiTenancy.feature b/src/Orchard.Specs/MultiTenancy.feature index c8602abb4..b6cabfd97 100644 --- a/src/Orchard.Specs/MultiTenancy.feature +++ b/src/Orchard.Specs/MultiTenancy.feature @@ -7,7 +7,7 @@ Scenario: Default site is listed Given I have installed Orchard And I have installed "Orchard.MultiTenancy" When I go to "Admin/MultiTenancy" - Then I should see "List of Site's Tenants" + Then I should see "List of Site's Tenants" And I should see "Default" And the status should be 200 OK diff --git a/src/Orchard.Web/Core/Common/Views/DisplayTemplates/Parts/Common.Body.ManageWrapperPost.ascx b/src/Orchard.Web/Core/Common/Views/DisplayTemplates/Parts/Common.Body.ManageWrapperPost.ascx index 18093dde3..86cbb66fa 100644 --- a/src/Orchard.Web/Core/Common/Views/DisplayTemplates/Parts/Common.Body.ManageWrapperPost.ascx +++ b/src/Orchard.Web/Core/Common/Views/DisplayTemplates/Parts/Common.Body.ManageWrapperPost.ascx @@ -1,2 +1,7 @@ <%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> <%@ Import Namespace="Orchard.Core.Common.ViewModels"%> +<%-- begin: knowingly broken HTML (hence the ManageWrapperPre and ManageWrapperPost templates) +we need "wrapper templates" (among other functionality) in the future of UI composition +please do not delete or the front end will be broken when the user is authenticated. --%> + +<%-- begin: knowingly broken HTML --%> \ No newline at end of file diff --git a/src/Orchard.Web/Core/Indexing/Commands/IndexingCommands.cs b/src/Orchard.Web/Core/Indexing/Commands/IndexingCommands.cs new file mode 100644 index 000000000..2cb83c3e6 --- /dev/null +++ b/src/Orchard.Web/Core/Indexing/Commands/IndexingCommands.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Orchard.Commands; +using Orchard.ContentManagement; +using Orchard.Indexing; +using Orchard.Security; +using Orchard.Tasks.Indexing; + +namespace Orchard.Core.Indexing.Commands { + public class IndexingCommands : DefaultOrchardCommandHandler { + private readonly IEnumerable _indexNotifierHandlers; + private readonly IIndexManager _indexManager; + private readonly IIndexingTaskManager _indexingTaskManager; + private readonly IContentManager _contentManager; + private const string SearchIndexName = "Search"; + + public IndexingCommands( + IEnumerable indexNotifierHandlers, + IIndexManager indexManager, + IIndexingTaskManager indexingTaskManager, + IContentManager contentManager) { + _indexNotifierHandlers = indexNotifierHandlers; + _indexingTaskManager = indexingTaskManager; + _contentManager = contentManager; + _indexManager = indexManager; + } + + [OrchardSwitch] + public string IndexName { get; set; } + + [OrchardSwitch] + public string Query { get; set; } + + [OrchardSwitch] + public string ContentItemId { get; set; } + + [CommandName("index update")] + [CommandHelp("index update [/IndexName:]\r\n\t" + "Updates the index with the specified , or the search index if not specified")] + [OrchardSwitches("IndexName")] + public string Update() { + if ( !_indexManager.HasIndexProvider() ) { + return "No index available"; + } + + var indexName = String.IsNullOrWhiteSpace(IndexName) ? SearchIndexName : IndexName; + foreach ( var handler in _indexNotifierHandlers ) { + handler.UpdateIndex(indexName); + } + + return "Index is now being updated..."; + } + + [CommandName("index rebuild")] + [CommandHelp("index rebuild [/IndexName:]\r\n\t" + "Rebuilds the index with the specified , or the search index if not specified")] + [OrchardSwitches("IndexName")] + public string Rebuild() { + if ( !_indexManager.HasIndexProvider() ) { + return "No index available"; + } + + var indexName = String.IsNullOrWhiteSpace(IndexName) ? SearchIndexName : IndexName; + var searchProvider = _indexManager.GetSearchIndexProvider(); + if ( searchProvider.Exists(indexName) ) + searchProvider.DeleteIndex(indexName); + + searchProvider.CreateIndex(indexName); + return "Index is now being rebuilt..."; + } + + [CommandName("index search")] + [CommandHelp("index search /Query: [/IndexName:]\r\n\t" + "Searches the specified terms in the index with the specified , or in the search index if not specified")] + [OrchardSwitches("Query,IndexName")] + public string Search() { + if ( !_indexManager.HasIndexProvider() ) { + return "No index available"; + } + var indexName = String.IsNullOrWhiteSpace(IndexName) ? SearchIndexName : IndexName; + var searchBuilder = _indexManager.GetSearchIndexProvider().CreateSearchBuilder(indexName); + var results = searchBuilder.WithField("body", Query).WithField("title", Query).Search(); + + Context.Output.WriteLine("{0} result{1}\r\n-----------------\r\n", results.Count(), results.Count() > 0 ? "s" : ""); + + Context.Output.WriteLine("┌──────────────────────────────────────────────────────────────┬────────┐"); + Context.Output.WriteLine("│ {0} │ {1,6} │", "Title" + new string(' ', 60 - "Title".Length), "Score"); + Context.Output.WriteLine("├──────────────────────────────────────────────────────────────┼────────┤"); + foreach ( var searchHit in results ) { + var title = searchHit.GetString("title"); + title = title.Substring(0, Math.Min(60, title.Length)) ?? "- no title -"; + var score = searchHit.Score; + Context.Output.WriteLine("│ {0} │ {1,6} │", title + new string(' ', 60 - title.Length), score); + } + Context.Output.WriteLine("└──────────────────────────────────────────────────────────────┴────────┘"); + + Context.Output.WriteLine(); + return "End of search results"; + } + + [CommandName("index stats")] + [CommandHelp("index stats [/IndexName:]\r\n\t" + "Displays some statistics about the index with the specified , or in the search index if not specified")] + [OrchardSwitches("IndexName")] + public string Stats() { + if ( !_indexManager.HasIndexProvider() ) { + return "No index available"; + } + var indexName = String.IsNullOrWhiteSpace(IndexName) ? SearchIndexName : IndexName; + Context.Output.WriteLine("Number of indexed documents: {0}", _indexManager.GetSearchIndexProvider().NumDocs(indexName)); + return ""; + } + + [CommandName("index refresh")] + [CommandHelp("index refresh /ContenItem: \r\n\t" + "Refreshes the index for the specifed ")] + [OrchardSwitches("ContentItem")] + public string Refresh() { + int contenItemId; + if ( !int.TryParse(ContentItemId, out contenItemId) ) { + return "Invalid content item id. Not an integer."; + } + + var contentItem = _contentManager.Get(contenItemId); + _indexingTaskManager.CreateUpdateIndexTask(contentItem); + + return "Content Item marked for reindexing"; + } + + [CommandName("index delete")] + [CommandHelp("index delete /ContenItem:\r\n\t" + "Deletes the specifed fromthe index")] + [OrchardSwitches("ContentItem")] + public string Delete() { + int contenItemId; + if(!int.TryParse(ContentItemId, out contenItemId)) { + return "Invalid content item id. Not an integer."; + } + + var contentItem = _contentManager.Get(contenItemId); + _indexingTaskManager.CreateDeleteIndexTask(contentItem); + + return "Content Item marked for deletion"; + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Indexing/Lucene/DefaultIndexProvider.cs b/src/Orchard.Web/Core/Indexing/Lucene/DefaultIndexProvider.cs index 9a5e01174..8132e1a54 100644 --- a/src/Orchard.Web/Core/Indexing/Lucene/DefaultIndexProvider.cs +++ b/src/Orchard.Web/Core/Indexing/Lucene/DefaultIndexProvider.cs @@ -13,6 +13,7 @@ using Orchard.Indexing; using Directory = Lucene.Net.Store.Directory; using Version = Lucene.Net.Util.Version; using Orchard.Logging; +using System.Xml.Linq; namespace Orchard.Core.Indexing.Lucene { /// @@ -22,14 +23,18 @@ namespace Orchard.Core.Indexing.Lucene { private readonly IAppDataFolder _appDataFolder; private readonly ShellSettings _shellSettings; public static readonly Version LuceneVersion = Version.LUCENE_29; - private readonly Analyzer _analyzer = new StandardAnalyzer(LuceneVersion); + private readonly Analyzer _analyzer ; private readonly string _basePath; + public static readonly DateTime DefaultMinDateTime = new DateTime(1980, 1, 1); + public static readonly string Settings = "Settings"; + public static readonly string LastIndexUtc = "LastIndexedUtc"; public ILogger Logger { get; set; } public DefaultIndexProvider(IAppDataFolder appDataFolder, ShellSettings shellSettings) { _appDataFolder = appDataFolder; _shellSettings = shellSettings; + _analyzer = CreateAnalyzer(); // TODO: (sebros) Find a common way to get where tenant's specific files should go. "Sites/Tenant" is hard coded in multiple places _basePath = Path.Combine("Sites", _shellSettings.Name, "Indexes"); @@ -37,6 +42,15 @@ namespace Orchard.Core.Indexing.Lucene { Logger = NullLogger.Instance; // Ensures the directory exists + EnsureDirectoryExists(); + } + + public static Analyzer CreateAnalyzer() { + // StandardAnalyzer does lower-case and stop-word filtering. It also removes punctuation + return new StandardAnalyzer(LuceneVersion); + } + + private void EnsureDirectoryExists() { var directory = new DirectoryInfo(_appDataFolder.MapPath(_basePath)); if(!directory.Exists) { directory.Create(); @@ -62,6 +76,36 @@ namespace Orchard.Core.Indexing.Lucene { return new DirectoryInfo(_appDataFolder.MapPath(Path.Combine(_basePath, indexName))).Exists; } + public bool IsEmpty(string indexName) { + if ( !Exists(indexName) ) { + return true; + } + + var reader = IndexReader.Open(GetDirectory(indexName), true); + + try { + return reader.NumDocs() == 0; + } + finally { + reader.Close(); + } + } + + public int NumDocs(string indexName) { + if ( !Exists(indexName) ) { + return 0; + } + + var reader = IndexReader.Open(GetDirectory(indexName), true); + + try { + return reader.NumDocs(); + } + finally { + reader.Close(); + } + } + public void CreateIndex(string indexName) { var writer = new IndexWriter(GetDirectory(indexName), _analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED); writer.Close(); @@ -72,6 +116,11 @@ namespace Orchard.Core.Indexing.Lucene { public void DeleteIndex(string indexName) { new DirectoryInfo(Path.Combine(_appDataFolder.MapPath(Path.Combine(_basePath, indexName)))) .Delete(true); + + var settingsFileName = GetSettingsFileName(indexName); + if(File.Exists(settingsFileName)) { + File.Delete(settingsFileName); + } } public void Store(string indexName, IIndexDocument indexDocument) { @@ -105,7 +154,6 @@ namespace Orchard.Core.Indexing.Lucene { writer.Optimize(); writer.Close(); } - } public void Delete(string indexName, int documentId) { @@ -148,5 +196,38 @@ namespace Orchard.Core.Indexing.Lucene { return new DefaultSearchBuilder(GetDirectory(indexName)); } + private string GetSettingsFileName(string indexName) { + return Path.Combine(_appDataFolder.MapPath(_basePath), indexName + ".settings.xml"); + } + + public DateTime GetLastIndexUtc(string indexName) { + var settingsFileName = GetSettingsFileName(indexName); + + return File.Exists(settingsFileName) + ? DateTime.Parse(XDocument.Load(settingsFileName).Descendants(LastIndexUtc).First().Value) + : DefaultMinDateTime; + } + + public void SetLastIndexUtc(string indexName, DateTime lastIndexUtc) { + if ( lastIndexUtc < DefaultMinDateTime ) { + lastIndexUtc = DefaultMinDateTime; + } + + XDocument doc; + var settingsFileName = GetSettingsFileName(indexName); + if ( !File.Exists(settingsFileName) ) { + EnsureDirectoryExists(); + doc = new XDocument( + new XElement(Settings, + new XElement(LastIndexUtc, lastIndexUtc.ToString("s")))); + } + else { + doc = XDocument.Load(settingsFileName); + doc.Element(Settings).Element(LastIndexUtc).Value = lastIndexUtc.ToString("s"); + } + + doc.Save(settingsFileName); + } + } } diff --git a/src/Orchard.Web/Core/Indexing/Lucene/DefaultSearchBuilder.cs b/src/Orchard.Web/Core/Indexing/Lucene/DefaultSearchBuilder.cs index c73192c52..93a084672 100644 --- a/src/Orchard.Web/Core/Indexing/Lucene/DefaultSearchBuilder.cs +++ b/src/Orchard.Web/Core/Indexing/Lucene/DefaultSearchBuilder.cs @@ -2,12 +2,15 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Tokenattributes; using Lucene.Net.Index; using Lucene.Net.Search; using Lucene.Net.Store; using Orchard.Logging; using Lucene.Net.Documents; using Orchard.Indexing; +using Lucene.Net.QueryParsers; namespace Orchard.Core.Indexing.Lucene { public class DefaultSearchBuilder : ISearchBuilder { @@ -23,6 +26,9 @@ namespace Orchard.Core.Indexing.Lucene { private readonly Dictionary _after; private string _sort; private bool _sortDescending; + private string _parse; + private readonly Analyzer _analyzer; + private string _defaultField; public ILogger Logger { get; set; } @@ -37,9 +43,21 @@ namespace Orchard.Core.Indexing.Lucene { _fields = new Dictionary(); _sort = String.Empty; _sortDescending = true; + _parse = String.Empty; + _analyzer = DefaultIndexProvider.CreateAnalyzer(); } - public ISearchBuilder Parse(string query) { + public ISearchBuilder Parse(string defaultField, string query) { + if ( String.IsNullOrWhiteSpace(defaultField) ) { + throw new ArgumentException("Default field can't be empty"); + } + + if ( String.IsNullOrWhiteSpace(query) ) { + throw new ArgumentException("Query can't be empty"); + } + + _defaultField = defaultField; + _parse = query; return this; } @@ -49,8 +67,17 @@ namespace Orchard.Core.Indexing.Lucene { public ISearchBuilder WithField(string field, string value, bool wildcardSearch) { - _fields[field] = value.Split(' ') + var tokens = new List(); + using(var sr = new System.IO.StringReader(value)) { + var stream = _analyzer.TokenStream(field, sr); + while(stream.IncrementToken()) { + tokens.Add(((TermAttribute)stream.GetAttribute(typeof(TermAttribute))).Term()); + } + } + + _fields[field] = tokens .Where(k => !String.IsNullOrWhiteSpace(k)) + .Select(QueryParser.Escape) .Select(k => wildcardSearch ? (Query)new PrefixQuery(new Term(field, k)) : new TermQuery(new Term(k))) .ToArray(); @@ -93,6 +120,10 @@ namespace Orchard.Core.Indexing.Lucene { } private Query CreateQuery() { + if(!String.IsNullOrWhiteSpace(_parse)) { + return new QueryParser(DefaultIndexProvider.LuceneVersion, _defaultField, DefaultIndexProvider.CreateAnalyzer()).Parse(_parse); + } + var query = new BooleanQuery(); if ( _fields.Keys.Count > 0 ) { // apply specific filters if defined diff --git a/src/Orchard.Web/Core/Indexing/Models/IndexingSettingsRecord.cs b/src/Orchard.Web/Core/Indexing/Models/IndexingSettingsRecord.cs deleted file mode 100644 index 28cd06b9d..000000000 --- a/src/Orchard.Web/Core/Indexing/Models/IndexingSettingsRecord.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Orchard.Core.Indexing.Models { - public class IndexingSettingsRecord { - public virtual int Id { get; set; } - public virtual DateTime? LatestIndexingUtc { get; set; } - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Indexing/Services/IndexingTaskExecutor.cs b/src/Orchard.Web/Core/Indexing/Services/IndexingTaskExecutor.cs index 1b4d53035..baa9b1b8d 100644 --- a/src/Orchard.Web/Core/Indexing/Services/IndexingTaskExecutor.cs +++ b/src/Orchard.Web/Core/Indexing/Services/IndexingTaskExecutor.cs @@ -10,134 +10,165 @@ using Orchard.Logging; using Orchard.Services; using Orchard.Tasks; using Orchard.Core.Indexing.Models; +using Orchard.Tasks.Indexing; +using Orchard.Indexing; namespace Orchard.Core.Indexing.Services { /// /// Contains the logic which is regularly executed to retrieve index information from multiple content handlers. /// [UsedImplicitly] - public class IndexingTaskExecutor : IBackgroundTask { + public class IndexingTaskExecutor : IBackgroundTask, IIndexNotifierHandler { private readonly IClock _clock; private readonly IRepository _repository; - private readonly IRepository _settings; private readonly IEnumerable _handlers; private IIndexProvider _indexProvider; private readonly IIndexManager _indexManager; + private readonly IIndexingTaskManager _indexingTaskManager; private readonly IContentManager _contentManager; private const string SearchIndexName = "Search"; + + private readonly object _synLock = new object(); public IndexingTaskExecutor( IClock clock, IRepository repository, - IRepository settings, IEnumerable handlers, IIndexManager indexManager, + IIndexingTaskManager indexingTaskManager, IContentManager contentManager) { _clock = clock; _repository = repository; - _settings = settings; _indexManager = indexManager; _handlers = handlers; + _indexingTaskManager = indexingTaskManager; _contentManager = contentManager; Logger = NullLogger.Instance; } public ILogger Logger { get; set; } + public void UpdateIndex(string indexName) { + if (indexName == SearchIndexName) { + Sweep(); + } + } + public void Sweep() { - if ( !_indexManager.HasIndexProvider() ) { + if ( !System.Threading.Monitor.TryEnter(_synLock) ) { + Logger.Information("Index was requested but was already running"); return; } - _indexProvider = _indexManager.GetSearchIndexProvider(); - - // retrieve last processed index time - var settingsRecord = _settings.Table.FirstOrDefault(); - - if ( settingsRecord == null ) { - _settings.Create(settingsRecord = new IndexingSettingsRecord { LatestIndexingUtc = new DateTime(1980, 1, 1) }); - } - - var lastIndexing = settingsRecord.LatestIndexingUtc; - settingsRecord.LatestIndexingUtc = _clock.UtcNow; - - // retrieved not yet processed tasks - var taskRecords = _repository.Fetch(x => x.CreatedUtc >= lastIndexing) - .ToArray(); - - if ( taskRecords.Length == 0 ) - return; - - Logger.Information("Processing {0} indexing tasks", taskRecords.Length); - - - if ( !_indexProvider.Exists(SearchIndexName) ) { - _indexProvider.CreateIndex(SearchIndexName); - } - - var updateIndexDocuments = new List(); - var deleteIndexDocuments = new List(); - - // process Delete tasks - foreach ( var taskRecord in taskRecords.Where(t => t.Action == IndexingTaskRecord.Delete) ) { - var task = new IndexingTask(_contentManager, taskRecord); - deleteIndexDocuments.Add(taskRecord.ContentItemRecord.Id); - - try { - _repository.Delete(taskRecord); - } - catch ( Exception ex ) { - Logger.Error(ex, "Could not delete task #{0}", taskRecord.Id); - } - } - - try { - if ( deleteIndexDocuments.Count > 0 ) { - _indexProvider.Delete(SearchIndexName, deleteIndexDocuments); + + if (!_indexManager.HasIndexProvider()) { + return; } - } - catch ( Exception ex ) { - Logger.Warning(ex, "An error occured while remove a document from the index"); - } - // process Update tasks - foreach ( var taskRecord in taskRecords.Where(t => t.Action == IndexingTaskRecord.Update) ) { - var task = new IndexingTask(_contentManager, taskRecord); + _indexProvider = _indexManager.GetSearchIndexProvider(); + var updateIndexDocuments = new List(); + var lastIndexing = DateTime.UtcNow; - try { - var context = new IndexContentContext { - ContentItem = task.ContentItem, - IndexDocument = _indexProvider.New(task.ContentItem.Id) - }; + // Do we need to rebuild the full index (first time module is used, or rebuild index requested) ? + if (_indexProvider.IsEmpty(SearchIndexName)) { + Logger.Information("Rebuild index started"); - // dispatch to handlers to retrieve index information - foreach ( var handler in _handlers ) { - handler.Indexing(context); + // mark current last task, as we should process older ones (in case of rebuild index only) + lastIndexing = _indexingTaskManager.GetLastTaskDateTime(); + + // get every existing content item to index it + foreach (var contentItem in _contentManager.Query(VersionOptions.Published).List()) { + try { + var context = new IndexContentContext { + ContentItem = contentItem, + IndexDocument = _indexProvider.New(contentItem.Id) + }; + + // dispatch to handlers to retrieve index information + foreach (var handler in _handlers) { + handler.Indexing(context); + } + + updateIndexDocuments.Add(context.IndexDocument); + + foreach (var handler in _handlers) { + handler.Indexed(context); + } + } + catch (Exception ex) { + Logger.Warning(ex, "Unable to index content item #{0} during rebuild", contentItem.Id); + } } - updateIndexDocuments.Add(context.IndexDocument); + } + else { + // retrieve last processed index time + lastIndexing = _indexProvider.GetLastIndexUtc(SearchIndexName); + } - foreach ( var handler in _handlers ) { - handler.Indexed(context); + _indexProvider.SetLastIndexUtc(SearchIndexName, _clock.UtcNow); + + // retrieve not yet processed tasks + var taskRecords = _repository.Fetch(x => x.CreatedUtc >= lastIndexing) + .ToArray(); + + if (taskRecords.Length == 0) + return; + + Logger.Information("Processing {0} indexing tasks", taskRecords.Length); + + if (!_indexProvider.Exists(SearchIndexName)) { + _indexProvider.CreateIndex(SearchIndexName); + } + + // process Delete tasks + try { + _indexProvider.Delete(SearchIndexName, taskRecords.Where(t => t.Action == IndexingTaskRecord.Delete).Select(t => t.Id)); + } + catch (Exception ex) { + Logger.Warning(ex, "An error occured while removing a document from the index"); + } + + // process Update tasks + foreach (var taskRecord in taskRecords.Where(t => t.Action == IndexingTaskRecord.Update)) { + var task = new IndexingTask(_contentManager, taskRecord); + + try { + var context = new IndexContentContext { + ContentItem = task.ContentItem, + IndexDocument = _indexProvider.New(task.ContentItem.Id) + }; + + // dispatch to handlers to retrieve index information + foreach (var handler in _handlers) { + handler.Indexing(context); + } + + updateIndexDocuments.Add(context.IndexDocument); + + foreach (var handler in _handlers) { + handler.Indexed(context); + } + } + catch (Exception ex) { + Logger.Warning(ex, "Unable to process indexing task #{0}", taskRecord.Id); } } - catch ( Exception ex ) { - Logger.Warning(ex, "Unable to process indexing task #{0}", taskRecord.Id); + + if (updateIndexDocuments.Count > 0) { + try { + _indexProvider.Store(SearchIndexName, updateIndexDocuments); + } + catch (Exception ex) { + Logger.Warning(ex, "An error occured while adding a document to the index"); + } } } - - try { - if ( updateIndexDocuments.Count > 0 ) { - _indexProvider.Store(SearchIndexName, updateIndexDocuments); - } + finally { + System.Threading.Monitor.Exit(_synLock); } - catch ( Exception ex ) { - Logger.Warning(ex, "An error occured while adding a document to the index"); - } - - _settings.Update(settingsRecord); } } } diff --git a/src/Orchard.Web/Core/Indexing/Services/IndexingTaskManager.cs b/src/Orchard.Web/Core/Indexing/Services/IndexingTaskManager.cs index 1dde1d257..9969920c3 100644 --- a/src/Orchard.Web/Core/Indexing/Services/IndexingTaskManager.cs +++ b/src/Orchard.Web/Core/Indexing/Services/IndexingTaskManager.cs @@ -16,18 +16,15 @@ namespace Orchard.Core.Indexing.Services { public class IndexingTaskManager : IIndexingTaskManager { private readonly IContentManager _contentManager; private readonly IRepository _repository; - private readonly IRepository _settings; private readonly IClock _clock; public IndexingTaskManager( IContentManager contentManager, IRepository repository, - IRepository settings, IClock clock) { _clock = clock; _repository = repository; _contentManager = contentManager; - _settings = settings; Logger = NullLogger.Instance; } @@ -38,14 +35,7 @@ namespace Orchard.Core.Indexing.Services { throw new ArgumentNullException("contentItem"); } - // remove previous tasks for the same content item - var tasks = _repository - .Fetch(x => x.ContentItemRecord.Id == contentItem.Id) - .ToArray(); - - foreach ( var task in tasks ) { - _repository.Delete(task); - } + DeleteTasks(contentItem); var taskRecord = new IndexingTaskRecord { CreatedUtc = _clock.UtcNow, @@ -69,45 +59,20 @@ namespace Orchard.Core.Indexing.Services { Logger.Information("Deleting index task created for [{0}:{1}]", contentItem.ContentType, contentItem.Id); } - public IEnumerable GetTasks(DateTime? createdAfter) { - return _repository - .Fetch(x => x.CreatedUtc > createdAfter) - .Select(x => new IndexingTask(_contentManager, x)) - .Cast() - .ToReadOnlyCollection(); - } - - public void DeleteTasks(DateTime? createdBefore) { - Logger.Debug("Deleting Indexing tasks created before {0}", createdBefore); - - var tasks = _repository - .Fetch(x => x.CreatedUtc <= createdBefore); - - foreach (var task in tasks) { - _repository.Delete(task); - } + public DateTime GetLastTaskDateTime() { + return _repository.Table.Max(t => t.CreatedUtc) ?? DateTime.MinValue; } + /// + /// Removes existing tasks for the specified content item + /// public void DeleteTasks(ContentItem contentItem) { - Logger.Debug("Deleting Indexing tasks for ContentItem [{0}:{1}]", contentItem.ContentType, contentItem.Id); - var tasks = _repository - .Fetch(x => x.Id == contentItem.Id); - + .Fetch(x => x.ContentItemRecord.Id == contentItem.Id) + .ToArray(); foreach (var task in tasks) { _repository.Delete(task); } } - - public void RebuildIndex() { - var settingsRecord = _settings.Table.FirstOrDefault(); - if (settingsRecord == null) { - _settings.Create(settingsRecord = new IndexingSettingsRecord() ); - } - - settingsRecord.LatestIndexingUtc = new DateTime(1980, 1, 1); - _settings.Update(settingsRecord); - } - } } diff --git a/src/Orchard.Web/Core/Localization/Models/Localized.cs b/src/Orchard.Web/Core/Localization/Models/Localized.cs new file mode 100644 index 000000000..897b5bc99 --- /dev/null +++ b/src/Orchard.Web/Core/Localization/Models/Localized.cs @@ -0,0 +1,20 @@ +using System.Web.Mvc; +using Orchard.ContentManagement; + +namespace Orchard.Core.Localization.Models { + public sealed class Localized : ContentPart { + [HiddenInput(DisplayValue = false)] + public int Id { get { return ContentItem.Id; } } + + public int CultureId { + get { return Record.CultureId; } + set { Record.CultureId = value; } + } + + public int MasterContentItemId { + get { return Record.MasterContentItemId; } + set { Record.MasterContentItemId = value; } + } + + } +} diff --git a/src/Orchard.Web/Core/Localization/Models/LocalizedRecord.cs b/src/Orchard.Web/Core/Localization/Models/LocalizedRecord.cs new file mode 100644 index 000000000..cab717d5f --- /dev/null +++ b/src/Orchard.Web/Core/Localization/Models/LocalizedRecord.cs @@ -0,0 +1,8 @@ +using Orchard.ContentManagement.Records; + +namespace Orchard.Core.Localization.Models { + public class LocalizedRecord : ContentPartRecord { + public virtual int CultureId { get; set; } + public virtual int MasterContentItemId { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Localization/Module.txt b/src/Orchard.Web/Core/Localization/Module.txt new file mode 100644 index 000000000..6cb082dda --- /dev/null +++ b/src/Orchard.Web/Core/Localization/Module.txt @@ -0,0 +1,11 @@ +name: Localization +antiforgery: enabled +author: The Orchard Team +website: http://orchardproject.net +version: 0.1 +orchardversion: 0.1.2010.0312 +description: Support for localizing content items for cultures. +features: + Localization: + Description: Localize content items. + Category: Core \ No newline at end of file diff --git a/src/Orchard.Web/Core/Localization/Services/ContentItemLocalizationService.cs b/src/Orchard.Web/Core/Localization/Services/ContentItemLocalizationService.cs new file mode 100644 index 000000000..e89f0820e --- /dev/null +++ b/src/Orchard.Web/Core/Localization/Services/ContentItemLocalizationService.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Orchard.ContentManagement; +using Orchard.Core.Localization.Models; +using Orchard.Localization.Services; +using Localized = Orchard.Core.Localization.Models.Localized; + +namespace Orchard.Core.Localization.Services { + [UsedImplicitly] + public class ContentItemLocalizationService : IContentItemLocalizationService { + private readonly IContentManager _contentManager; + private readonly ICultureManager _cultureManager; + + public ContentItemLocalizationService(IContentManager contentManager, ICultureManager cultureManager) { + _contentManager = contentManager; + _cultureManager = cultureManager; + } + + public IEnumerable Get() { + return _contentManager.Query().List(); + } + + public Localized Get(int localizedId) { + return _contentManager.Get(localizedId); + } + + public Localized GetLocalizationForCulture(int masterId, string cultureName) { + var cultures = _cultureManager.ListCultures(); + if (cultures.Contains(cultureName)) { + int cultureId = _cultureManager.GetCultureIdByName(cultureName); + if (cultureId != 0) { + return _contentManager.Query() + .Where(x => x.MasterContentItemId == masterId && x.CultureId == cultureId).List().FirstOrDefault(); + } + } + return null; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Localization/Services/IContentItemLocalizationService.cs b/src/Orchard.Web/Core/Localization/Services/IContentItemLocalizationService.cs new file mode 100644 index 000000000..87a5eb993 --- /dev/null +++ b/src/Orchard.Web/Core/Localization/Services/IContentItemLocalizationService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Orchard.Core.Localization.Models; + +namespace Orchard.Core.Localization.Services { + public interface IContentItemLocalizationService : IDependency { + IEnumerable Get(); + Localized Get(int localizedId); + Localized GetLocalizationForCulture(int masterId, string cultureName); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Orchard.Core.csproj b/src/Orchard.Web/Core/Orchard.Core.csproj index 79cf03728..d593473a2 100644 --- a/src/Orchard.Web/Core/Orchard.Core.csproj +++ b/src/Orchard.Web/Core/Orchard.Core.csproj @@ -115,16 +115,20 @@ + - + + + + @@ -193,6 +197,7 @@ + diff --git a/src/Orchard.Web/Modules/Orchard.Search/AdminMenu.cs b/src/Orchard.Web/Modules/Orchard.Search/AdminMenu.cs new file mode 100644 index 000000000..369fcea1c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Search/AdminMenu.cs @@ -0,0 +1,16 @@ +using Orchard.Localization; +using Orchard.UI.Navigation; + +namespace Orchard.Search { + public class AdminMenu : INavigationProvider { + public Localizer T { get; set; } + public string MenuName { get { return "admin"; } } + + public void GetNavigation(NavigationBuilder builder) { + builder.Add(T("Site"), "11", + menu => menu + .Add(T("Search Index"), "10.0", item => item.Action("Index", "Admin", new {area = "Orchard.Search"}) + .Permission(Permissions.ManageSearchIndex))); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Search/Controllers/AdminController.cs new file mode 100644 index 000000000..217e1db53 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Search/Controllers/AdminController.cs @@ -0,0 +1,50 @@ +using System; +using System.Web.Mvc; +using Orchard.Localization; +using Orchard.Search.Services; +using Orchard.Search.ViewModels; +using Orchard.UI.Notify; + +namespace Orchard.Search.Controllers { + public class AdminController : Controller { + private readonly ISearchService _searchService; + + public AdminController(ISearchService searchService, IOrchardServices services) { + _searchService = searchService; + Services = services; + T = NullLocalizer.Instance; + } + + public IOrchardServices Services { get; private set; } + public Localizer T { get; set; } + + public ActionResult Index() { + var viewModel = new SearchIndexViewModel {HasIndexToManage = _searchService.HasIndexToManage, IndexUpdatedUtc = _searchService.GetIndexUpdatedUtc()}; + + if (!viewModel.HasIndexToManage) + Services.Notifier.Information(T("There is not search index to manage for this site.")); + + return View(viewModel); + } + + [HttpPost] + public ActionResult Update() { + if (!Services.Authorizer.Authorize(Permissions.ManageSearchIndex, T("Not allowed to manage the search index."))) + return new HttpUnauthorizedResult(); + + _searchService.UpdateIndex(); + + return RedirectToAction("Index"); + } + + [HttpPost] + public ActionResult Rebuild() { + if (!Services.Authorizer.Authorize(Permissions.ManageSearchIndex, T("Not allowed to manage the search index."))) + return new HttpUnauthorizedResult(); + + _searchService.RebuildIndex(); + + return RedirectToAction("Index"); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/Controllers/SearchController.cs b/src/Orchard.Web/Modules/Orchard.Search/Controllers/SearchController.cs index 8e4e4e45a..ba7f8f63d 100644 --- a/src/Orchard.Web/Modules/Orchard.Search/Controllers/SearchController.cs +++ b/src/Orchard.Web/Modules/Orchard.Search/Controllers/SearchController.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Web.Mvc; using Orchard.ContentManagement; using Orchard.Search.Services; @@ -14,14 +15,17 @@ namespace Orchard.Search.Controllers { _contentManager = contentManager; } - public ActionResult Index(string q) { - var searchViewModel = new SearchViewModel {Query = q}; + public ActionResult Index(string q, int page = 1, int pageSize = 10) { + var searchViewModel = new SearchViewModel { + Query = q, + DefaultPageSize = 10, // <- yeah, I know :| + PageOfResults = _searchService.Query(q, page, pageSize, searchHit => new SearchResultViewModel { + Content = _contentManager.BuildDisplayModel(_contentManager.Get(searchHit.Id), "SummaryForSearch"), + SearchHit = searchHit + }) + }; - var results = _searchService.Query(q); - searchViewModel.Results = results.Select(result => new SearchResultViewModel { - Content = _contentManager.BuildDisplayModel(_contentManager.Get(result.Id), "SummaryForSearch"), - SearchHit = result - }).ToList(); + //todo: deal with page requests beyond result count return View(searchViewModel); } diff --git a/src/Orchard.Web/Modules/Orchard.Search/Models/ISearchResult.cs b/src/Orchard.Web/Modules/Orchard.Search/Models/ISearchResult.cs new file mode 100644 index 000000000..73ecec10e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Search/Models/ISearchResult.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Orchard.Indexing; + +namespace Orchard.Search.Models { + public interface ISearchResult { + IEnumerable Page { get; set; } + int TotalCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/Models/SearchResult.cs b/src/Orchard.Web/Modules/Orchard.Search/Models/SearchResult.cs new file mode 100644 index 000000000..77f86c7c3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Search/Models/SearchResult.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Orchard.Indexing; + +namespace Orchard.Search.Models { + public class SearchResult : ISearchResult { + public IEnumerable Page { get; set; } + public int TotalCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/Orchard.Search.csproj b/src/Orchard.Web/Modules/Orchard.Search/Orchard.Search.csproj index 770b58c82..bd2162dbb 100644 --- a/src/Orchard.Web/Modules/Orchard.Search/Orchard.Search.csproj +++ b/src/Orchard.Web/Modules/Orchard.Search/Orchard.Search.csproj @@ -65,11 +65,17 @@ + + + + + + @@ -82,7 +88,9 @@ + + diff --git a/src/Orchard.Web/Modules/Orchard.Search/Permissions.cs b/src/Orchard.Web/Modules/Orchard.Search/Permissions.cs new file mode 100644 index 000000000..901f4ae65 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Search/Permissions.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Orchard.Security.Permissions; + +namespace Orchard.Search { + public class Permissions : IPermissionProvider { + public static readonly Permission ManageSearchIndex = new Permission { Description = "Manage Search Index", Name = "ManageSearchIndex" }; + + public string ModuleName { + get { + return "Search"; + } + } + + public IEnumerable GetPermissions() { + return new Permission[] { + ManageSearchIndex, + }; + } + + public IEnumerable GetDefaultStereotypes() { + return new[] { + new PermissionStereotype { + Name = "Administrator", + Permissions = new[] {ManageSearchIndex} + }, + }; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/Services/ISearchService.cs b/src/Orchard.Web/Modules/Orchard.Search/Services/ISearchService.cs index 490fcc31c..0437e73ba 100644 --- a/src/Orchard.Web/Modules/Orchard.Search/Services/ISearchService.cs +++ b/src/Orchard.Web/Modules/Orchard.Search/Services/ISearchService.cs @@ -1,8 +1,13 @@ -using System.Collections.Generic; +using System; +using Orchard.Collections; using Orchard.Indexing; namespace Orchard.Search.Services { public interface ISearchService : IDependency { - IEnumerable Query(string term); + bool HasIndexToManage { get; } + IPageOfItems Query(string query, int skip, int? take, Func shapeResult); + void RebuildIndex(); + void UpdateIndex(); + DateTime GetIndexUpdatedUtc(); } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/Services/SearchService.cs b/src/Orchard.Web/Modules/Orchard.Search/Services/SearchService.cs index 27c7b6fc7..f68d0919e 100644 --- a/src/Orchard.Web/Modules/Orchard.Search/Services/SearchService.cs +++ b/src/Orchard.Web/Modules/Orchard.Search/Services/SearchService.cs @@ -1,25 +1,84 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using Orchard.Collections; using Orchard.Indexing; +using Orchard.Localization; +using Orchard.Search.Models; +using Orchard.UI.Notify; namespace Orchard.Search.Services { public class SearchService : ISearchService { + private const string SearchIndexName = "Search"; private readonly IIndexManager _indexManager; + private readonly IEnumerable _indexNotifierHandlers; - public SearchService(IIndexManager indexManager) { + public SearchService(IOrchardServices services, IIndexManager indexManager, IEnumerable indexNotifierHandlers) { + Services = services; _indexManager = indexManager; + _indexNotifierHandlers = indexNotifierHandlers; + T = NullLocalizer.Instance; } - public IEnumerable Query(string term) { - if (string.IsNullOrWhiteSpace(term) || !_indexManager.HasIndexProvider()) - return Enumerable.Empty(); + public IOrchardServices Services { get; set; } + public Localizer T { get; set; } - return _indexManager.GetSearchIndexProvider().CreateSearchBuilder("search") - .WithField("title", term) - .WithField("body", term) - .Search(); + public bool HasIndexToManage { + get { return _indexManager.HasIndexProvider(); } + } + + IPageOfItems ISearchService.Query(string query, int page, int? pageSize, Func shapeResult) { + if (string.IsNullOrWhiteSpace(query) || !_indexManager.HasIndexProvider()) + return null; + + var searchBuilder = _indexManager.GetSearchIndexProvider().CreateSearchBuilder(SearchIndexName) + .WithField("title", query) + .WithField("body", query); + + var totalCount = searchBuilder.Count(); + if (pageSize != null) + searchBuilder = searchBuilder + .Slice((page > 0 ? page - 1 : 0) * (int)pageSize, (int)pageSize); + + + var pageOfItems = new PageOfItems(searchBuilder.Search().Select(shapeResult)) { + PageNumber = page, + PageSize = pageSize != null ? (int) pageSize : totalCount, + TotalItemCount = totalCount + }; + + return pageOfItems; + } + + void ISearchService.RebuildIndex() { + if (!_indexManager.HasIndexProvider()) { + Services.Notifier.Warning(T("There is no search index to rebuild.")); + return; + } + + var searchProvider = _indexManager.GetSearchIndexProvider(); + if (searchProvider.Exists(SearchIndexName)) + searchProvider.DeleteIndex(SearchIndexName); + + searchProvider.CreateIndex(SearchIndexName); // or just reset the updated date and let the background process recreate the index + Services.Notifier.Information(T("The search index has been rebuilt.")); + } + + void ISearchService.UpdateIndex() { + + foreach(var handler in _indexNotifierHandlers) { + handler.UpdateIndex(SearchIndexName); + } + + Services.Notifier.Information(T("The search index has been updated.")); + } + + DateTime ISearchService.GetIndexUpdatedUtc() { + return !HasIndexToManage + ? DateTime.MinValue + : _indexManager.GetSearchIndexProvider().GetLastIndexUtc(SearchIndexName); } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/Styles/admin.css b/src/Orchard.Web/Modules/Orchard.Search/Styles/admin.css new file mode 100644 index 000000000..057874152 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Search/Styles/admin.css @@ -0,0 +1,3 @@ +#main button { + display:block; +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/ViewModels/SearchIndexViewModel.cs b/src/Orchard.Web/Modules/Orchard.Search/ViewModels/SearchIndexViewModel.cs new file mode 100644 index 000000000..2a0637cda --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Search/ViewModels/SearchIndexViewModel.cs @@ -0,0 +1,10 @@ +using System; +using Orchard.Mvc.ViewModels; + +namespace Orchard.Search.ViewModels { + public class SearchIndexViewModel : BaseViewModel { + public bool HasIndexToManage { get; set; } + //todo: hang the index updated date off here to show in the admin UI (e.g. -> index updated: June 4, 2010 [update index]) + public DateTime IndexUpdatedUtc { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/ViewModels/SearchViewModel.cs b/src/Orchard.Web/Modules/Orchard.Search/ViewModels/SearchViewModel.cs index 0ccfa956e..3da6fc7cb 100644 --- a/src/Orchard.Web/Modules/Orchard.Search/ViewModels/SearchViewModel.cs +++ b/src/Orchard.Web/Modules/Orchard.Search/ViewModels/SearchViewModel.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using Orchard.Collections; using Orchard.Mvc.ViewModels; namespace Orchard.Search.ViewModels { public class SearchViewModel : BaseViewModel { - public IEnumerable Results { get; set; } public string Query { get; set; } + public int DefaultPageSize { get; set; } + public IPageOfItems PageOfResults { get; set; } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/Views/Admin/Index.ascx b/src/Orchard.Web/Modules/Orchard.Search/Views/Admin/Index.ascx new file mode 100644 index 000000000..f68e7e59b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Search/Views/Admin/Index.ascx @@ -0,0 +1,16 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Mvc.Html" %><% +Html.RegisterStyle("admin.css"); %> +

<%=Html.TitleForPage(T("Search Index Mangement").ToString()) %>

<% +using (Html.BeginForm("update", "admin", FormMethod.Post, new {area = "Orchard.Search"})) { %> +
+

<%=T("The search index was last updated {0}. ", Html.DateTimeRelative(Model.IndexUpdatedUtc))%>

+ <%=Html.AntiForgeryTokenOrchard() %> +
<% +} +using (Html.BeginForm("rebuild", "admin", FormMethod.Post, new {area = "Orchard.Search"})) { %> +
+

<%=T("Rebuild the search index for a fresh start. ") %>

+ <%=Html.AntiForgeryTokenOrchard() %> +
<% +} %> \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Search/Views/Search/Index.ascx b/src/Orchard.Web/Modules/Orchard.Search/Views/Search/Index.ascx index 415dab029..472083772 100644 --- a/src/Orchard.Web/Modules/Orchard.Search/Views/Search/Index.ascx +++ b/src/Orchard.Web/Modules/Orchard.Search/Views/Search/Index.ascx @@ -1,14 +1,17 @@ <%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> <%@ Import Namespace="Orchard.Mvc.Html" %><% Html.RegisterStyle("search.css"); %> -

<%=Html.TitleForPage(T("Search"))%>

-<% -Html.Zone("search"); %><% - -if (!string.IsNullOrWhiteSpace(Model.Query)) { %> -

<%=T("{0} results", Model.Results.Count()) %>

<% +

<%=Html.TitleForPage(T("Search").Text)%>

<% +Html.Zone("search"); +if (!string.IsNullOrWhiteSpace(Model.Query)) { + if (Model.PageOfResults.Count() == 0) { %> +

<%=T("zero results") %>

<% + } + else { %> +

<%=T("{0} - {1} of {2} results", Model.PageOfResults.StartPosition, Model.PageOfResults.EndPosition, Model.PageOfResults.TotalItemCount)%>

<% + } } - -if (Model.Results.Count() > 0) { %> - <%=Html.UnorderedList(Model.Results, (r, i) => Html.DisplayForItem(r.Content).ToHtmlString(), "search-results contentItems") %><% +if (Model.PageOfResults != null && Model.PageOfResults.Count() > 0) { %> +<%=Html.UnorderedList(Model.PageOfResults, (r, i) => Html.DisplayForItem(r.Content).ToHtmlString() , "search-results contentItems") %> +<%=Html.Pager(Model.PageOfResults, Model.PageOfResults.PageNumber, Model.DefaultPageSize, new {q = Model.Query}) %><% } %> \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs b/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs index 350218344..81cce9280 100644 --- a/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs +++ b/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs @@ -64,6 +64,7 @@ namespace Orchard.Setup.Services { "Navigation", "Scheduling", "Indexing", + "Localization", "Settings", "XmlRpc", "Orchard.Users", diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/LogOn.ascx b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/LogOn.ascx index c6813f5cc..c147703c9 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/LogOn.ascx +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/LogOn.ascx @@ -1,7 +1,7 @@ <%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> <%@ Import Namespace="Orchard.Users.ViewModels"%>

<%=Html.TitleForPage(Model.Title)%>

-

<%=_Encoded("Please enter your username and password.")%> <%= Html.ActionLink(T("Register"), "Register")%><%=_Encoded(" if you don't have an account.")%>

+

<%=_Encoded("Please enter your username and password.")%> <%= Html.ActionLink(T("Register").Text, "Register")%><%=_Encoded(" if you don't have an account.")%>

<%= Html.ValidationSummary(T("Login was unsuccessful. Please correct the errors and try again.").ToString())%> <% using (Html.BeginFormAntiForgeryPost(Url.Action("LogOn", new {ReturnUrl = Request.QueryString["ReturnUrl"]}))) { %> diff --git a/src/Orchard.Web/Themes/TheAdmin/Styles/site.css b/src/Orchard.Web/Themes/TheAdmin/Styles/site.css index 59cb915bd..e692f8aeb 100644 --- a/src/Orchard.Web/Themes/TheAdmin/Styles/site.css +++ b/src/Orchard.Web/Themes/TheAdmin/Styles/site.css @@ -57,7 +57,6 @@ body { } button { font-family:Segoe UI,Trebuchet,Arial,Sans-Serif; - font-size:1.01em; } body#preview { min-width:0; @@ -157,6 +156,7 @@ table.items th, table.items td, table.items caption { font-size:1.4em; line-heig table.items p, table.items label, table.items input, table.items .button { font-size:1em; line-height:1em; } p .button { font-size:inherit; } .meta, .hint { font-size:1.2em; } /* 12px */ +form.link button { font-size:1.01em; } /* Links ----------------------------------------------------------*/ @@ -450,9 +450,6 @@ button, .button, .button:link, .button:visited { text-align:center; padding:0 .8em .1em; } -button { - padding-top:.08em; -} form.link button { background:inherit; border:0; @@ -681,7 +678,7 @@ table .button { .contentItems .properties .icon { margin:0 .2em -.2em; } -.contentItems .related{ +.contentItems .related { float:right; font-size:1.4em; text-align:right; diff --git a/src/Orchard.Web/Web.config b/src/Orchard.Web/Web.config index 8cef8bee4..df511b385 100644 --- a/src/Orchard.Web/Web.config +++ b/src/Orchard.Web/Web.config @@ -8,7 +8,8 @@ \Windows\Microsoft.Net\Framework\v2.x\Config --> - + + - + + + + @@ -114,8 +118,9 @@ - - + + + diff --git a/src/Orchard/Collections/IPageOfItems.cs b/src/Orchard/Collections/IPageOfItems.cs new file mode 100644 index 000000000..4ed9362bd --- /dev/null +++ b/src/Orchard/Collections/IPageOfItems.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Orchard.Collections { + public interface IPageOfItems : IEnumerable { + int PageNumber { get; set; } + int PageSize { get; set; } + int TotalItemCount { get; set; } + int TotalPageCount { get; } + int StartPosition { get; } + int EndPosition { get; } + } +} \ No newline at end of file diff --git a/src/Orchard/Collections/PageOfItems.cs b/src/Orchard/Collections/PageOfItems.cs new file mode 100644 index 000000000..3e69aa696 --- /dev/null +++ b/src/Orchard/Collections/PageOfItems.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace Orchard.Collections { + public class PageOfItems : List, IPageOfItems { + public PageOfItems(IEnumerable items) { + AddRange(items); + } + + #region IPageOfItems Members + + public int PageNumber { get; set; } + public int PageSize { get; set; } + public int TotalItemCount { get; set; } + + public int TotalPageCount { + get { return (int) Math.Ceiling((double) TotalItemCount/PageSize); } + } + public int StartPosition { + get { return (PageNumber - 1)*PageSize + 1; } + } + public int EndPosition { + get { return PageNumber * PageSize > TotalItemCount ? TotalItemCount : PageNumber * PageSize; } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Orchard/Commands/DefaultOrchardCommandHandler.cs b/src/Orchard/Commands/DefaultOrchardCommandHandler.cs index 1242227e4..5e591d205 100644 --- a/src/Orchard/Commands/DefaultOrchardCommandHandler.cs +++ b/src/Orchard/Commands/DefaultOrchardCommandHandler.cs @@ -45,7 +45,7 @@ namespace Orchard.Commands { propertyInfo.SetValue(this, stringValue, null); } else { - throw new InvalidOperationException(T("No property named {0} found of type bool, int or string.", commandSwitch)); + throw new InvalidOperationException(T("No property named {0} found of type bool, int or string.", commandSwitch).ToString()); } } } @@ -55,7 +55,7 @@ namespace Orchard.Commands { CheckMethodForSwitches(context.CommandDescriptor.MethodInfo, context.Switches); object[] invokeParameters = GetInvokeParametersForMethod(context.CommandDescriptor.MethodInfo, (context.Arguments ?? Enumerable.Empty()).ToArray()); if (invokeParameters == null) { - throw new InvalidOperationException(T("Command arguments don't match")); + throw new InvalidOperationException(T("Command arguments don't match").ToString()); } this.Context = context; @@ -105,7 +105,7 @@ namespace Orchard.Commands { } foreach (var commandSwitch in switches.Keys) { if (!supportedSwitches.Contains(commandSwitch)) { - throw new InvalidOperationException(T("Method {0} does not support switch {1}.", methodInfo.Name, commandSwitch)); + throw new InvalidOperationException(T("Method {0} does not support switch {1}.", methodInfo.Name, commandSwitch).ToString()); } } } diff --git a/src/Orchard/Environment/Extensions/Compilers/CSharpExtensionBuildProvider.cs b/src/Orchard/Environment/Extensions/Compilers/CSharpExtensionBuildProvider.cs new file mode 100644 index 000000000..826eccc64 --- /dev/null +++ b/src/Orchard/Environment/Extensions/Compilers/CSharpExtensionBuildProvider.cs @@ -0,0 +1,21 @@ +using System.Web.Compilation; + +namespace Orchard.Environment.Extensions.Compilers { + public class CSharpExtensionBuildProvider : BuildProvider { + private readonly CompilerType _codeCompilerType; + + public CSharpExtensionBuildProvider() { + _codeCompilerType = GetDefaultCompilerTypeForLanguage("C#"); + } + + public override CompilerType CodeCompilerType { get { return _codeCompilerType; } } + + public override void GenerateCode(AssemblyBuilder assemblyBuilder) { + var virtualPathProvider = new DefaultVirtualPathProvider(); + var compiler = new CSharpProjectMediumTrustCompiler(virtualPathProvider); + + var aspNetAssemblyBuilder = new AspNetAssemblyBuilder(assemblyBuilder, this); + compiler.CompileProject(this.VirtualPath, aspNetAssemblyBuilder); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Compilers/CSharpExtensionCompiler.cs b/src/Orchard/Environment/Extensions/Compilers/CSharpExtensionCompiler.cs new file mode 100644 index 000000000..b267831db --- /dev/null +++ b/src/Orchard/Environment/Extensions/Compilers/CSharpExtensionCompiler.cs @@ -0,0 +1,46 @@ +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Web.Compilation; + +namespace Orchard.Environment.Extensions.Compilers { + /// + /// Compile a C# extension into an assembly given a directory location + /// + public class CSharpExtensionCompiler { + public CompilerResults CompileProject(string location) { + var codeProvider = CodeDomProvider.CreateProvider("cs"); + + var references = GetAssemblyReferenceNames(); + var options = new CompilerParameters(references.ToArray()); + + var fileNames = GetSourceFileNames(location); + var results = codeProvider.CompileAssemblyFromFile(options, fileNames.ToArray()); + return results; + } + + private IEnumerable GetAssemblyReferenceNames() { + return Enumerable.Distinct(BuildManager.GetReferencedAssemblies() + .OfType() + .Select(x => x.Location) + .Where(x => !string.IsNullOrEmpty(x))); + } + + private IEnumerable GetSourceFileNames(string path) { + foreach (var file in Directory.GetFiles(path, "*.cs")) { + yield return file; + } + + foreach (var folder in Directory.GetDirectories(path)) { + if (Path.GetFileName(folder).StartsWith(".")) + continue; + + foreach (var file in GetSourceFileNames(folder)) { + yield return file; + } + } + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Compilers/CSharpProjectFullTrustCompiler.cs b/src/Orchard/Environment/Extensions/Compilers/CSharpProjectFullTrustCompiler.cs new file mode 100644 index 000000000..d8d0ced8c --- /dev/null +++ b/src/Orchard/Environment/Extensions/Compilers/CSharpProjectFullTrustCompiler.cs @@ -0,0 +1,51 @@ +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Orchard.Environment.Extensions.Compilers { + /// + /// Compile a C# extension into an assembly given a directory location + /// + public class CSharpProjectFullTrustCompiler { + private readonly IVirtualPathProvider _virtualPathProvider; + private readonly IBuildManager _buildManager; + + public CSharpProjectFullTrustCompiler(IVirtualPathProvider virtualPathProvider, IBuildManager buildManager) { + _virtualPathProvider = virtualPathProvider; + _buildManager = buildManager; + } + + /// + /// Compile a csproj file given its virtual path. Use the CSharp CodeDomProvider + /// class, which is only available in full trust. + /// + public CompilerResults CompileProject(string virtualPath, string outputDirectory) { + var codeProvider = CodeDomProvider.CreateProvider("cs"); + var directory = _virtualPathProvider.GetDirectoryName(virtualPath); + + using (var stream = _virtualPathProvider.OpenFile(virtualPath)) { + var descriptor = new CSharpProjectParser().Parse(stream); + + var references = GetReferencedAssembliesLocation(); + var options = new CompilerParameters(references.ToArray()); + options.GenerateExecutable = false; + options.OutputAssembly = Path.Combine(outputDirectory, descriptor.AssemblyName + ".dll"); + + var fileNames = descriptor.SourceFilenames + .Select(f => _virtualPathProvider.Combine(directory, f)) + .Select(f => _virtualPathProvider.MapPath(f)); + + var results = codeProvider.CompileAssemblyFromFile(options, fileNames.ToArray()); + return results; + } + } + + private IEnumerable GetReferencedAssembliesLocation() { + return _buildManager.GetReferencedAssemblies() + .Select(a => a.Location) + .Where(a => !string.IsNullOrEmpty(a)) + .Distinct(); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Compilers/CSharpProjectMediumTrustCompiler.cs b/src/Orchard/Environment/Extensions/Compilers/CSharpProjectMediumTrustCompiler.cs new file mode 100644 index 000000000..1ab94e637 --- /dev/null +++ b/src/Orchard/Environment/Extensions/Compilers/CSharpProjectMediumTrustCompiler.cs @@ -0,0 +1,50 @@ +using System.CodeDom; +using System.IO; +using System.Linq; + +namespace Orchard.Environment.Extensions.Compilers { + /// + /// Compile a C# extension into an assembly given a directory location + /// + public class CSharpProjectMediumTrustCompiler { + private readonly IVirtualPathProvider _virtualPathProvider; + + public CSharpProjectMediumTrustCompiler(IVirtualPathProvider virtualPathProvider) { + _virtualPathProvider = virtualPathProvider; + } + /// + /// Compile a csproj file given its virtual path, a build provider and an assembly builder. + /// This method works in medium trust. + /// + public void CompileProject(string virtualPath, IAssemblyBuilder assemblyBuilder) { + using (var stream = _virtualPathProvider.OpenFile(virtualPath)) { + var descriptor = new CSharpProjectParser().Parse(stream); + + var directory = _virtualPathProvider.GetDirectoryName(virtualPath); + foreach (var filename in descriptor.SourceFilenames.Select(f => _virtualPathProvider.Combine(directory, f))) { + assemblyBuilder.AddCodeCompileUnit(CreateCompileUnit(filename)); + } + } + } + + private CodeCompileUnit CreateCompileUnit(string virtualPath) { + var contents = GetContents(virtualPath); + var unit = new CodeSnippetCompileUnit(contents); + var physicalPath = _virtualPathProvider.MapPath(virtualPath); + if (!string.IsNullOrEmpty(physicalPath)) { + unit.LinePragma = new CodeLinePragma(physicalPath, 1); + } + return unit; + } + + private string GetContents(string virtualPath) { + string contents; + using (var stream = _virtualPathProvider.OpenFile(virtualPath)) { + using (var reader = new StreamReader(stream)) { + contents = reader.ReadToEnd(); + } + } + return contents; + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Compilers/CSharpProjectParser.cs b/src/Orchard/Environment/Extensions/Compilers/CSharpProjectParser.cs new file mode 100644 index 000000000..1c0288149 --- /dev/null +++ b/src/Orchard/Environment/Extensions/Compilers/CSharpProjectParser.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; + +namespace Orchard.Environment.Extensions.Compilers { + public class CSharpProjectDescriptor { + public string AssemblyName { get; set; } + public IEnumerable SourceFilenames { get; set; } + public IEnumerable References { get; set; } + } + + public class ReferenceDescriptor { + public string AssemblyName { get; set; } + + public override string ToString() { + return "{" + (AssemblyName ?? "") + "}"; + } + } + + public class CSharpProjectParser { + public CSharpProjectDescriptor Parse(Stream stream) { + var document = XDocument.Load(XmlReader.Create(stream)); + return new CSharpProjectDescriptor { + AssemblyName = GetAssemblyName(document), + SourceFilenames = GetSourceFilenames(document).ToArray(), + References = GetReferences(document).ToArray() + }; + } + + private string GetAssemblyName(XDocument document) { + return document + .Elements(ns("Project")) + .Elements(ns("PropertyGroup")) + .Elements(ns("AssemblyName")) + .Single() + .Value; + } + + private IEnumerable GetSourceFilenames(XDocument document) { + return document + .Elements(ns("Project")) + .Elements(ns("ItemGroup")) + .Elements(ns("Compile")) + .Attributes("Include") + .Select(c => c.Value); + } + + private IEnumerable GetReferences(XDocument document) { + return document + .Elements(ns("Project")) + .Elements(ns("ItemGroup")) + .Elements(ns("Reference")) + .Attributes("Include") + .Select(c => new ReferenceDescriptor { AssemblyName = c.Value }); + } + + private static XName ns(string name) { + return XName.Get(name, "http://schemas.microsoft.com/developer/msbuild/2003"); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/ExtensionManager.cs b/src/Orchard/Environment/Extensions/ExtensionManager.cs index 7fba8f53e..bf05612ba 100644 --- a/src/Orchard/Environment/Extensions/ExtensionManager.cs +++ b/src/Orchard/Environment/Extensions/ExtensionManager.cs @@ -53,7 +53,7 @@ namespace Orchard.Environment.Extensions { private Feature LoadFeature(FeatureDescriptor featureDescriptor) { var featureName = featureDescriptor.Name; string extensionName = GetExtensionForFeature(featureName); - if (extensionName == null) throw new ArgumentException(T("Feature {0} was not found in any of the installed extensions", featureName)); + if (extensionName == null) throw new ArgumentException(T("Feature {0} was not found in any of the installed extensions", featureName).ToString()); var extension = BuildActiveExtensions().Where(x => x.Descriptor.Name == extensionName).FirstOrDefault(); if (extension == null) throw new InvalidOperationException(T("Extension ") + extensionName + T(" is not active")); diff --git a/src/Orchard/Environment/Extensions/Loaders/AreaExtensionLoader.cs b/src/Orchard/Environment/Extensions/Loaders/AreaExtensionLoader.cs index 7168eb034..5323212b7 100644 --- a/src/Orchard/Environment/Extensions/Loaders/AreaExtensionLoader.cs +++ b/src/Orchard/Environment/Extensions/Loaders/AreaExtensionLoader.cs @@ -5,17 +5,17 @@ using Orchard.Environment.Extensions.Models; namespace Orchard.Environment.Extensions.Loaders { public class AreaExtensionLoader : IExtensionLoader { - public int Order { get { return 5; } } + public int Order { get { return 50; } } public ExtensionEntry Load(ExtensionDescriptor descriptor) { if (descriptor.Location == "~/Areas") { var assembly = Assembly.Load("Orchard.Web"); return new ExtensionEntry { - Descriptor = descriptor, - Assembly = assembly, - ExportedTypes = assembly.GetExportedTypes().Where(x => IsTypeFromModule(x, descriptor)) - }; + Descriptor = descriptor, + Assembly = assembly, + ExportedTypes = assembly.GetExportedTypes().Where(x => IsTypeFromModule(x, descriptor)) + }; } return null; } diff --git a/src/Orchard/Environment/Extensions/Loaders/CoreExtensionLoader.cs b/src/Orchard/Environment/Extensions/Loaders/CoreExtensionLoader.cs index 856df10f0..ba36c7f6c 100644 --- a/src/Orchard/Environment/Extensions/Loaders/CoreExtensionLoader.cs +++ b/src/Orchard/Environment/Extensions/Loaders/CoreExtensionLoader.cs @@ -4,18 +4,20 @@ using System.Reflection; using Orchard.Environment.Extensions.Models; namespace Orchard.Environment.Extensions.Loaders { + /// + /// Load an extension by looking into specific namespaces of the "Orchard.Core" assembly + /// public class CoreExtensionLoader : IExtensionLoader { - public int Order { get { return 1; } } + public int Order { get { return 10; } } public ExtensionEntry Load(ExtensionDescriptor descriptor) { if (descriptor.Location == "~/Core") { - var assembly = Assembly.Load("Orchard.Core"); return new ExtensionEntry { - Descriptor = descriptor, - Assembly = assembly, - ExportedTypes = assembly.GetExportedTypes().Where(x => IsTypeFromModule(x, descriptor)) - }; + Descriptor = descriptor, + Assembly = assembly, + ExportedTypes = assembly.GetExportedTypes().Where(x => IsTypeFromModule(x, descriptor)) + }; } return null; } diff --git a/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs b/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs index 3178d2780..fd000783c 100644 --- a/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs +++ b/src/Orchard/Environment/Extensions/Loaders/DynamicExtensionLoader.cs @@ -1,59 +1,41 @@ -using System.CodeDom.Compiler; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Web.Compilation; -using System.Web.Hosting; +using System; using Orchard.Environment.Extensions.Models; +using Orchard.FileSystems.Dependencies; namespace Orchard.Environment.Extensions.Loaders { public class DynamicExtensionLoader : IExtensionLoader { - public int Order { get { return 10; } } + private readonly IHostEnvironment _hostEnvironment; + private readonly IBuildManager _buildManager; + private readonly IVirtualPathProvider _virtualPathProvider; + private readonly IDependenciesFolder _dependenciesFolder; + + public DynamicExtensionLoader(IHostEnvironment hostEnvironment, IBuildManager buildManager, IVirtualPathProvider virtualPathProvider, IDependenciesFolder dependenciesFolder) { + _hostEnvironment = hostEnvironment; + _buildManager = buildManager; + _virtualPathProvider = virtualPathProvider; + _dependenciesFolder = dependenciesFolder; + } + + public int Order { get { return 100; } } public ExtensionEntry Load(ExtensionDescriptor descriptor) { - if (HostingEnvironment.IsHosted == false) + string projectPath = _virtualPathProvider.Combine(descriptor.Location, descriptor.Name, + descriptor.Name + ".csproj"); + if (!_virtualPathProvider.FileExists(projectPath)) { return null; + } - var codeProvider = CodeDomProvider.CreateProvider("cs"); + var assembly = _buildManager.GetCompiledAssembly(projectPath); - var references = GetAssemblyReferenceNames(); - var options = new CompilerParameters(references.ToArray()); - - var locationPath = HostingEnvironment.MapPath(descriptor.Location); - var extensionPath = Path.Combine(locationPath, descriptor.Name); - - var fileNames = GetSourceFileNames(extensionPath); - var results = codeProvider.CompileAssemblyFromFile(options, fileNames.ToArray()); + if (_hostEnvironment.IsFullTrust) { + _dependenciesFolder.StoreAssemblyFile(descriptor.Name, assembly.Location); + } return new ExtensionEntry { - Descriptor = descriptor, - Assembly = results.CompiledAssembly, - ExportedTypes = results.CompiledAssembly.GetExportedTypes(), - }; - } - - private IEnumerable GetAssemblyReferenceNames() { - return BuildManager.GetReferencedAssemblies() - .OfType() - .Select(x => x.Location) - .Where(x => !string.IsNullOrEmpty(x)) - .Distinct(); - } - - private IEnumerable GetSourceFileNames(string path) { - foreach (var file in Directory.GetFiles(path, "*.cs")) { - yield return file; - } - - foreach (var folder in Directory.GetDirectories(path)) { - if (Path.GetFileName(folder).StartsWith(".")) - continue; - - foreach (var file in GetSourceFileNames(folder)) { - yield return file; - } - } + Descriptor = descriptor, + Assembly = assembly, + ExportedTypes = assembly.GetExportedTypes(), + }; } } } \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/PrecompiledExtensionLoader.cs b/src/Orchard/Environment/Extensions/Loaders/PrecompiledExtensionLoader.cs index 3f7a30ee2..8e65e2183 100644 --- a/src/Orchard/Environment/Extensions/Loaders/PrecompiledExtensionLoader.cs +++ b/src/Orchard/Environment/Extensions/Loaders/PrecompiledExtensionLoader.cs @@ -1,17 +1,39 @@ using Orchard.Environment.Extensions.Models; +using Orchard.FileSystems.Dependencies; namespace Orchard.Environment.Extensions.Loaders { + /// + /// Load an extension by looking into the "bin" subdirectory of an + /// extension directory. + /// public class PrecompiledExtensionLoader : IExtensionLoader { - public int Order { get { return 3; } } + private readonly IDependenciesFolder _folder; + private readonly IVirtualPathProvider _virtualPathProvider; + + public PrecompiledExtensionLoader(IDependenciesFolder folder, IVirtualPathProvider virtualPathProvider) { + _folder = folder; + _virtualPathProvider = virtualPathProvider; + } + + public int Order { get { return 30; } } public ExtensionEntry Load(ExtensionDescriptor descriptor) { - //var assembly = Assembly.Load(descriptor.Name); - //return new ModuleEntry { - // Descriptor = descriptor, - // Assembly = assembly, - // ExportedTypes = assembly.GetExportedTypes() - //}; - return null; + var extensionPath = _virtualPathProvider.Combine(descriptor.Location, descriptor.Name, "bin", + descriptor.Name + ".dll"); + if (!_virtualPathProvider.FileExists(extensionPath)) + return null; + + _folder.StoreAssemblyFile(descriptor.Name, _virtualPathProvider.MapPath(extensionPath)); + + var assembly = _folder.LoadAssembly(descriptor.Name); + if (assembly == null) + return null; + + return new ExtensionEntry { + Descriptor = descriptor, + Assembly = assembly, + ExportedTypes = assembly.GetExportedTypes() + }; } } } \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/ProbingExtensionLoader.cs b/src/Orchard/Environment/Extensions/Loaders/ProbingExtensionLoader.cs new file mode 100644 index 000000000..6f83506a2 --- /dev/null +++ b/src/Orchard/Environment/Extensions/Loaders/ProbingExtensionLoader.cs @@ -0,0 +1,30 @@ +using Orchard.Environment.Extensions.Models; +using Orchard.FileSystems.Dependencies; + +namespace Orchard.Environment.Extensions.Loaders { + /// + /// Load an extension using the "Assembly.Load" method if the + /// file can be found in the "App_Data/Dependencies" folder. + /// + public class ProbingExtensionLoader : IExtensionLoader { + private readonly IDependenciesFolder _folder; + + public ProbingExtensionLoader(IDependenciesFolder folder) { + _folder = folder; + } + + public int Order { get { return 40; } } + + public ExtensionEntry Load(ExtensionDescriptor descriptor) { + var assembly = _folder.LoadAssembly(descriptor.Name); + if (assembly == null) + return null; + + return new ExtensionEntry { + Descriptor = descriptor, + Assembly = assembly, + ExportedTypes = assembly.GetExportedTypes() + }; + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/Extensions/Loaders/ReferencedExtensionLoader.cs b/src/Orchard/Environment/Extensions/Loaders/ReferencedExtensionLoader.cs index 4aa7ce345..39b8a19f0 100644 --- a/src/Orchard/Environment/Extensions/Loaders/ReferencedExtensionLoader.cs +++ b/src/Orchard/Environment/Extensions/Loaders/ReferencedExtensionLoader.cs @@ -5,8 +5,11 @@ using System.Web.Hosting; using Orchard.Environment.Extensions.Models; namespace Orchard.Environment.Extensions.Loaders { + /// + /// Load an extension by looking through the BuildManager referenced assemblies + /// public class ReferencedExtensionLoader : IExtensionLoader { - public int Order { get { return 2; } } + public int Order { get { return 20; } } public ExtensionEntry Load(ExtensionDescriptor descriptor) { if (HostingEnvironment.IsHosted == false) @@ -20,10 +23,10 @@ namespace Orchard.Environment.Extensions.Loaders { return null; return new ExtensionEntry { - Descriptor = descriptor, - Assembly = assembly, - ExportedTypes = assembly.GetExportedTypes() - }; + Descriptor = descriptor, + Assembly = assembly, + ExportedTypes = assembly.GetExportedTypes() + }; } } } \ No newline at end of file diff --git a/src/Orchard/Environment/IAssemblyBuilder.cs b/src/Orchard/Environment/IAssemblyBuilder.cs new file mode 100644 index 000000000..055fb1caa --- /dev/null +++ b/src/Orchard/Environment/IAssemblyBuilder.cs @@ -0,0 +1,22 @@ +using System.CodeDom; +using System.Web.Compilation; + +namespace Orchard.Environment { + public interface IAssemblyBuilder { + void AddCodeCompileUnit(CodeCompileUnit compileUnit); + } + + public class AspNetAssemblyBuilder : IAssemblyBuilder { + private readonly AssemblyBuilder _assemblyBuilder; + private readonly BuildProvider _buildProvider; + + public AspNetAssemblyBuilder(AssemblyBuilder assemblyBuilder, BuildProvider buildProvider) { + _assemblyBuilder = assemblyBuilder; + _buildProvider = buildProvider; + } + + public void AddCodeCompileUnit(CodeCompileUnit compileUnit) { + _assemblyBuilder.AddCodeCompileUnit(_buildProvider, compileUnit); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/IBuildManager.cs b/src/Orchard/Environment/IBuildManager.cs new file mode 100644 index 000000000..625e7190a --- /dev/null +++ b/src/Orchard/Environment/IBuildManager.cs @@ -0,0 +1,21 @@ +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using System.Web.Compilation; + +namespace Orchard.Environment { + public interface IBuildManager : IDependency { + IEnumerable GetReferencedAssemblies(); + Assembly GetCompiledAssembly(string virtualPath); + } + + public class DefaultBuildManager : IBuildManager { + public IEnumerable GetReferencedAssemblies() { + return BuildManager.GetReferencedAssemblies().OfType(); + } + + public Assembly GetCompiledAssembly(string virtualPath) { + return BuildManager.GetCompiledAssembly(virtualPath); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/IHostEnvironment.cs b/src/Orchard/Environment/IHostEnvironment.cs new file mode 100644 index 000000000..694bc5cc0 --- /dev/null +++ b/src/Orchard/Environment/IHostEnvironment.cs @@ -0,0 +1,22 @@ +using System; +using System.Web.Hosting; + +namespace Orchard.Environment { + /// + /// Abstraction of the running environment + /// + public interface IHostEnvironment : IDependency { + bool IsFullTrust { get; } + string MapPath(string virtualPath); + } + + public class DefaultHostEnvironment : IHostEnvironment { + public bool IsFullTrust { + get { return AppDomain.CurrentDomain.IsFullyTrusted; } + } + + public string MapPath(string virtualPath) { + return HostingEnvironment.MapPath(virtualPath); + } + } +} diff --git a/src/Orchard/Environment/IVirtualPathProvider.cs b/src/Orchard/Environment/IVirtualPathProvider.cs new file mode 100644 index 000000000..37b824e39 --- /dev/null +++ b/src/Orchard/Environment/IVirtualPathProvider.cs @@ -0,0 +1,50 @@ +using System.IO; +using System.Web.Hosting; +using Orchard.Caching; + +namespace Orchard.Environment { + public interface IVirtualPathProvider : IVolatileProvider { + string GetDirectoryName(string virtualPath); + string Combine(params string[] paths); + Stream OpenFile(string virtualPath); + StreamWriter CreateText(string virtualPath); + string MapPath(string virtualPath); + bool FileExists(string virtualPath); + bool DirectoryExists(string virtualPath); + void CreateDirectory(string virtualPath); + } + + public class DefaultVirtualPathProvider : IVirtualPathProvider { + public string GetDirectoryName(string virtualPath) { + return Path.GetDirectoryName(virtualPath).Replace('\\', '/'); + } + + public string Combine(params string[] paths) { + return Path.Combine(paths).Replace('\\', '/'); + } + + public Stream OpenFile(string virtualPath) { + return HostingEnvironment.VirtualPathProvider.GetFile(virtualPath).Open(); + } + + public StreamWriter CreateText(string virtualPath) { + return File.CreateText(MapPath(virtualPath)); + } + + public string MapPath(string virtualPath) { + return HostingEnvironment.MapPath(virtualPath); + } + + public bool FileExists(string virtualPath) { + return HostingEnvironment.VirtualPathProvider.FileExists(virtualPath); + } + + public bool DirectoryExists(string virtualPath) { + return HostingEnvironment.VirtualPathProvider.DirectoryExists(virtualPath); + } + + public void CreateDirectory(string virtualPath) { + Directory.CreateDirectory(MapPath(virtualPath)); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Environment/OrchardStarter.cs b/src/Orchard/Environment/OrchardStarter.cs index f8e6ec8f4..a86204858 100644 --- a/src/Orchard/Environment/OrchardStarter.cs +++ b/src/Orchard/Environment/OrchardStarter.cs @@ -15,6 +15,7 @@ using Orchard.Environment.State; using Orchard.Environment.Topology; using Orchard.Events; using Orchard.FileSystems.AppData; +using Orchard.FileSystems.Dependencies; using Orchard.FileSystems.WebSite; using Orchard.Logging; using Orchard.Services; @@ -30,10 +31,14 @@ namespace Orchard.Environment { // a single default host implementation is needed for bootstrapping a web app domain builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); RegisterVolatileProvider(builder); RegisterVolatileProvider(builder); RegisterVolatileProvider(builder); + RegisterVolatileProvider(builder); + RegisterVolatileProvider(builder); builder.RegisterType().As().As().SingleInstance(); { @@ -63,6 +68,7 @@ namespace Orchard.Environment { builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); } } diff --git a/src/Orchard/Environment/State/ShellStateCoordinator.cs b/src/Orchard/Environment/State/ShellStateCoordinator.cs index 1ef1d1854..3d408998f 100644 --- a/src/Orchard/Environment/State/ShellStateCoordinator.cs +++ b/src/Orchard/Environment/State/ShellStateCoordinator.cs @@ -148,13 +148,13 @@ namespace Orchard.Environment.State { })); // lower enabled states in reverse order - foreach (var entry in allEntries.Where(entry => entry.FeatureState.EnableState == ShellFeatureState.State.Falling)) { + foreach (var entry in allEntries.Reverse().Where(entry => entry.FeatureState.EnableState == ShellFeatureState.State.Falling)) { _featureEvents.Disable(entry.Feature); _stateManager.UpdateEnabledState(entry.FeatureState, ShellFeatureState.State.Down); } // lower installed states in reverse order - foreach (var entry in allEntries.Where(entry => entry.FeatureState.InstallState == ShellFeatureState.State.Falling)) { + foreach (var entry in allEntries.Reverse().Where(entry => entry.FeatureState.InstallState == ShellFeatureState.State.Falling)) { _featureEvents.Uninstall(entry.Feature); _stateManager.UpdateInstalledState(entry.FeatureState, ShellFeatureState.State.Down); } diff --git a/src/Orchard/FileSystems/Dependencies/IDependenciesFolder.cs b/src/Orchard/FileSystems/Dependencies/IDependenciesFolder.cs new file mode 100644 index 000000000..64ffa7578 --- /dev/null +++ b/src/Orchard/FileSystems/Dependencies/IDependenciesFolder.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Orchard.Caching; +using Orchard.Environment; + +namespace Orchard.FileSystems.Dependencies { + public interface IDependenciesFolder : IVolatileProvider { + void StoreAssemblyFile(string assemblyName, string assemblyFileName); + Assembly LoadAssembly(string assemblyName); + } + + public class DefaultDependenciesFolder : IDependenciesFolder { + private const string _basePath = "~/App_Data/Dependencies"; + private readonly IVirtualPathProvider _virtualPathProvider; + + public DefaultDependenciesFolder(IVirtualPathProvider virtualPathProvider) { + _virtualPathProvider = virtualPathProvider; + } + + private string BasePath { + get { + return _basePath; + } + } + + private string PersistencePath { + get { + return _virtualPathProvider.Combine(BasePath, "dependencies.xml"); + } + } + + public void StoreAssemblyFile(string assemblyName, string assemblyFileName) { + _virtualPathProvider.CreateDirectory(BasePath); + + // Only store assembly if it's more recent that what we have stored already (if anything) + if (IsNewerAssembly(assemblyName, assemblyFileName)) { + var destinationFileName = Path.GetFileName(assemblyFileName); + var destinationPath = _virtualPathProvider.MapPath(_virtualPathProvider.Combine(BasePath, destinationFileName)); + File.Copy(assemblyFileName, destinationPath); + + StoreDepencyInformation(assemblyName, destinationFileName); + } + } + + private bool IsNewerAssembly(string assemblyName, string assemblyFileName) { + var dependency = ReadDependencies().SingleOrDefault(d => d.Name == assemblyName); + if (dependency == null) { + return true; + } + + var existingFileName = _virtualPathProvider.MapPath(_virtualPathProvider.Combine(BasePath, dependency.FileName)); + if (!File.Exists(existingFileName)) { + return true; + } + + return (File.GetCreationTimeUtc(existingFileName) <= File.GetCreationTimeUtc(assemblyFileName)); + } + + private void StoreDepencyInformation(string name, string fileName) { + var dependencies = ReadDependencies().ToList(); + + var dependency = dependencies.SingleOrDefault(d => d.Name == name); + if (dependency == null) { + dependency = new DependencyDescritpor { Name = name, FileName = fileName }; + dependencies.Add(dependency); + } + dependency.FileName = fileName; + + WriteDependencies(dependencies); + } + + public Assembly LoadAssembly(string assemblyName) { + _virtualPathProvider.CreateDirectory(BasePath); + + var dependency = ReadDependencies().SingleOrDefault(d => d.Name == assemblyName); + if (dependency == null) + return null; + + if (!_virtualPathProvider.FileExists(_virtualPathProvider.Combine(BasePath, dependency.FileName))) + return null; + + return Assembly.Load(Path.GetFileNameWithoutExtension(dependency.FileName)); + } + + private class DependencyDescritpor { + public string Name { get; set; } + public string FileName { get; set; } + } + + private IEnumerable ReadDependencies() { + if (!_virtualPathProvider.FileExists(PersistencePath)) + return Enumerable.Empty(); + + using (var stream = _virtualPathProvider.OpenFile(PersistencePath)) { + XDocument document = XDocument.Load(stream); + return document + .Elements(ns("Dependencies")) + .Elements(ns("Dependency")) + .Select(e => new DependencyDescritpor { Name = e.Element("Name").Value, FileName = e.Element("FileName").Value }) + .ToList(); + } + } + + private void WriteDependencies(IEnumerable dependencies) { + var document = new XDocument(); + document.Add(new XElement(ns("Dependencies"))); + var elements = dependencies.Select(d => new XElement("Dependency", + new XElement(ns("Name"), d.Name), + new XElement(ns("FileName"), d.FileName))); + document.Root.Add(elements); + + using (var stream = _virtualPathProvider.CreateText(PersistencePath)) { + document.Save(stream, SaveOptions.None); + } + } + + private static XName ns(string name) { + return XName.Get(name/*, "http://schemas.microsoft.com/developer/msbuild/2003"*/); + } + } +} diff --git a/src/Orchard/Indexing/IIndexNotifierHandler.cs b/src/Orchard/Indexing/IIndexNotifierHandler.cs new file mode 100644 index 000000000..f115544d9 --- /dev/null +++ b/src/Orchard/Indexing/IIndexNotifierHandler.cs @@ -0,0 +1,5 @@ +namespace Orchard.Indexing { + public interface IIndexNotifierHandler : IEvents { + void UpdateIndex(string indexName); + } +} diff --git a/src/Orchard/Indexing/IIndexProvider.cs b/src/Orchard/Indexing/IIndexProvider.cs index 4045931e8..d873b1a13 100644 --- a/src/Orchard/Indexing/IIndexProvider.cs +++ b/src/Orchard/Indexing/IIndexProvider.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Orchard.Indexing { public interface IIndexProvider : IDependency { @@ -17,6 +18,16 @@ namespace Orchard.Indexing { ///
void DeleteIndex(string name); + /// + /// Whether an index is empty or not + /// + bool IsEmpty(string indexName); + + /// + /// Gets the number of indexed documents + /// + int NumDocs(string indexName); + /// /// Creates an empty document /// @@ -48,5 +59,16 @@ namespace Orchard.Indexing { /// /// A search builder instance ISearchBuilder CreateSearchBuilder(string indexName); + + /// + /// Returns the date and time when the index was last processed + /// + DateTime GetLastIndexUtc(string indexName); + + /// + /// Sets the date and time when the index was last processed + /// + void SetLastIndexUtc(string indexName, DateTime lastIndexUtc); + } } \ No newline at end of file diff --git a/src/Orchard/Indexing/ISearchBuilder.cs b/src/Orchard/Indexing/ISearchBuilder.cs index 3994a37bd..83e3ffda4 100644 --- a/src/Orchard/Indexing/ISearchBuilder.cs +++ b/src/Orchard/Indexing/ISearchBuilder.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Orchard.Indexing { public interface ISearchBuilder { - ISearchBuilder Parse(string query); + ISearchBuilder Parse(string defaultField, string query); ISearchBuilder WithField(string field, string value); ISearchBuilder WithField(string field, string value, bool wildcardSearch); diff --git a/src/Orchard/Localization/LocalizedString.cs b/src/Orchard/Localization/LocalizedString.cs index a9bd2d934..8197f4219 100644 --- a/src/Orchard/Localization/LocalizedString.cs +++ b/src/Orchard/Localization/LocalizedString.cs @@ -12,12 +12,12 @@ namespace Orchard.Localization { return new LocalizedString(x); } - public override string ToString() { - return _localized; + public string Text { + get { return _localized; } } - public static implicit operator string(LocalizedString x) { - return x._localized; + public override string ToString() { + return _localized; } public override int GetHashCode() { diff --git a/src/Orchard/Localization/Services/DefaultCultureManager.cs b/src/Orchard/Localization/Services/DefaultCultureManager.cs index 36a48f445..ae77a121c 100644 --- a/src/Orchard/Localization/Services/DefaultCultureManager.cs +++ b/src/Orchard/Localization/Services/DefaultCultureManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Web; @@ -46,6 +47,13 @@ namespace Orchard.Localization.Services { return String.Empty; } + public int GetCultureIdByName(string cultureName) { + if (!IsValidCulture(cultureName)) { + throw new ArgumentException("cultureName"); + } + return _cultureRepository.Get(x => x.Culture == cultureName).Id; + } + // "" or // "-" or // "--" diff --git a/src/Orchard/Localization/Services/ICultureManager.cs b/src/Orchard/Localization/Services/ICultureManager.cs index 2a55558f1..53c7d13af 100644 --- a/src/Orchard/Localization/Services/ICultureManager.cs +++ b/src/Orchard/Localization/Services/ICultureManager.cs @@ -6,5 +6,6 @@ namespace Orchard.Localization.Services { IEnumerable ListCultures(); void AddCulture(string cultureName); string GetCurrentCulture(HttpContext requestContext); + int GetCultureIdByName(string cultureName); } } diff --git a/src/Orchard/Mvc/Html/HtmlHelperExtensions.cs b/src/Orchard/Mvc/Html/HtmlHelperExtensions.cs index 8615286b2..c302658aa 100644 --- a/src/Orchard/Mvc/Html/HtmlHelperExtensions.cs +++ b/src/Orchard/Mvc/Html/HtmlHelperExtensions.cs @@ -3,10 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Text; -using System.Text.RegularExpressions; using System.Web.Mvc; using System.Web.Mvc.Html; using System.Web.Routing; +using Orchard.Collections; using Orchard.Mvc.ViewModels; using Orchard.Services; using Orchard.Settings; @@ -41,6 +41,74 @@ namespace Orchard.Mvc.Html { return MvcHtmlString.Create(builder.ToString(TagRenderMode.Normal)); } + #region Pager + + public static string Pager(this HtmlHelper html, IPageOfItems pageOfItems, int currentPage, int defaultPageSize, object values = null, string previousText = "<", string nextText = ">", bool alwaysShowPreviousAndNext = false) { + if (pageOfItems.TotalPageCount < 2) + return ""; + + var sb = new StringBuilder(75); + var rvd = new RouteValueDictionary {{"q", ""},{"page", 0}}; + var viewContext = html.ViewContext; + var urlHelper = new UrlHelper(viewContext.RequestContext); + + if (pageOfItems.PageSize != defaultPageSize) + rvd.Add("pagesize", pageOfItems.PageSize); + + foreach (var item in viewContext.RouteData.Values) { + rvd.Add(item.Key, item.Value); + } + + + if (values != null) { + var rvd2 = new RouteValueDictionary(values); + + foreach (var item in rvd2) { + rvd[item.Key] = item.Value; + } + } + + sb.Append("

"); + + if (currentPage > 1 || alwaysShowPreviousAndNext) { + if (currentPage == 2) + rvd.Remove("page"); + else + rvd["page"] = currentPage - 1; + + sb.AppendFormat(" {0}", previousText, + urlHelper.RouteUrl(rvd)); + } + + //todo: when there are many pages (> 15?) maybe do something like 1 2 3...6 7 8...13 14 15 + for (var p = 1; p <= pageOfItems.TotalPageCount; p++) { + if (p == currentPage) { + sb.AppendFormat(" {0}", p); + } + else { + if (p == 1) + rvd.Remove("page"); + else + rvd["page"] = p; + + sb.AppendFormat(" {0}", p, + urlHelper.RouteUrl(rvd)); + } + } + + if (currentPage < pageOfItems.TotalPageCount || alwaysShowPreviousAndNext) { + rvd["page"] = currentPage + 1; + sb.AppendFormat("{0}", nextText, + urlHelper.RouteUrl(rvd)); + } + + sb.Append("

"); + + return sb.ToString(); + } + + #endregion + #region UnorderedList public static string UnorderedList(this HtmlHelper htmlHelper, IEnumerable items, Func generateContent, string cssClass) { @@ -111,7 +179,7 @@ namespace Orchard.Mvc.Html { TimeSpan time = htmlHelper.Resolve().UtcNow - value; if (time.TotalDays > 7) - return "at " + htmlHelper.DateTime(value); + return "on " + htmlHelper.DateTime(value, "MMM d yyyy 'at' h:mm tt"); if (time.TotalHours > 24) return string.Format("{0} day{1} ago", time.Days, time.Days == 1 ? "" : "s"); if (time.TotalMinutes > 60) diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 6195a1980..a867a4a91 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -130,6 +130,8 @@ + + Code @@ -345,6 +347,17 @@ + + + + + + + + + + + @@ -353,6 +366,7 @@ + diff --git a/src/Orchard/Tasks/Indexing/IIndexingTaskManager.cs b/src/Orchard/Tasks/Indexing/IIndexingTaskManager.cs index cdb950e0f..1bd06d54c 100644 --- a/src/Orchard/Tasks/Indexing/IIndexingTaskManager.cs +++ b/src/Orchard/Tasks/Indexing/IIndexingTaskManager.cs @@ -15,14 +15,9 @@ namespace Orchard.Tasks.Indexing { void CreateDeleteIndexTask(ContentItem contentItem); /// - /// Loads all indexing tasks created after to a specific date and time + /// Returns the Date Time of the last task created /// - IEnumerable GetTasks(DateTime? createdAfter); - - /// - /// Deletes all indexing tasks previous to a specific date and time - /// - void DeleteTasks(DateTime? createdBefore); + DateTime GetLastTaskDateTime(); /// /// Deletes all indexing tasks assigned to a specific content item diff --git a/src/Orchard/UI/Navigation/NavigationBuilder.cs b/src/Orchard/UI/Navigation/NavigationBuilder.cs index 5848557b4..8fbb45108 100644 --- a/src/Orchard/UI/Navigation/NavigationBuilder.cs +++ b/src/Orchard/UI/Navigation/NavigationBuilder.cs @@ -1,35 +1,32 @@ using System; using System.Collections.Generic; using System.Linq; +using Orchard.Localization; namespace Orchard.UI.Navigation { public class NavigationBuilder { IEnumerable Contained { get; set; } - public NavigationBuilder Add(string caption, string position, Action itemBuilder) { + public NavigationBuilder Add(LocalizedString caption, string position, Action itemBuilder) { var childBuilder = new NavigationItemBuilder(); - if (!string.IsNullOrEmpty(caption)) - childBuilder.Caption(caption); - - if (!string.IsNullOrEmpty(position)) - childBuilder.Position(position); - + childBuilder.Caption(caption); + childBuilder.Position(position); itemBuilder(childBuilder); Contained = (Contained ?? Enumerable.Empty()).Concat(childBuilder.Build()); return this; } - public NavigationBuilder Add(string caption, Action itemBuilder) { + public NavigationBuilder Add(LocalizedString caption, Action itemBuilder) { return Add(caption, null, itemBuilder); } public NavigationBuilder Add(Action itemBuilder) { return Add(null, null, itemBuilder); } - public NavigationBuilder Add(string caption, string position) { + public NavigationBuilder Add(LocalizedString caption, string position) { return Add(caption, position, x=> { }); } - public NavigationBuilder Add(string caption) { + public NavigationBuilder Add(LocalizedString caption) { return Add(caption, null, x => { }); } diff --git a/src/Orchard/UI/Navigation/NavigationItemBuilder.cs b/src/Orchard/UI/Navigation/NavigationItemBuilder.cs index 52a0c9246..61bc5da3a 100644 --- a/src/Orchard/UI/Navigation/NavigationItemBuilder.cs +++ b/src/Orchard/UI/Navigation/NavigationItemBuilder.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Web.Routing; +using Orchard.Localization; using Orchard.Security.Permissions; namespace Orchard.UI.Navigation { @@ -11,8 +12,9 @@ namespace Orchard.UI.Navigation { _item = new MenuItem(); } - public NavigationItemBuilder Caption(string caption) { - _item.Text = caption; + public NavigationItemBuilder Caption(LocalizedString caption) { + if (caption != null) + _item.Text = caption.Text; return this; }