mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-26 12:03:16 +08:00
Aligning package update page to gallery. Moving the update feature to the packaging module as a feature.
--HG-- branch : dev
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
using Orchard.Environment.Extensions;
|
||||
using System.Linq;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Packaging.Services;
|
||||
using Orchard.UI.Navigation;
|
||||
using Orchard.Security;
|
||||
|
||||
@@ -9,6 +12,14 @@ namespace Orchard.Packaging {
|
||||
public Localizer T { get; set; }
|
||||
public string MenuName { get { return "admin"; } }
|
||||
|
||||
private readonly IBackgroundPackageUpdateStatus _backgroundPackageUpdateStatus;
|
||||
|
||||
public AdminMenu(IBackgroundPackageUpdateStatus backgroundPackageUpdateStatus) {
|
||||
_backgroundPackageUpdateStatus = backgroundPackageUpdateStatus;
|
||||
}
|
||||
|
||||
public AdminMenu() {}
|
||||
|
||||
public void GetNavigation(NavigationBuilder builder) {
|
||||
builder.Add(T("Themes"), "25", menu => menu
|
||||
.Add(T("Available"), "1", item => item.Action("Themes", "Gallery", new { area = "Orchard.Packaging" })
|
||||
@@ -21,6 +32,37 @@ namespace Orchard.Packaging {
|
||||
builder.Add(T("Configuration"), "50", menu => menu
|
||||
.Add(T("Feeds"), "25", item => item.Action("Sources", "Gallery", new { area = "Orchard.Packaging" })
|
||||
.Permission(StandardPermissions.SiteOwner)));
|
||||
|
||||
if (_backgroundPackageUpdateStatus != null) {
|
||||
// Only available if feature is enabled
|
||||
|
||||
int modulesUpdateCount = GetUpdateCount(DefaultExtensionTypes.Module);
|
||||
LocalizedString modulesCaption = (modulesUpdateCount == 0 ? T("Updates") : T("Updates ({0})", modulesUpdateCount));
|
||||
|
||||
int themesUpdateCount = GetUpdateCount(DefaultExtensionTypes.Theme);
|
||||
LocalizedString themesCaption = (themesUpdateCount == 0 ? T("Updates") : T("Updates ({0})", themesUpdateCount));
|
||||
|
||||
builder.Add(T("Modules"), "20", menu => menu
|
||||
.Add(modulesCaption, "30.0", item => item.Action("ModulesUpdates", "GalleryUpdates", new { area = "Orchard.Packaging" })
|
||||
.Permission(StandardPermissions.SiteOwner).LocalNav()));
|
||||
|
||||
builder.Add(T("Themes"), "25", menu => menu
|
||||
.Add(themesCaption, "30.0", item => item.Action("ThemesUpdates", "GalleryUpdates", new { area = "Orchard.Packaging" })
|
||||
.Permission(StandardPermissions.SiteOwner).LocalNav()));
|
||||
}
|
||||
}
|
||||
|
||||
private int GetUpdateCount(string extensionType) {
|
||||
try {
|
||||
// Admin menu should never block, so simply return the result from the background task
|
||||
return _backgroundPackageUpdateStatus.Value == null ?
|
||||
0 :
|
||||
_backgroundPackageUpdateStatus.Value.Entries.Where(updatePackageEntry =>
|
||||
updatePackageEntry.NewVersionToInstall != null &&
|
||||
updatePackageEntry.ExtensionsDescriptor.ExtensionType.Equals(extensionType)).Count();
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.DisplayManagement;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Packaging.Models;
|
||||
using Orchard.Packaging.Services;
|
||||
using Orchard.Packaging.ViewModels;
|
||||
using Orchard.Reports;
|
||||
using Orchard.Reports.Services;
|
||||
using Orchard.Security;
|
||||
using Orchard.Themes;
|
||||
using Orchard.UI.Admin;
|
||||
using Orchard.UI.Navigation;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Packaging.Controllers {
|
||||
[OrchardFeature("Gallery.Updates")]
|
||||
[Themed, Admin]
|
||||
public class GalleryUpdatesController : Controller {
|
||||
private readonly IPackagingSourceManager _packagingSourceManager;
|
||||
private readonly INotifier _notifier;
|
||||
private readonly IPackageUpdateService _packageUpdateService;
|
||||
private readonly IBackgroundPackageUpdateStatus _backgroundPackageUpdateStatus;
|
||||
private readonly IReportsCoordinator _reportsCoordinator;
|
||||
private readonly IReportsManager _reportsManager;
|
||||
|
||||
public GalleryUpdatesController(IOrchardServices services,
|
||||
IPackagingSourceManager packagingSourceManager,
|
||||
INotifier notifier,
|
||||
IPackageUpdateService packageUpdateService,
|
||||
IBackgroundPackageUpdateStatus backgroundPackageUpdateStatus,
|
||||
IReportsCoordinator reportsCoordinator,
|
||||
IReportsManager reportsManager,
|
||||
IShapeFactory shapeFactory) {
|
||||
|
||||
_packagingSourceManager = packagingSourceManager;
|
||||
_notifier = notifier;
|
||||
_packageUpdateService = packageUpdateService;
|
||||
_backgroundPackageUpdateStatus = backgroundPackageUpdateStatus;
|
||||
_reportsCoordinator = reportsCoordinator;
|
||||
_reportsManager = reportsManager;
|
||||
Services = services;
|
||||
Shape = shapeFactory;
|
||||
|
||||
T = NullLocalizer.Instance;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public IOrchardServices Services { get; private set; }
|
||||
public Localizer T { get; set; }
|
||||
public ILogger Logger { get; set; }
|
||||
public dynamic Shape { get; set; }
|
||||
|
||||
public ActionResult ThemesUpdates(int? reportId, PagerParameters pagerParameters) {
|
||||
return PackageUpdate("ThemesUpdates", DefaultExtensionTypes.Theme, reportId, pagerParameters);
|
||||
}
|
||||
|
||||
public ActionResult ModulesUpdates(int? reportId, PagerParameters pagerParameters) {
|
||||
return PackageUpdate("ModulesUpdates", DefaultExtensionTypes.Module, reportId, pagerParameters);
|
||||
}
|
||||
|
||||
private ActionResult PackageUpdate(string view, string extensionType, int? reportId, PagerParameters pagerParameters) {
|
||||
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to install packages")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
Pager pager = new Pager(Services.WorkContext.CurrentSite, pagerParameters);
|
||||
|
||||
if (reportId != null)
|
||||
CreateNotificationsFromReport(reportId.Value);
|
||||
|
||||
if (!_packagingSourceManager.GetSources().Any()) {
|
||||
Services.Notifier.Error(T("No Gallery feed configured"));
|
||||
return View(view, new PackagingListViewModel { Entries = new List<UpdatePackageEntry>() });
|
||||
}
|
||||
|
||||
// Get status from background task state or directly
|
||||
_backgroundPackageUpdateStatus.Value =
|
||||
_backgroundPackageUpdateStatus.Value ??
|
||||
_packageUpdateService.GetPackagesStatus(_packagingSourceManager.GetSources());
|
||||
|
||||
foreach (var error in _backgroundPackageUpdateStatus.Value.Errors) {
|
||||
for (var scan = error; scan != null; scan = scan.InnerException) {
|
||||
Services.Notifier.Warning(T("Package retrieve error: {0}", scan.Message));
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<UpdatePackageEntry> updatedPackages = _backgroundPackageUpdateStatus.Value.Entries
|
||||
.Where(updatePackageEntry =>
|
||||
updatePackageEntry.ExtensionsDescriptor.ExtensionType.Equals(extensionType) &&
|
||||
updatePackageEntry.NewVersionToInstall != null);
|
||||
|
||||
int totalItemCount = updatedPackages.Count();
|
||||
|
||||
if (pager.PageSize != 0) {
|
||||
updatedPackages = updatedPackages.Skip((pager.Page - 1) * pager.PageSize).Take(pager.PageSize);
|
||||
}
|
||||
|
||||
return View(view, new PackagingListViewModel {
|
||||
Entries = updatedPackages,
|
||||
Pager = Shape.Pager(pager).TotalItemCount(totalItemCount)
|
||||
});
|
||||
}
|
||||
|
||||
public ActionResult Install(string packageId, string version, int sourceId, string returnUrl) {
|
||||
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to install packages")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
_backgroundPackageUpdateStatus.Value =
|
||||
_backgroundPackageUpdateStatus.Value ??
|
||||
_packageUpdateService.GetPackagesStatus(_packagingSourceManager.GetSources());
|
||||
|
||||
var entry = _backgroundPackageUpdateStatus.Value
|
||||
.Entries
|
||||
.SelectMany(e => e.PackageVersions)
|
||||
.Where(e => e.PackageId == packageId && e.Version == version && e.Source.Id == sourceId)
|
||||
.FirstOrDefault();
|
||||
if (entry == null) {
|
||||
return HttpNotFound();
|
||||
}
|
||||
|
||||
try {
|
||||
_packageUpdateService.Update(entry);
|
||||
}
|
||||
catch (Exception exception) {
|
||||
Logger.Error(exception, "Error installing package {0}, version {1} from source {2}", packageId, version, sourceId);
|
||||
_notifier.Error(T("Error installing package update."));
|
||||
for (Exception scan = exception; scan != null; scan = scan.InnerException) {
|
||||
_notifier.Error(T("{0}", scan.Message));
|
||||
}
|
||||
}
|
||||
|
||||
int reportId = CreateReport(T("Package Update"), T("Update of package {0} to version {1}", packageId, version));
|
||||
|
||||
return RedirectToAction(returnUrl, new { reportId });
|
||||
}
|
||||
|
||||
private void CreateNotificationsFromReport(int reportId) {
|
||||
// If we have notification in TempData, we don't need to display the
|
||||
// report as notifications (i.e. the AppDomain hasn't been restarted)
|
||||
// Note: This relies on an implementation detail of "Orchard.UI.Notify.NotifyFilter"
|
||||
if (TempData["messages"] != null)
|
||||
return;
|
||||
|
||||
var report = _reportsManager.Get(reportId);
|
||||
if (report == null)
|
||||
return;
|
||||
|
||||
if (report.Entries.Any()) {
|
||||
_notifier.Information(T("Application has been restarted. The following notifications originate from report #{0}:", reportId));
|
||||
}
|
||||
foreach(var entry in report.Entries) {
|
||||
switch(entry.Type) {
|
||||
case ReportEntryType.Information:
|
||||
_notifier.Add(NotifyType.Information, T(entry.Message));
|
||||
break;
|
||||
case ReportEntryType.Warning:
|
||||
_notifier.Add(NotifyType.Warning, T(entry.Message));
|
||||
break;
|
||||
case ReportEntryType.Error:
|
||||
default:
|
||||
_notifier.Add(NotifyType.Error, T(entry.Message));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int CreateReport(LocalizedString activityName, LocalizedString title) {
|
||||
// Create a persistent report with all notifications, in case the application needs to be restarted
|
||||
const string reportKey = "PackageManager";
|
||||
|
||||
int reportId = _reportsCoordinator.Register(reportKey, activityName.Text, title.Text);
|
||||
|
||||
foreach(var notifyEntry in _notifier.List()) {
|
||||
switch (notifyEntry.Type) {
|
||||
case NotifyType.Information:
|
||||
_reportsCoordinator.Add(reportKey, ReportEntryType.Information, notifyEntry.Message.Text);
|
||||
break;
|
||||
case NotifyType.Warning:
|
||||
_reportsCoordinator.Add(reportKey, ReportEntryType.Warning, notifyEntry.Message.Text);
|
||||
break;
|
||||
case NotifyType.Error:
|
||||
default:
|
||||
_reportsCoordinator.Add(reportKey, ReportEntryType.Error, notifyEntry.Message.Text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return reportId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.Localization;
|
||||
using Orchard.PackageManager.Events;
|
||||
using Orchard.Packaging.Models;
|
||||
using Orchard.Packaging.Services;
|
||||
|
||||
namespace Orchard.Packaging.Events {
|
||||
[OrchardFeature("Gallery.Updates")]
|
||||
public class ExtensionDisplayEventHandler : IExtensionDisplayEventHandler {
|
||||
private readonly IBackgroundPackageUpdateStatus _backgroundPackageUpdateStatus;
|
||||
private readonly IPackagingSourceManager _packagingSourceManager;
|
||||
private readonly IPackageUpdateService _packageUpdateService;
|
||||
|
||||
public ExtensionDisplayEventHandler(IBackgroundPackageUpdateStatus backgroundPackageUpdateStatus,
|
||||
IPackagingSourceManager packagingSourceManager,
|
||||
IPackageUpdateService packageUpdateService) {
|
||||
|
||||
_backgroundPackageUpdateStatus = backgroundPackageUpdateStatus;
|
||||
_packagingSourceManager = packagingSourceManager;
|
||||
_packageUpdateService = packageUpdateService;
|
||||
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public IEnumerable<string> Displaying(ExtensionDescriptor extensionDescriptor) {
|
||||
// Get status from background task state or directly
|
||||
_backgroundPackageUpdateStatus.Value =
|
||||
_backgroundPackageUpdateStatus.Value ??
|
||||
_packageUpdateService.GetPackagesStatus(_packagingSourceManager.GetSources());
|
||||
|
||||
UpdatePackageEntry updatePackageEntry = _backgroundPackageUpdateStatus.Value.Entries
|
||||
.Where(package => package.ExtensionsDescriptor.Id.Equals(extensionDescriptor.Id)).FirstOrDefault();
|
||||
|
||||
if (updatePackageEntry != null) {
|
||||
if (updatePackageEntry.NewVersionToInstall != null) {
|
||||
yield return T("New version available: {0}", updatePackageEntry.NewVersionToInstall.Version).ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.Events;
|
||||
|
||||
namespace Orchard.PackageManager.Events {
|
||||
public interface IExtensionDisplayEventHandler : IEventHandler {
|
||||
/// <summary>
|
||||
/// Called before an extension is displayed
|
||||
/// </summary>
|
||||
IEnumerable<string> Displaying(ExtensionDescriptor extensionDescriptor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
|
||||
namespace Orchard.Packaging.Models {
|
||||
public class PackagesStatusResult {
|
||||
public IEnumerable<UpdatePackageEntry> Entries { get; set; }
|
||||
public IEnumerable<Exception> Errors { get; set; }
|
||||
}
|
||||
|
||||
public class UpdatePackageEntry {
|
||||
public ExtensionDescriptor ExtensionsDescriptor { get; set; }
|
||||
public IList<PackagingEntry> PackageVersions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return version to install if out-of-date, null otherwise.
|
||||
/// </summary>
|
||||
public PackagingEntry NewVersionToInstall {
|
||||
get {
|
||||
PackagingEntry updateToVersion = null;
|
||||
var latestUpdate = this.PackageVersions.OrderBy(v => new Version(v.Version)).Last();
|
||||
if (new Version(latestUpdate.Version) > new Version(this.ExtensionsDescriptor.Version)) {
|
||||
updateToVersion = latestUpdate;
|
||||
}
|
||||
return updateToVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,3 +20,8 @@ Features:
|
||||
Description: Module gallery management.
|
||||
Category: Packaging
|
||||
Dependencies: Orchard.Packaging
|
||||
Gallery.Updates
|
||||
Name: Gallery Updates
|
||||
Description: Manages updates for packages.
|
||||
Category: Packaging
|
||||
Dependencies: Gallery
|
||||
|
||||
@@ -60,11 +60,15 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="AdminMenu.cs" />
|
||||
<Compile Include="Commands\PackagingCommands.cs" />
|
||||
<Compile Include="Controllers\GalleryUpdatesController.cs" />
|
||||
<Compile Include="Controllers\PackagingServicesController.cs" />
|
||||
<Compile Include="Controllers\GalleryController.cs" />
|
||||
<Compile Include="DefaultPackagingUpdater.cs" />
|
||||
<Compile Include="Events\ExtensionDisplayEventHandler.cs" />
|
||||
<Compile Include="Events\IExtensionDisplayEventHandler.cs" />
|
||||
<Compile Include="Migrations.cs" />
|
||||
<Compile Include="Models\PackagingSource.cs" />
|
||||
<Compile Include="Models\UpdatePackageEntry.cs" />
|
||||
<Compile Include="Permissions.cs" />
|
||||
<Compile Include="ResourceManifest.cs" />
|
||||
<Compile Include="Service References\GalleryServer\Reference.cs">
|
||||
@@ -72,8 +76,11 @@
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>Reference.datasvcmap</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Services\BackgroundPackageUpdateStatus.cs" />
|
||||
<Compile Include="Services\BackgroundPackageUpdateTask.cs" />
|
||||
<Compile Include="Services\ExtensionReferenceRepository.cs" />
|
||||
<Compile Include="Services\FileBaseProjectSystem.cs" />
|
||||
<Compile Include="Services\FolderUpdater.cs" />
|
||||
<Compile Include="Services\IPackageBuilder.cs" />
|
||||
<Compile Include="Services\IPackageInstaller.cs" />
|
||||
<Compile Include="Services\IPackageManager.cs" />
|
||||
@@ -84,11 +91,13 @@
|
||||
<Compile Include="Services\PackageInstaller.cs" />
|
||||
<Compile Include="Services\PackageManager.cs" />
|
||||
<Compile Include="Models\PackagingEntry.cs" />
|
||||
<Compile Include="Services\PackageUpdateManager.cs" />
|
||||
<Compile Include="Services\PackagingSourceManager.cs" />
|
||||
<Compile Include="ViewModels\PackagingAddSourceViewModel.cs" />
|
||||
<Compile Include="ViewModels\PackagingHarvestViewModel.cs" />
|
||||
<Compile Include="ViewModels\PackagingExtensionsViewModel.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ViewModels\PackagingListViewModel.cs" />
|
||||
<Compile Include="ViewModels\PackagingSourcesViewModel.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -145,6 +154,12 @@
|
||||
<ItemGroup>
|
||||
<Content Include="web.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\GalleryUpdates\ThemesUpdates.cshtml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\GalleryUpdates\ModulesUpdates.cshtml" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Packaging.Models;
|
||||
|
||||
namespace Orchard.Packaging.Services {
|
||||
public interface IBackgroundPackageUpdateStatus : ISingletonDependency {
|
||||
PackagesStatusResult Value { get; set; }
|
||||
}
|
||||
|
||||
[OrchardFeature("Gallery.Updates")]
|
||||
public class BackgroundPackageUpdateStatus : IBackgroundPackageUpdateStatus {
|
||||
public PackagesStatusResult Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Tasks;
|
||||
|
||||
namespace Orchard.Packaging.Services {
|
||||
/// <summary>
|
||||
/// Background task responsible for fetching feeds from the Gallery into the
|
||||
/// BackgroundPackageUpdateStatus singleton dependency.
|
||||
/// The purpose is to make sure we don't block the Admin panel the first time
|
||||
/// it's accessed when the PackageManager feature is enabled. The first time
|
||||
/// the panel is accessed, the list of updates will be empty. It will be non empty
|
||||
/// only if the user asks for an explicit refresh or after the first background
|
||||
/// task sweep.
|
||||
/// </summary>
|
||||
[OrchardFeature("Gallery.Updates")]
|
||||
public class BackgroundPackageUpdateTask : IBackgroundTask {
|
||||
private readonly IPackageUpdateService _packageUpdateService;
|
||||
private readonly IPackagingSourceManager _packagingSourceManager;
|
||||
private readonly IBackgroundPackageUpdateStatus _backgroundPackageUpdateStatus;
|
||||
|
||||
public BackgroundPackageUpdateTask(IPackageUpdateService packageUpdateService,
|
||||
IPackagingSourceManager packagingSourceManager,
|
||||
IBackgroundPackageUpdateStatus backgroundPackageUpdateStatus) {
|
||||
|
||||
_packageUpdateService = packageUpdateService;
|
||||
_packagingSourceManager = packagingSourceManager;
|
||||
_backgroundPackageUpdateStatus = backgroundPackageUpdateStatus;
|
||||
}
|
||||
|
||||
public void Sweep() {
|
||||
_backgroundPackageUpdateStatus.Value = _packageUpdateService.GetPackagesStatus(_packagingSourceManager.GetSources());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Packaging.Services {
|
||||
public interface IFolderUpdater : IDependency {
|
||||
void Backup(DirectoryInfo existingFolder, DirectoryInfo backupfolder);
|
||||
void Update(DirectoryInfo destinationFolder, DirectoryInfo newFolder);
|
||||
}
|
||||
|
||||
[OrchardFeature("Gallery.Updates")]
|
||||
public class FolderUpdater : IFolderUpdater {
|
||||
private readonly INotifier _notifier;
|
||||
|
||||
public class FolderContent {
|
||||
public DirectoryInfo Folder { get; set; }
|
||||
public IEnumerable<string> Files { get; set; }
|
||||
}
|
||||
|
||||
public FolderUpdater(INotifier notifier) {
|
||||
_notifier = notifier;
|
||||
T = NullLocalizer.Instance;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void Backup(DirectoryInfo existingFolder, DirectoryInfo backupfolder) {
|
||||
CopyFolder(GetFolderContent(existingFolder), backupfolder);
|
||||
}
|
||||
|
||||
public void Update(DirectoryInfo destinationFolder, DirectoryInfo newFolder) {
|
||||
var destinationContent = GetFolderContent(destinationFolder);
|
||||
var newContent = GetFolderContent(newFolder);
|
||||
|
||||
Update(destinationContent, newContent);
|
||||
}
|
||||
|
||||
private void Update(FolderContent destinationContent, FolderContent newContent) {
|
||||
// Copy files from new folder to existing folder
|
||||
foreach (var file in newContent.Files) {
|
||||
CopyFile(newContent.Folder, file, destinationContent.Folder);
|
||||
}
|
||||
|
||||
// Delete files that are in the existing folder but not in the new folder
|
||||
foreach (var file in destinationContent.Files.Except(newContent.Files, StringComparer.OrdinalIgnoreCase)) {
|
||||
var fileToDelete = new FileInfo(Path.Combine(destinationContent.Folder.FullName, file));
|
||||
try {
|
||||
fileToDelete.Delete();
|
||||
}
|
||||
catch (Exception exception) {
|
||||
for (Exception scan = exception; scan != null; scan = scan.InnerException) {
|
||||
_notifier.Warning(T("Unable to delete file \"{0}\": {1}", fileToDelete.FullName, scan.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyFolder(FolderContent source, DirectoryInfo dest) {
|
||||
foreach (var file in source.Files) {
|
||||
CopyFile(source.Folder, file, dest);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyFile(DirectoryInfo sourceFolder, string fileName, DirectoryInfo destinationFolder) {
|
||||
var sourceFile = new FileInfo(Path.Combine(sourceFolder.FullName, fileName));
|
||||
var destFile = new FileInfo(Path.Combine(destinationFolder.FullName, fileName));
|
||||
|
||||
// If destination file exist, overwrite only if changed
|
||||
if (destFile.Exists) {
|
||||
if (sourceFile.Length == destFile.Length) {
|
||||
var source = File.ReadAllBytes(sourceFile.FullName);
|
||||
var dest = File.ReadAllBytes(destFile.FullName);
|
||||
if (source.SequenceEqual(dest)) {
|
||||
//_notifier.Information(T("Skipping file \"{0}\" because it is the same content as the source file", destFile.FullName));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create destination directory
|
||||
if (!destFile.Directory.Exists) {
|
||||
destFile.Directory.Create();
|
||||
}
|
||||
|
||||
File.Copy(sourceFile.FullName, destFile.FullName, true);
|
||||
}
|
||||
|
||||
private FolderContent GetFolderContent(DirectoryInfo folder) {
|
||||
var files = new List<string>();
|
||||
GetFolderContent(folder, "", files);
|
||||
return new FolderContent { Folder = folder, Files = files };
|
||||
}
|
||||
|
||||
private void GetFolderContent(DirectoryInfo folder, string prefix, List<string> files) {
|
||||
if (!folder.Exists)
|
||||
return;
|
||||
|
||||
foreach (var file in folder.GetFiles()) {
|
||||
files.Add(Path.Combine(prefix, file.Name));
|
||||
}
|
||||
|
||||
foreach (var child in folder.GetDirectories()) {
|
||||
GetFolderContent(child, Path.Combine(prefix, child.Name), files);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Orchard.Caching;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Environment.Extensions.Models;
|
||||
using Orchard.FileSystems.VirtualPath;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Packaging.Models;
|
||||
using Orchard.Services;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.Packaging.Services {
|
||||
public interface IPackageUpdateService : IDependency {
|
||||
PackagesStatusResult GetPackagesStatus(IEnumerable<PackagingSource> sources);
|
||||
void TriggerRefresh();
|
||||
void Update(PackagingEntry entry);
|
||||
void Uninstall(string packageId);
|
||||
}
|
||||
|
||||
[OrchardFeature("Gallery.Updates")]
|
||||
public class PackageUpdateService : IPackageUpdateService {
|
||||
private readonly IPackagingSourceManager _packagingSourceManager;
|
||||
private readonly IExtensionManager _extensionManager;
|
||||
private readonly ICacheManager _cacheManager;
|
||||
private readonly IClock _clock;
|
||||
private readonly ISignals _signals;
|
||||
private readonly INotifier _notifier;
|
||||
private readonly IVirtualPathProvider _virtualPathProvider;
|
||||
private readonly IPackageManager _packageManager;
|
||||
private readonly IFolderUpdater _folderUpdater;
|
||||
|
||||
public PackageUpdateService(IPackagingSourceManager packagingSourceManager,
|
||||
IExtensionManager extensionManager,
|
||||
ICacheManager cacheManager,
|
||||
IClock clock,
|
||||
ISignals signals,
|
||||
INotifier notifier,
|
||||
IVirtualPathProvider virtualPathProvider,
|
||||
IPackageManager packageManager,
|
||||
IFolderUpdater folderUpdater) {
|
||||
|
||||
_packagingSourceManager = packagingSourceManager;
|
||||
_extensionManager = extensionManager;
|
||||
_cacheManager = cacheManager;
|
||||
_clock = clock;
|
||||
_signals = signals;
|
||||
_notifier = notifier;
|
||||
_virtualPathProvider = virtualPathProvider;
|
||||
_packageManager = packageManager;
|
||||
_folderUpdater = folderUpdater;
|
||||
T = NullLocalizer.Instance;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public PackagesStatusResult GetPackagesStatus(IEnumerable<PackagingSource> sources) {
|
||||
var result = new PackagesStatusResult {
|
||||
Entries = new List<UpdatePackageEntry>(),
|
||||
Errors = new List<Exception>()
|
||||
};
|
||||
|
||||
foreach (var source in sources) {
|
||||
var sourceResult = GetPackages(source);
|
||||
result.Entries = result.Entries.Concat(sourceResult.Entries);
|
||||
result.Errors = result.Errors.Concat(sourceResult.Errors);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void TriggerRefresh() {
|
||||
_signals.Trigger("PackageUpdateService");
|
||||
}
|
||||
|
||||
private PackagesStatusResult GetPackages(PackagingSource packagingSource) {
|
||||
return _cacheManager.Get(packagingSource.FeedUrl, ctx => {
|
||||
// Refresh every minute or when signal was triggered
|
||||
ctx.Monitor(_clock.When(TimeSpan.FromMinutes(5)));
|
||||
ctx.Monitor(_signals.When("PackageUpdateService"));
|
||||
|
||||
// We cache exception because we are calling on a network feed, and failure may
|
||||
// take quite some time.
|
||||
var result = new PackagesStatusResult {
|
||||
Entries = new List<UpdatePackageEntry>(),
|
||||
Errors = new List<Exception>()
|
||||
};
|
||||
try {
|
||||
result.Entries = GetPackagesWorker(packagingSource);
|
||||
}
|
||||
catch (Exception e) {
|
||||
result.Errors = new[] { e };
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private IEnumerable<UpdatePackageEntry> GetPackagesWorker(PackagingSource packagingSource) {
|
||||
var list = new Dictionary<string, UpdatePackageEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var extensions = _extensionManager.AvailableExtensions();
|
||||
foreach (var extension in extensions) {
|
||||
var packageId = PackageBuilder.BuildPackageId(extension.Id, extension.ExtensionType);
|
||||
|
||||
GetOrAddEntry(list, packageId).ExtensionsDescriptor = extension;
|
||||
}
|
||||
|
||||
var packages = _packagingSourceManager.GetExtensionList(packagingSource)
|
||||
.ToList()
|
||||
.GroupBy(p => p.PackageId, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var package in packages) {
|
||||
var entry = GetOrAddEntry(list, package.Key);
|
||||
entry.PackageVersions = entry.PackageVersions.Concat(package).ToList();
|
||||
}
|
||||
|
||||
return list.Values.Where(e => e.ExtensionsDescriptor != null && e.PackageVersions.Any());
|
||||
}
|
||||
|
||||
private UpdatePackageEntry GetOrAddEntry(Dictionary<string, UpdatePackageEntry> list, string packageId) {
|
||||
UpdatePackageEntry entry;
|
||||
if (!list.TryGetValue(packageId, out entry)) {
|
||||
entry = new UpdatePackageEntry { PackageVersions = new List<PackagingEntry>() };
|
||||
list.Add(packageId, entry);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
public class UpdateContext {
|
||||
public PackagingEntry PackagingEntry { get; set; }
|
||||
public bool IsTheme {
|
||||
get {
|
||||
return PackagingEntry.PackageId.StartsWith(PackagingSourceManager.GetExtensionPrefix(DefaultExtensionTypes.Theme));
|
||||
}
|
||||
}
|
||||
public string ExtensionFolder {
|
||||
get { return IsTheme ? "Themes" : "Modules"; }
|
||||
}
|
||||
public string ExtensionId {
|
||||
get {
|
||||
return IsTheme ?
|
||||
PackagingEntry.PackageId.Substring(PackagingSourceManager.GetExtensionPrefix(DefaultExtensionTypes.Theme).Length) :
|
||||
PackagingEntry.PackageId.Substring(PackagingSourceManager.GetExtensionPrefix(DefaultExtensionTypes.Module).Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(PackagingEntry entry) {
|
||||
var context = new UpdateContext { PackagingEntry = entry };
|
||||
|
||||
// 1. Backup extension folder
|
||||
try {
|
||||
BackupExtensionFolder(context.ExtensionFolder, context.ExtensionId);
|
||||
}
|
||||
catch (Exception exception) {
|
||||
throw new OrchardException(T("Unable to backup existing local package directory."), exception);
|
||||
}
|
||||
|
||||
// 2. If extension is installed, need to un-install first
|
||||
try {
|
||||
UninstallExtensionIfNeeded(context);
|
||||
}
|
||||
catch (Exception exception) {
|
||||
throw new OrchardException(T("Unable to un-install local package before updating."), exception);
|
||||
}
|
||||
|
||||
// 3. Install package from Gallery to temporary folder
|
||||
DirectoryInfo newPackageFolder;
|
||||
try {
|
||||
newPackageFolder = InstallPackage(context);
|
||||
}
|
||||
catch (Exception exception) {
|
||||
throw new OrchardException(T("Package installation failed."), exception);
|
||||
}
|
||||
|
||||
// 4. Copy new package content to extension folder
|
||||
try {
|
||||
UpdateExtensionFolder(context, newPackageFolder);
|
||||
}
|
||||
catch (Exception exception) {
|
||||
throw new OrchardException(T("Package update failed."), exception);
|
||||
}
|
||||
}
|
||||
|
||||
public void Uninstall(string packageId) {
|
||||
var context = new UpdateContext { PackagingEntry = new PackagingEntry {PackageId = packageId} };
|
||||
|
||||
// Backup extension folder
|
||||
try {
|
||||
BackupExtensionFolder(context.ExtensionFolder, context.ExtensionId);
|
||||
}
|
||||
catch (Exception exception) {
|
||||
throw new OrchardException(T("Unable to backup existing local package directory."), exception);
|
||||
}
|
||||
|
||||
// Uninstall package from local folder
|
||||
_packageManager.Uninstall(packageId, _virtualPathProvider.MapPath("~/"));
|
||||
_notifier.Information(T("Successfully un-installed local package {0}", packageId));
|
||||
}
|
||||
|
||||
private void BackupExtensionFolder(string extensionFolder, string extensionId) {
|
||||
var tempPath = _virtualPathProvider.Combine("~", extensionFolder, "_Backup", extensionId);
|
||||
string localTempPath = null;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
localTempPath = _virtualPathProvider.MapPath(tempPath) + (i == 0 ? "" : "." + i.ToString());
|
||||
if (!Directory.Exists(localTempPath)) {
|
||||
Directory.CreateDirectory(localTempPath);
|
||||
break;
|
||||
}
|
||||
localTempPath = null;
|
||||
}
|
||||
|
||||
if (localTempPath == null) {
|
||||
throw new OrchardException(T("Backup folder {0} has too many backups subfolder (limit is 1,000)", tempPath));
|
||||
}
|
||||
|
||||
var backupFolder = new DirectoryInfo(localTempPath);
|
||||
var source = new DirectoryInfo(_virtualPathProvider.MapPath(_virtualPathProvider.Combine("~", extensionFolder, extensionId)));
|
||||
_folderUpdater.Backup(source, backupFolder);
|
||||
_notifier.Information(T("Successfully backed up local package to local folder \"{0}\"", backupFolder));
|
||||
}
|
||||
|
||||
private void UninstallExtensionIfNeeded(UpdateContext context) {
|
||||
// Nuget requires to un-install the currently installed packages if the new
|
||||
// package is the same version or an older version
|
||||
var extension = _extensionManager
|
||||
.AvailableExtensions()
|
||||
.Where(e => e.Id == context.ExtensionId && new Version(e.Version) >= new Version(context.PackagingEntry.Version))
|
||||
.FirstOrDefault();
|
||||
if (extension == null)
|
||||
return;
|
||||
|
||||
_packageManager.Uninstall(context.PackagingEntry.PackageId, _virtualPathProvider.MapPath("~/"));
|
||||
_notifier.Information(T("Successfully un-installed local package {0}", context.ExtensionId));
|
||||
}
|
||||
|
||||
private DirectoryInfo InstallPackage(UpdateContext context) {
|
||||
var tempPath = _virtualPathProvider.Combine("~", context.ExtensionFolder, "_Updates");
|
||||
var destPath = _virtualPathProvider.Combine(tempPath, context.ExtensionFolder, context.ExtensionId);
|
||||
var localDestPath = (_virtualPathProvider.MapPath(destPath));
|
||||
if (Directory.Exists(localDestPath)) {
|
||||
Directory.Delete(localDestPath, true);
|
||||
}
|
||||
_packageManager.Install(context.PackagingEntry.PackageId, context.PackagingEntry.Version, context.PackagingEntry.Source.FeedUrl, _virtualPathProvider.MapPath(tempPath));
|
||||
return new DirectoryInfo(localDestPath);
|
||||
}
|
||||
|
||||
private void UpdateExtensionFolder(UpdateContext context, DirectoryInfo newPackageFolder) {
|
||||
var extensionPath = _virtualPathProvider.Combine("~", context.ExtensionFolder, context.ExtensionId);
|
||||
var extensionFolder = new DirectoryInfo(_virtualPathProvider.MapPath(extensionPath));
|
||||
|
||||
_folderUpdater.Update(extensionFolder, newPackageFolder);
|
||||
|
||||
_notifier.Information(T("Successfully installed package \"{0}\" to local folder \"{1}\"", context.ExtensionId, extensionFolder));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Packaging.Models;
|
||||
|
||||
namespace Orchard.Packaging.ViewModels {
|
||||
public class PackagingListViewModel {
|
||||
public IEnumerable<UpdatePackageEntry> Entries { get; set; }
|
||||
public dynamic Pager { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
@using Orchard.Modules.Extensions
|
||||
@using Orchard.Mvc.Html;
|
||||
@using Orchard.Packaging.ViewModels;
|
||||
@using Orchard.Packaging.Services;
|
||||
@using Orchard.Packaging.Models;
|
||||
@using Orchard.Environment.Extensions.Models;
|
||||
@using Orchard.Utility.Extensions;
|
||||
@model PackagingListViewModel
|
||||
|
||||
@{
|
||||
Style.Require("PackagingAdmin");
|
||||
|
||||
Layout.Title = T("Modules").ToString();
|
||||
}
|
||||
|
||||
@functions {
|
||||
public string InstallAction(PackagingEntry package) {
|
||||
return Url.Action("Install", "GalleryUpdates", new {
|
||||
area = "Orchard.Packaging",
|
||||
packageId = package.PackageId,
|
||||
version = package.Version,
|
||||
sourceId = package.Source.Id,
|
||||
returnUrl = "ModulesUpdates"
|
||||
});
|
||||
}
|
||||
public string UninstallAction(PackagingEntry package) {
|
||||
return Url.Action("Uninstall", "GalleryUpdates", new {
|
||||
area = "Orchard.Packaging",
|
||||
packageId = package.PackageId,
|
||||
returnUrl = "ModulesUpdates"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.Entries.Count() <= 0) {
|
||||
<p>No package updates available.</p>
|
||||
} else {
|
||||
<ul class="contentItems">
|
||||
@foreach (var module in Model.Entries) {
|
||||
<li>
|
||||
@{
|
||||
string iconUrl = @module.NewVersionToInstall.IconUrl;
|
||||
if (string.IsNullOrWhiteSpace(iconUrl)) {
|
||||
iconUrl = Href("../../Content/Images/ModuleDefaultIcon.png");
|
||||
}
|
||||
}
|
||||
|
||||
<div class="iconThumbnail">
|
||||
<div class="extensionDetails column">
|
||||
<div class="extensionName">
|
||||
@if (!string.IsNullOrWhiteSpace(module.NewVersionToInstall.GalleryDetailsUrl)) {
|
||||
<a href="@module.NewVersionToInstall.GalleryDetailsUrl">
|
||||
<h2>@module.NewVersionToInstall.Title<span> - @T("Version: {0}", module.NewVersionToInstall.Version)</span></h2>
|
||||
</a>
|
||||
} else {
|
||||
<h2>@module.NewVersionToInstall.Title<span> - @T("Version: {0}", module.NewVersionToInstall.Version)</span></h2>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="related">
|
||||
@Html.Link(T("Install Latest").Text, InstallAction(module.NewVersionToInstall))@T(" | ")
|
||||
<a href="@module.NewVersionToInstall.PackageStreamUri">@T("Download")</a>
|
||||
</div>
|
||||
|
||||
<div class="properties">
|
||||
<p>@(module.NewVersionToInstall.Description == null ? T("(No description").Text : module.NewVersionToInstall.Description)</p>
|
||||
<ul class="pageStatus">
|
||||
<li>@T("Last Updated: {0}", module.NewVersionToInstall.LastUpdated)</li>
|
||||
<li> | @T("Author: {0}", !string.IsNullOrEmpty(module.NewVersionToInstall.Authors) ? module.NewVersionToInstall.Authors : T("Unknown").ToString())</li>
|
||||
<li> | @T("Downloads: {0}", module.NewVersionToInstall.DownloadCount)</li>
|
||||
<li> | @T("Website: ")
|
||||
@if (!string.IsNullOrEmpty(module.NewVersionToInstall.ProjectUrl)) { <a href="@module.NewVersionToInstall.ProjectUrl">@module.NewVersionToInstall.ProjectUrl</a> } else { @T("Unknown").ToString() }
|
||||
</li>
|
||||
<li><div> | @T("Rating: ")
|
||||
<div class="ratings" style="width:@(15 * 5)px" title="@T("Ratings: {0} ({1})", module.NewVersionToInstall.Rating, module.NewVersionToInstall.RatingsCount)">
|
||||
<div class="score" style="width:@(15 * (module.NewVersionToInstall.Rating))px"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="extensionThumbnail column">
|
||||
<img src="@iconUrl" class="thumbnail" alt="module" />
|
||||
</div>
|
||||
</div>
|
||||
</li>}
|
||||
</ul>
|
||||
|
||||
@Display(Model.Pager)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
@using Orchard.Modules.Extensions
|
||||
@using Orchard.Mvc.Html;
|
||||
@using Orchard.Packaging.ViewModels;
|
||||
@using Orchard.Packaging.Services;
|
||||
@using Orchard.Packaging.Models;
|
||||
@using Orchard.Environment.Extensions.Models;
|
||||
@using Orchard.Utility.Extensions;
|
||||
@model PackagingListViewModel
|
||||
|
||||
@{
|
||||
Style.Require("PackagingAdmin");
|
||||
|
||||
Layout.Title = T("Themes").ToString();
|
||||
}
|
||||
|
||||
@functions {
|
||||
public string InstallAction(PackagingEntry package) {
|
||||
return Url.Action("Install", "GalleryUpdates", new {
|
||||
area = "Orchard.Packaging",
|
||||
packageId = package.PackageId,
|
||||
version = package.Version,
|
||||
sourceId = package.Source.Id,
|
||||
returnUrl = "ThemesUpdates"
|
||||
});
|
||||
}
|
||||
public string UninstallAction(PackagingEntry package) {
|
||||
return Url.Action("Uninstall", "GalleryUpdates", new {
|
||||
area = "Orchard.Packaging",
|
||||
packageId = package.PackageId,
|
||||
returnUrl = "ThemesUpdates"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.Entries.Count() <= 0) {
|
||||
<p>No package updates available.</p>
|
||||
} else {
|
||||
<ul class="contentItems theme">
|
||||
@foreach (var theme in Model.Entries) {
|
||||
<li>
|
||||
@{
|
||||
string extensionClass = "iconThumbnail";
|
||||
string iconUrl = @theme.NewVersionToInstall.IconUrl;
|
||||
if (!string.IsNullOrWhiteSpace(@theme.NewVersionToInstall.FirstScreenshot)) {
|
||||
iconUrl = @theme.NewVersionToInstall.FirstScreenshot;
|
||||
extensionClass = "screenshotThumbnail";
|
||||
} else if (string.IsNullOrWhiteSpace(iconUrl)) {
|
||||
iconUrl = Href("../../Content/Images/imagePlaceholder.png");
|
||||
extensionClass = "screenshotThumbnail";
|
||||
}
|
||||
}
|
||||
|
||||
<div class="@extensionClass">
|
||||
<div class="extensionDetails column">
|
||||
<div class="extensionName">
|
||||
@if (!string.IsNullOrWhiteSpace(theme.NewVersionToInstall.GalleryDetailsUrl)) {
|
||||
<a href="@theme.NewVersionToInstall.GalleryDetailsUrl">
|
||||
<h2>@theme.NewVersionToInstall.Title<span> - @T("Version: {0}", theme.NewVersionToInstall.Version)</span></h2>
|
||||
</a>
|
||||
} else {
|
||||
<h2>@theme.NewVersionToInstall.Title<span> - @T("Version: {0}", theme.NewVersionToInstall.Version)</span></h2>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="related">
|
||||
@Html.Link(T("Install Latest").Text, InstallAction(theme.NewVersionToInstall))@T(" | ")
|
||||
<a href="@theme.NewVersionToInstall.PackageStreamUri">@T("Download")</a>
|
||||
</div>
|
||||
|
||||
<div class="properties">
|
||||
<p>@(theme.NewVersionToInstall.Description == null ? T("(No description").Text : theme.NewVersionToInstall.Description)</p>
|
||||
<ul class="pageStatus">
|
||||
<li>@T("Last Updated: {0}", theme.NewVersionToInstall.LastUpdated)</li>
|
||||
<li> | @T("Author: {0}", !string.IsNullOrEmpty(theme.NewVersionToInstall.Authors) ? theme.NewVersionToInstall.Authors : T("Unknown").ToString())</li>
|
||||
<li> | @T("Downloads: {0}", theme.NewVersionToInstall.DownloadCount)</li>
|
||||
<li> | @T("Website: ")
|
||||
@if (!string.IsNullOrEmpty(theme.NewVersionToInstall.ProjectUrl)) { <a href="@theme.NewVersionToInstall.ProjectUrl">@theme.NewVersionToInstall.ProjectUrl</a> } else { @T("Unknown").ToString() }
|
||||
</li>
|
||||
<li><div> | @T("Rating: ")
|
||||
<div class="ratings" style="width:@(15 * 5)px" title="@T("Ratings: {0} ({1})", theme.NewVersionToInstall.Rating, theme.NewVersionToInstall.RatingsCount)">
|
||||
<div class="score" style="width:@(15 * (theme.NewVersionToInstall.Rating))px"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="extensionThumbnail column">
|
||||
<img src="@iconUrl" class="thumbnail" alt="theme" />
|
||||
</div>
|
||||
</div>
|
||||
</li>}
|
||||
</ul>
|
||||
|
||||
@Display(Model.Pager)
|
||||
}
|
||||
Reference in New Issue
Block a user