diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index 8929922320..78a1ff4ccf 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -1055,6 +1055,7 @@ namespace Umbraco.Core.Services
uow.Commit();
//Special case for the Upload DataType
+ //TODO: Should we handle this with events?
var uploadDataTypeId = new Guid(Constants.PropertyEditors.UploadField);
if (content.Properties.Any(x => x.PropertyType.DataTypeId == uploadDataTypeId))
{
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/edit.html b/src/Umbraco.Web.UI.Client/src/views/media/edit.html
index 8ff21f8015..dc4fe83b0c 100644
--- a/src/Umbraco.Web.UI.Client/src/views/media/edit.html
+++ b/src/Umbraco.Web.UI.Client/src/views/media/edit.html
@@ -6,23 +6,12 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/mediaedit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/mediaedit.controller.js
index 3fa3120372..76f9eaa52e 100644
--- a/src/Umbraco.Web.UI.Client/src/views/media/mediaedit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/media/mediaedit.controller.js
@@ -1,4 +1,4 @@
-function mediaEditController($scope, $routeParams, mediaResource, notificationsService) {
+function mediaEditController($scope, $routeParams, mediaResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper) {
if ($routeParams.create) {
@@ -13,6 +13,13 @@ function mediaEditController($scope, $routeParams, mediaResource, notificationsS
.then(function (data) {
$scope.contentLoaded = true;
$scope.content = data;
+
+ //in one particular special case, after we've created a new item we redirect back to the edit
+ // route but there might be server validation errors in the collection which we need to display
+ // after the redirect, so we will bind all subscriptions which will show the server validation errors
+ // if there are any and then clear them so the collection no longer persists them.
+ serverValidationManager.executeAndClearAllSubscriptions();
+
});
}
@@ -28,22 +35,27 @@ function mediaEditController($scope, $routeParams, mediaResource, notificationsS
}
};
- //TODO: Clean this up and share this code with the content editor
- $scope.saveAndPublish = function (cnt) {
- mediaResource.saveMedia(cnt, $routeParams.create, $scope.files)
- .then(function (data) {
- $scope.content = data;
- notificationsService.success("Published", "Media has been saved and published");
- });
- };
+ //ensure there is a form object assigned.
+ var currentForm = angularHelper.getRequiredCurrentForm($scope);
+
+ $scope.save = function (cnt) {
+
+ $scope.$broadcast("saving", { scope: $scope });
+
+ //don't continue if the form is invalid
+ if (currentForm.$invalid) return;
+
+ serverValidationManager.reset();
- //TODO: Clean this up and share this code with the content editor
- $scope.save = function (cnt) {
mediaResource.saveMedia(cnt, $routeParams.create, $scope.files)
.then(function (data) {
- $scope.content = data;
- notificationsService.success("Saved", "Media has been saved");
- });
+ contentEditingHelper.handleSuccessfulSave({
+ scope: $scope,
+ newContent: data
+ });
+ }, function (err) {
+ contentEditingHelper.handleSaveError(err, $scope);
+ });
};
}
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
index 8923314534..b5eda0d3c8 100644
--- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
@@ -777,7 +777,7 @@ To manage your website, simply open the umbraco back office and start adding con
Sent For Approval
Changes have been sent for approval
Media saved
-
+ Media saved without any errors
Member saved
Stylesheet Property Saved
Stylesheet saved
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
index e56017416b..0cdd9893bf 100644
--- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
@@ -763,7 +763,7 @@ To manage your website, simply open the umbraco back office and start adding con
Sent For Approval
Changes have been sent for approval
Media saved
-
+ Media saved without any errors
Member saved
Stylesheet Property Saved
Stylesheet saved
diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs
index be8cff37e7..55b3edc9fc 100644
--- a/src/Umbraco.Web/Editors/ContentController.cs
+++ b/src/Umbraco.Web/Editors/ContentController.cs
@@ -22,11 +22,94 @@ using umbraco;
namespace Umbraco.Web.Editors
{
+ public abstract class ContentControllerBase : UmbracoAuthorizedJsonController
+ {
+ ///
+ /// Constructor
+ ///
+ protected ContentControllerBase()
+ : this(UmbracoContext.Current)
+ {
+ }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ protected ContentControllerBase(UmbracoContext umbracoContext)
+ : base(umbracoContext)
+ {
+ }
+
+ protected void HandleContentNotFound(int id)
+ {
+ ModelState.AddModelError("id", string.Format("content with id: {0} was not found", id));
+ var errorResponse = Request.CreateErrorResponse(
+ HttpStatusCode.NotFound,
+ ModelState);
+ throw new HttpResponseException(errorResponse);
+ }
+
+ protected void UpdateName(ContentItemSave contentItem)
+ where TPersisted : IContentBase
+ {
+ //Don't update the name if it is empty
+ if (!contentItem.Name.IsNullOrWhiteSpace())
+ {
+ contentItem.PersistedContent.Name = contentItem.Name;
+ }
+ }
+
+ protected void MapPropertyValues(ContentItemSave contentItem)
+ where TPersisted : IContentBase
+ {
+ //Map the property values
+ foreach (var p in contentItem.ContentDto.Properties)
+ {
+ //get the dbo property
+ var dboProperty = contentItem.PersistedContent.Properties[p.Alias];
+
+ //create the property data to send to the property editor
+ var d = new Dictionary();
+ //add the files if any
+ var files = contentItem.UploadedFiles.Where(x => x.PropertyId == p.Id).ToArray();
+ if (files.Any())
+ {
+ d.Add("files", files);
+ }
+ var data = new ContentPropertyData(p.Value, d);
+
+ //get the deserialized value from the property editor
+ if (p.PropertyEditor == null)
+ {
+ LogHelper.Warn("No property editor found for property " + p.Alias);
+ }
+ else
+ {
+ dboProperty.Value = p.PropertyEditor.ValueEditor.DeserializeValue(data, dboProperty.Value);
+ }
+ }
+ }
+
+ protected void HandleInvalidModelState(ContentItemDisplayBase display)
+ where TPersisted : IContentBase
+ where T : ContentPropertyBasic
+ {
+ //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
+ if (!ModelState.IsValid)
+ {
+ display.Errors = ModelState.ToErrorDictionary();
+ throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Forbidden, display));
+ }
+ }
+
+ }
+
///
/// The API controller used for editing content
///
[PluginController("UmbracoApi")]
- public class ContentController : UmbracoAuthorizedJsonController
+ public class ContentController : ContentControllerBase
{
private readonly ContentModelMapper _contentModelMapper;
@@ -66,11 +149,7 @@ namespace Umbraco.Web.Editors
var foundContent = Services.ContentService.GetById(id);
if (foundContent == null)
{
- ModelState.AddModelError("id", string.Format("content with id: {0} was not found", id));
- var errorResponse = Request.CreateErrorResponse(
- HttpStatusCode.NotFound,
- ModelState);
- throw new HttpResponseException(errorResponse);
+ HandleContentNotFound(id);
}
return _contentModelMapper.ToContentItemDisplay(foundContent);
}
@@ -107,43 +186,14 @@ namespace Umbraco.Web.Editors
// * and validated
// * any file attachments have been saved to their temporary location for us to use
// * we have a reference to the DTO object and the persisted object
-
- //Don't update the name if it is empty
- if (!contentItem.Name.IsNullOrWhiteSpace())
- {
- contentItem.PersistedContent.Name = contentItem.Name;
- }
+
+ UpdateName(contentItem);
//TODO: We need to support 'send to publish'
//TODO: We'll need to save the new template, publishat, etc... values here
- //Map the property values
- foreach (var p in contentItem.ContentDto.Properties)
- {
- //get the dbo property
- var dboProperty = contentItem.PersistedContent.Properties[p.Alias];
-
- //create the property data to send to the property editor
- var d = new Dictionary();
- //add the files if any
- var files = contentItem.UploadedFiles.Where(x => x.PropertyId == p.Id).ToArray();
- if (files.Any())
- {
- d.Add("files", files);
- }
- var data = new ContentPropertyData(p.Value, d);
-
- //get the deserialized value from the property editor
- if (p.PropertyEditor == null)
- {
- LogHelper.Warn("No property editor found for property " + p.Alias);
- }
- else
- {
- dboProperty.Value = p.PropertyEditor.ValueEditor.DeserializeValue(data, dboProperty.Value);
- }
- }
+ MapPropertyValues(contentItem);
//We need to manually check the validation results here because:
// * We still need to save the entity even if there are validation value errors
@@ -191,12 +241,9 @@ namespace Umbraco.Web.Editors
//return the updated model
var display = _contentModelMapper.ToContentItemDisplay(contentItem.PersistedContent);
+
//lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
- if (!ModelState.IsValid)
- {
- display.Errors = ModelState.ToErrorDictionary();
- throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Forbidden, display));
- }
+ HandleInvalidModelState(display);
//put the correct msgs in
switch (contentItem.Action)
diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs
index 5e9dc789e7..7ea0df7f7e 100644
--- a/src/Umbraco.Web/Editors/MediaController.cs
+++ b/src/Umbraco.Web/Editors/MediaController.cs
@@ -4,6 +4,7 @@ using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.ModelBinding;
+using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
@@ -14,6 +15,7 @@ using Umbraco.Web.WebApi;
using System.Linq;
using Umbraco.Web.WebApi.Binders;
using Umbraco.Web.WebApi.Filters;
+using umbraco;
namespace Umbraco.Web.Editors
{
@@ -30,7 +32,7 @@ namespace Umbraco.Web.Editors
//}
[PluginController("UmbracoApi")]
- public class MediaController : UmbracoAuthorizedJsonController
+ public class MediaController : ContentControllerBase
{
private readonly MediaModelMapper _mediaModelMapper;
@@ -67,7 +69,7 @@ namespace Umbraco.Web.Editors
throw new HttpResponseException(HttpStatusCode.NotFound);
}
- var emptyContent = new Umbraco.Core.Models.Media("Empty", parentId, contentType);
+ var emptyContent = new Core.Models.Media("Empty", parentId, contentType);
return _mediaModelMapper.ToMediaItemDisplay(emptyContent);
}
@@ -81,11 +83,7 @@ namespace Umbraco.Web.Editors
var foundContent = Services.MediaService.GetById(id);
if (foundContent == null)
{
- ModelState.AddModelError("id", string.Format("media with id: {0} was not found", id));
- var errorResponse = Request.CreateErrorResponse(
- HttpStatusCode.NotFound,
- ModelState);
- throw new HttpResponseException(errorResponse);
+ HandleContentNotFound(id);
}
return _mediaModelMapper.ToMediaItemDisplay(foundContent);
}
@@ -123,35 +121,26 @@ namespace Umbraco.Web.Editors
// * any file attachments have been saved to their temporary location for us to use
// * we have a reference to the DTO object and the persisted object
- //Now, we just need to save the data
+ UpdateName(contentItem);
- contentItem.PersistedContent.Name = contentItem.Name;
- //TODO: We'll need to save the new template, publishat, etc... values here
+ MapPropertyValues(contentItem);
- //Save the property values (for properties that have a valid editor ... not legacy)
- foreach (var p in contentItem.ContentDto.Properties.Where(x => x.PropertyEditor != null))
+ //We need to manually check the validation results here because:
+ // * We still need to save the entity even if there are validation value errors
+ // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null)
+ // then we cannot continue saving, we can only display errors
+ // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display
+ // a message indicating this
+ if (!ModelState.IsValid)
{
- //get the dbo property
- var dboProperty = contentItem.PersistedContent.Properties[p.Alias];
-
- //create the property data to send to the property editor
- var d = new Dictionary();
- //add the files if any
- var files = contentItem.UploadedFiles.Where(x => x.PropertyId == p.Id).ToArray();
- if (files.Any())
+ if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem)
+ && (contentItem.Action == ContentSaveAction.SaveNew))
{
- d.Add("files", files);
- }
- var data = new ContentPropertyData(p.Value, d);
-
- //get the deserialized value from the property editor
- if (p.PropertyEditor == null)
- {
- LogHelper.Warn("No property editor found for property " + p.Alias);
- }
- else
- {
- dboProperty.Value = p.PropertyEditor.ValueEditor.DeserializeValue(data, dboProperty.Value);
+ //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
+ // add the modelstate to the outgoing object and throw a 403
+ var forDisplay = _mediaModelMapper.ToMediaItemDisplay(contentItem.PersistedContent);
+ forDisplay.Errors = ModelState.ToErrorDictionary();
+ throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Forbidden, forDisplay));
}
}
@@ -159,7 +148,21 @@ namespace Umbraco.Web.Editors
Services.MediaService.Save(contentItem.PersistedContent);
//return the updated model
- return _mediaModelMapper.ToMediaItemDisplay(contentItem.PersistedContent);
+ var display = _mediaModelMapper.ToMediaItemDisplay(contentItem.PersistedContent);
+
+ //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403
+ HandleInvalidModelState(display);
+
+ //put the correct msgs in
+ switch (contentItem.Action)
+ {
+ case ContentSaveAction.Save:
+ case ContentSaveAction.SaveNew:
+ display.AddSuccessNotification(ui.Text("speechBubbles", "editMediaSaved"), ui.Text("speechBubbles", "editMediaSavedText"));
+ break;
+ }
+
+ return display;
}
}
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
index 4e4f403760..e08540b242 100644
--- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.Serialization;
@@ -13,33 +12,11 @@ namespace Umbraco.Web.Models.ContentEditing
/// A model representing a content item to be displayed in the back office
///
[DataContract(Name = "content", Namespace = "")]
- public class ContentItemDisplay : TabbedContentItem, INotificationModel
+ public class ContentItemDisplay : ContentItemDisplayBase
{
- public ContentItemDisplay()
- {
- Notifications = new List();
- }
-
[DataMember(Name = "publishDate")]
public DateTime? PublishDate { get; set; }
-
- ///
- /// This is used for validation of a content item.
- ///
- ///
- /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will
- /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the
- /// updated model.
- ///
- [DataMember(Name = "modelState")]
- public IDictionary Errors { get; set; }
-
- ///
- /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes.
- ///
- [DataMember(Name = "notifications")]
- public List Notifications { get; private set; }
-
+
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs
new file mode 100644
index 0000000000..c696aeb675
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using Umbraco.Core.Models;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ public abstract class ContentItemDisplayBase : TabbedContentItem, INotificationModel, IErrorModel
+ where T : ContentPropertyBasic
+ where TPersisted : IContentBase
+ {
+ protected ContentItemDisplayBase()
+ {
+ Notifications = new List();
+ }
+
+ ///
+ /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes.
+ ///
+ [DataMember(Name = "notifications")]
+ public List Notifications { get; private set; }
+
+ ///
+ /// This is used for validation of a content item.
+ ///
+ ///
+ /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will
+ /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the
+ /// updated model.
+ ///
+ [DataMember(Name = "modelState")]
+ public IDictionary Errors { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/IErrorModel.cs b/src/Umbraco.Web/Models/ContentEditing/IErrorModel.cs
new file mode 100644
index 0000000000..b56ae528b4
--- /dev/null
+++ b/src/Umbraco.Web/Models/ContentEditing/IErrorModel.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Web.Models.ContentEditing
+{
+ public interface IErrorModel
+ {
+ ///
+ /// This is used for validation of a content item.
+ ///
+ ///
+ /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will
+ /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the
+ /// updated model.
+ ///
+ IDictionary Errors { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs
index 956194034b..d72fd84c33 100644
--- a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs
@@ -8,7 +8,7 @@ namespace Umbraco.Web.Models.ContentEditing
/// A model representing a content item to be displayed in the back office
///
[DataContract(Name = "content", Namespace = "")]
- public class MediaItemDisplay : TabbedContentItem
+ public class MediaItemDisplay : ContentItemDisplayBase
{
}
diff --git a/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs b/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs
index 6e1411f62e..b18118d8c2 100644
--- a/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs
+++ b/src/Umbraco.Web/Models/ContentEditing/TabbedContentItem.cs
@@ -8,7 +8,8 @@ using Umbraco.Core.Models;
namespace Umbraco.Web.Models.ContentEditing
{
public abstract class TabbedContentItem : ContentItemBasic
- where T : ContentPropertyBasic where TPersisted : IContentBase
+ where T : ContentPropertyBasic
+ where TPersisted : IContentBase
{
protected TabbedContentItem()
{
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index 40a697e1a9..6332d13afe 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -305,8 +305,10 @@
+
+