Feature: Job Queues

Also UI style, theme and element changes
This commit is contained in:
Gary Sharp
2014-02-03 14:50:08 +11:00
parent bdb3e1e6b4
commit 3f63281dc4
212 changed files with 17334 additions and 5441 deletions
@@ -138,7 +138,7 @@ namespace Disco.Web.Areas.API.Controllers
return Update(id, pScope, Scope, redirect);
}
[DiscoAuthorize(Claims.Config.DocumentTemplate.Configure)]
public virtual ActionResult UpdateSubTypes(string id, List<string> SubTypes = null)
public virtual ActionResult UpdateJobSubTypes(string id, List<string> JobSubTypes = null, bool redirect = false)
{
try
{
@@ -146,13 +146,19 @@ namespace Disco.Web.Areas.API.Controllers
throw new ArgumentNullException("id");
var documentTemplate = Database.DocumentTemplates.Find(id);
UpdateSubTypes(documentTemplate, SubTypes);
UpdateJobSubTypes(documentTemplate, JobSubTypes);
return Json("OK", JsonRequestBehavior.AllowGet);
if (redirect)
return RedirectToAction(MVC.Config.DocumentTemplate.Index(documentTemplate.Id));
else
return Json("OK", JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
return Json(string.Format("Error: {0}", ex.Message), JsonRequestBehavior.AllowGet);
if (redirect)
throw;
else
return Json(string.Format("Error: {0}", ex.Message), JsonRequestBehavior.AllowGet);
}
}
@@ -224,7 +230,7 @@ namespace Disco.Web.Areas.API.Controllers
Database.SaveChanges();
}
private void UpdateSubTypes(Disco.Models.Repository.DocumentTemplate documentTemplate, List<string> SubTypes)
private void UpdateJobSubTypes(Disco.Models.Repository.DocumentTemplate documentTemplate, List<string> JobSubTypes)
{
Database.Configuration.LazyLoadingEnabled = true;
@@ -236,10 +242,10 @@ namespace Disco.Web.Areas.API.Controllers
}
// Add New
if (SubTypes != null && SubTypes.Count > 0)
if (JobSubTypes != null && JobSubTypes.Count > 0)
{
var subTypes = new List<Disco.Models.Repository.JobSubType>();
foreach (var stId in SubTypes)
foreach (var stId in JobSubTypes)
{
var typeId = stId.Substring(0, stId.IndexOf("_"));
var subTypeId = stId.Substring(stId.IndexOf("_") + 1);
@@ -1690,9 +1690,9 @@ namespace Disco.Web.Areas.API.Controllers
Database.Configuration.LazyLoadingEnabled = true;
if (j != null)
{
if (j.CanForceClose())
if (j.CanCloseForced())
{
j.OnForceClose(Database, CurrentUser, Reason);
j.OnCloseForced(Database, CurrentUser, Reason);
Database.SaveChanges();
if (redirect.HasValue && redirect.Value)
@@ -1715,9 +1715,9 @@ namespace Disco.Web.Areas.API.Controllers
Database.Configuration.LazyLoadingEnabled = true;
if (j != null)
{
if (j.CanClose())
if (j.CanCloseNormally())
{
j.OnClose(CurrentUser);
j.OnCloseNormally(CurrentUser);
Database.SaveChanges();
if (redirect)
@@ -18,8 +18,6 @@ namespace Disco.Web.Areas.API.Controllers
Database.DiscoConfiguration.JobPreferences.LongRunningJobDaysThreshold = LongRunningJobDaysThreshold;
Database.SaveChanges();
Disco.Web.Controllers.JobController.ReInitializeLongRunningJobList(Database);
if (redirect)
return RedirectToAction(MVC.Config.JobPreferences.Index());
else
@@ -0,0 +1,393 @@
using Disco.BI.Interop.ActiveDirectory;
using Disco.Models.Interop.ActiveDirectory;
using Disco.Models.Repository;
using Disco.Services.Authorization;
using Disco.Services.Jobs.JobQueues;
using Disco.Services.Web;
using Disco.BI.Extensions;
using System;
using System.Linq;
using System.Web.Mvc;
using System.Collections.Generic;
namespace Disco.Web.Areas.API.Controllers
{
public partial class JobQueueController : AuthorizedDatabaseController
{
const string pName = "name";
const string pDescription = "description";
const string pIcon = "icon";
const string pIconColour = "iconcolour";
const string pPriority = "priority";
const string pDefaultSLAExpiry = "defaultslaexpiry";
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult Update(int id, string key, string value = null, Nullable<bool> redirect = null)
{
Authorization.Require(Claims.Config.JobQueue.Configure);
try
{
if (id < 0)
throw new ArgumentOutOfRangeException("id");
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
var jobQueue = Database.JobQueues.Find(id);
if (jobQueue != null)
{
switch (key.ToLower())
{
case pName:
UpdateName(jobQueue, value);
break;
case pDescription:
UpdateDescription(jobQueue, value);
break;
case pPriority:
UpdatePriority(jobQueue, value);
break;
case pIcon:
UpdateIcon(jobQueue, value);
break;
case pIconColour:
UpdateIconColour(jobQueue, value);
break;
case pDefaultSLAExpiry:
UpdateDefaultSLAExpiry(jobQueue, value);
break;
default:
throw new Exception("Invalid Update Key");
}
}
else
{
throw new Exception("Invalid Job Queue Id");
}
if (redirect.HasValue && redirect.Value)
return RedirectToAction(MVC.Config.JobQueue.Index(jobQueue.Id));
else
return Json("OK", JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
if (redirect.HasValue && redirect.Value)
throw;
else
return Json(string.Format("Error: {0}", ex.Message), JsonRequestBehavior.AllowGet);
}
}
#region Update Shortcut Methods
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult UpdateName(int id, string QueueName = null, Nullable<bool> redirect = null)
{
return Update(id, pName, QueueName, redirect);
}
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult UpdateDescription(int id, string Description = null, Nullable<bool> redirect = null)
{
return Update(id, pDescription, Description, redirect);
}
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult UpdatePriority(int id, string Priority = null, Nullable<bool> redirect = null)
{
return Update(id, pPriority, Priority, redirect);
}
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult UpdateDefaultSLAExpiry(int id, string DefaultSLAExpiry = null, Nullable<bool> redirect = null)
{
return Update(id, pDefaultSLAExpiry, DefaultSLAExpiry, redirect);
}
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult UpdateIcon(int id, string Icon = null, Nullable<bool> redirect = null)
{
return Update(id, pIcon, Icon, redirect);
}
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult UpdateIconColour(int id, string IconColour = null, Nullable<bool> redirect = null)
{
return Update(id, pIconColour, IconColour, redirect);
}
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult UpdateIconAndColour(int id, string Icon = null, string IconColour = null, bool redirect = false)
{
try
{
if (id < 0)
throw new ArgumentOutOfRangeException("id");
var jobQueue = Database.JobQueues.Find(id);
if (jobQueue != null)
{
UpdateIconAndColour(jobQueue, Icon, IconColour);
}
else
{
return Json("Invalid Job Queue Id", JsonRequestBehavior.AllowGet);
}
if (redirect)
return RedirectToAction(MVC.Config.JobQueue.Index(jobQueue.Id));
else
return Json("OK", JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
if (redirect)
throw;
else
return Json(string.Format("Error: {0}", ex.Message), JsonRequestBehavior.AllowGet);
}
}
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult UpdateSubjects(int id, string[] Subjects = null, bool redirect = false)
{
try
{
if (id < 0)
throw new ArgumentOutOfRangeException("id");
var jobQueue = Database.JobQueues.Find(id);
if (jobQueue != null)
{
UpdateSubjects(jobQueue, Subjects);
}
else
{
return Json("Invalid Job Queue Id", JsonRequestBehavior.AllowGet);
}
if (redirect)
return RedirectToAction(MVC.Config.JobQueue.Index(jobQueue.Id));
else
return Json("OK", JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
if (redirect)
throw;
else
return Json(string.Format("Error: {0}", ex.Message), JsonRequestBehavior.AllowGet);
}
}
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult UpdateJobSubTypes(int id, List<string> JobSubTypes = null, bool redirect = false)
{
try
{
var jobQueue = Database.JobQueues.Find(id);
if (jobQueue != null)
{
UpdateJobSubTypes(jobQueue, JobSubTypes);
}
else
{
return Json("Invalid Job Queue Id", JsonRequestBehavior.AllowGet);
}
if (redirect)
return RedirectToAction(MVC.Config.JobQueue.Index(jobQueue.Id));
else
return Json("OK", JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
if (redirect)
throw;
else
return Json(string.Format("Error: {0}", ex.Message), JsonRequestBehavior.AllowGet);
}
}
#endregion
#region Update Properties
private void UpdateIconAndColour(JobQueue jobQueue, string Icon, string IconColour)
{
if (string.IsNullOrWhiteSpace(Icon))
throw new ArgumentNullException("Icon");
if (string.IsNullOrWhiteSpace(IconColour))
throw new ArgumentNullException("IconColour");
jobQueue.Icon = Icon;
jobQueue.IconColour = IconColour;
JobQueueService.UpdateJobQueue(Database, jobQueue);
}
private void UpdateIcon(JobQueue jobQueue, string Icon)
{
if (string.IsNullOrWhiteSpace(Icon))
throw new ArgumentNullException("Icon");
jobQueue.Icon = Icon;
JobQueueService.UpdateJobQueue(Database, jobQueue);
}
private void UpdateIconColour(JobQueue jobQueue, string IconColour)
{
if (string.IsNullOrWhiteSpace(IconColour))
throw new ArgumentNullException("IconColour");
jobQueue.IconColour = IconColour;
JobQueueService.UpdateJobQueue(Database, jobQueue);
}
private void UpdateName(JobQueue jobQueue, string Name)
{
jobQueue.Name = Name;
JobQueueService.UpdateJobQueue(Database, jobQueue);
}
private void UpdateDescription(JobQueue jobQueue, string Description)
{
jobQueue.Description = Description;
JobQueueService.UpdateJobQueue(Database, jobQueue);
}
private void UpdatePriority(JobQueue jobQueue, string Priority)
{
JobQueuePriority priority;
if (!Enum.TryParse<JobQueuePriority>(Priority, out priority))
throw new ArgumentException("Invalid Priority Value", "Priority");
jobQueue.Priority = priority;
JobQueueService.UpdateJobQueue(Database, jobQueue);
}
private void UpdateDefaultSLAExpiry(JobQueue jobQueue, string DefaultSLAExpiry)
{
int? defaultSLAExpiry = null;
if (!string.IsNullOrEmpty(DefaultSLAExpiry))
{
int intValue;
if (!int.TryParse(DefaultSLAExpiry, out intValue))
throw new ArgumentException("Invalid Default SLA Expiry Value", "DefaultSLAPriority");
if (intValue < 0)
throw new ArgumentException("Default SLA Expiry Value must be greater than zero", "DefaultSLAPriority");
// if intValue == 0, then no SLA.
if (intValue > 0)
defaultSLAExpiry = intValue;
}
jobQueue.DefaultSLAExpiry = defaultSLAExpiry;
JobQueueService.UpdateJobQueue(Database, jobQueue);
}
private void UpdateSubjects(JobQueue jobQueue, string[] Subjects)
{
string subjectIds = null;
// Validate Subjects
if (Subjects != null && Subjects.Length > 0)
{
var subjects = Subjects.Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()).Select(s => new Tuple<string, IActiveDirectoryObject>(s, ActiveDirectory.GetObject(s))).ToList();
var invalidSubjects = subjects.Where(s => s.Item2 == null).ToList();
if (invalidSubjects.Count > 0)
throw new ArgumentException(string.Format("Subjects not found: {0}", string.Join(", ", invalidSubjects)), "Subjects");
var proposedSubjects = subjects.Select(s => s.Item2.SamAccountName).OrderBy(s => s).ToArray();
subjectIds = string.Join(",", proposedSubjects);
if (string.IsNullOrEmpty(subjectIds))
subjectIds = null;
}
if (jobQueue.SubjectIds != subjectIds)
{
jobQueue.SubjectIds = subjectIds;
JobQueueService.UpdateJobQueue(Database, jobQueue);
}
}
private void UpdateJobSubTypes(Disco.Models.Repository.JobQueue jobQueue, List<string> JobSubTypes)
{
Database.Configuration.LazyLoadingEnabled = true;
// Remove All Existing
if (jobQueue.JobSubTypes != null)
{
foreach (var st in jobQueue.JobSubTypes.ToArray())
jobQueue.JobSubTypes.Remove(st);
}
// Add New
if (JobSubTypes != null && JobSubTypes.Count > 0)
{
var subTypes = new List<Disco.Models.Repository.JobSubType>();
foreach (var stId in JobSubTypes)
{
var typeId = stId.Substring(0, stId.IndexOf("_"));
var subTypeId = stId.Substring(stId.IndexOf("_") + 1);
var subType = Database.JobSubTypes.FirstOrDefault(jst => jst.JobTypeId == typeId && jst.Id == subTypeId);
subTypes.Add(subType);
}
jobQueue.JobSubTypes = subTypes;
}
Database.SaveChanges();
}
#endregion
#region Actions
[DiscoAuthorize(Claims.Config.JobQueue.Delete)]
public virtual ActionResult Delete(int id, Nullable<bool> redirect = false)
{
try
{
var jq = Database.JobQueues.Find(id);
if (jq != null)
{
var status = JobQueueDeleteTask.ScheduleNow(id);
status.SetFinishedUrl(Url.Action(MVC.Config.JobQueue.Index(null)));
if (redirect.HasValue && redirect.Value)
return RedirectToAction(MVC.Config.Logging.TaskStatus(status.SessionId));
else
return Json("OK", JsonRequestBehavior.AllowGet);
}
throw new Exception("Invalid Job Queue Id");
}
catch (Exception ex)
{
if (redirect.HasValue && redirect.Value)
throw;
else
return Json(string.Format("Error: {0}", ex.Message), JsonRequestBehavior.AllowGet);
}
}
#endregion
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult SearchSubjects(string term)
{
var groupResults = BI.Interop.ActiveDirectory.ActiveDirectory.SearchGroups(term).Cast<IActiveDirectoryObject>();
var userResults = BI.Interop.ActiveDirectory.ActiveDirectory.SearchUsers(term).Cast<IActiveDirectoryObject>();
var results = groupResults.Concat(userResults).OrderBy(r => r.SamAccountName)
.Select(r => Models.JobQueue.SubjectItem.FromActiveDirectoryObject(r)).ToList();
return Json(results, JsonRequestBehavior.AllowGet);
}
[DiscoAuthorize(Claims.Config.JobQueue.Configure)]
public virtual ActionResult Subject(string Id)
{
var subject = ActiveDirectory.GetObject(Id);
if (subject == null || !(subject is ActiveDirectoryUserAccount || subject is ActiveDirectoryGroup))
return Json(null, JsonRequestBehavior.AllowGet);
else
return Json(Models.JobQueue.SubjectItem.FromActiveDirectoryObject(subject), JsonRequestBehavior.AllowGet);
}
}
}
@@ -0,0 +1,234 @@
using Disco.Models.Repository;
using Disco.Services.Authorization;
using Disco.Services.Jobs.JobQueues;
using Disco.BI.Extensions;
using Disco.Services.Web;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Disco.Web.Areas.API.Controllers
{
public partial class JobQueueJobController : AuthorizedDatabaseController
{
const string pAddedComment = "addedcomment";
const string pRemovedComment = "removedcomment";
const string pSla = "sla";
const string pPriority = "priority";
public virtual ActionResult Update(int id, string key, string value = null, Nullable<bool> redirect = null)
{
try
{
if (id < 0)
throw new ArgumentOutOfRangeException("id");
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
var jobQueueJob = Database.JobQueueJobs.Include("Job").FirstOrDefault(jqj => jqj.Id == id);
if (jobQueueJob != null)
{
switch (key.ToLower())
{
case pAddedComment:
UpdateAddedComment(jobQueueJob, value);
break;
case pRemovedComment:
UpdateRemovedComment(jobQueueJob, value);
break;
case pSla:
UpdateSla(jobQueueJob, value);
break;
case pPriority:
UpdatePriority(jobQueueJob, value);
break;
default:
throw new Exception("Invalid Update Key");
}
}
else
{
throw new Exception("Invalid Job Queue Job Id");
}
if (redirect.HasValue && redirect.Value)
return Redirect(string.Format("{0}#jobDetailTab-Queues", Url.Action(MVC.Job.Show(jobQueueJob.JobId))));
else
return Json("OK", JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
if (redirect.HasValue && redirect.Value)
throw;
else
return Json(string.Format("Error: {0}", ex.Message), JsonRequestBehavior.AllowGet);
}
}
#region Update Shortcut Methods
[DiscoAuthorizeAny(Claims.Job.Properties.JobQueueProperties.EditAnyComments, Claims.Job.Properties.JobQueueProperties.EditOwnComments)]
public virtual ActionResult UpdateAddedComment(int id, string AddedComment = null, Nullable<bool> redirect = null)
{
return Update(id, pAddedComment, AddedComment, redirect);
}
[DiscoAuthorizeAny(Claims.Job.Properties.JobQueueProperties.EditAnyComments, Claims.Job.Properties.JobQueueProperties.EditOwnComments)]
public virtual ActionResult UpdateRemovedComment(int id, string RemovedComment = null, Nullable<bool> redirect = null)
{
return Update(id, pRemovedComment, RemovedComment, redirect);
}
[DiscoAuthorizeAny(Claims.Job.Properties.JobQueueProperties.EditAnySLA, Claims.Job.Properties.JobQueueProperties.EditOwnSLA)]
public virtual ActionResult UpdateSla(int id, string SLA = null, Nullable<bool> redirect = null)
{
return Update(id, pSla, SLA, redirect);
}
[DiscoAuthorizeAny(Claims.Job.Properties.JobQueueProperties.EditAnyPriority, Claims.Job.Properties.JobQueueProperties.EditOwnPriority)]
public virtual ActionResult UpdatePriority(int id, string Priority = null, Nullable<bool> redirect = null)
{
return Update(id, pPriority, Priority, redirect);
}
[DiscoAuthorizeAny(Claims.Job.Properties.JobQueueProperties.EditAnySLA, Claims.Job.Properties.JobQueueProperties.EditOwnSLA,
Claims.Job.Properties.JobQueueProperties.EditAnyPriority, Claims.Job.Properties.JobQueueProperties.EditOwnPriority)]
public virtual ActionResult UpdateSlaAndPriority(int id, string Sla = null, string Priority = null, Nullable<bool> redirect = null)
{
try
{
if (id < 0)
throw new ArgumentOutOfRangeException("id");
var jobQueueJob = Database.JobQueueJobs.Include("Job").FirstOrDefault(jqj => jqj.Id == id);
if (jobQueueJob != null)
{
UpdateSla(jobQueueJob, Sla);
UpdatePriority(jobQueueJob, Priority);
}
else
{
throw new Exception("Invalid Job Queue Job Id");
}
if (redirect.HasValue && redirect.Value)
return Redirect(string.Format("{0}#jobDetailTab-Queues", Url.Action(MVC.Job.Show(jobQueueJob.JobId))));
else
return Json("OK", JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
if (redirect.HasValue && redirect.Value)
throw;
else
return Json(string.Format("Error: {0}", ex.Message), JsonRequestBehavior.AllowGet);
}
}
#endregion
#region Update Properties
private void UpdateAddedComment(JobQueueJob jobQueueJob, string AddedComment)
{
if (!jobQueueJob.CanEditAddedComment())
throw new InvalidOperationException("Editing added comment for job queue job is Denied");
jobQueueJob.OnEditAddedComment(AddedComment);
Database.SaveChanges();
}
private void UpdateRemovedComment(JobQueueJob jobQueueJob, string RemovedComment)
{
if (!jobQueueJob.CanEditRemovedComment())
throw new InvalidOperationException("Editing removed comment for job queue job is Denied");
jobQueueJob.OnEditRemovedComment(RemovedComment);
Database.SaveChanges();
}
private void UpdateSla(JobQueueJob jobQueueJob, string Sla)
{
if (!jobQueueJob.CanEditSla())
throw new InvalidOperationException("Editing SLA for job queue job is Denied");
if (!string.IsNullOrEmpty(Sla))
{
DateTime SLADate;
if (DateTime.TryParse(Sla, out SLADate))
{
jobQueueJob.OnEditSla(SLADate);
Database.SaveChanges();
}
else
{
throw new ArgumentException("Unable to Parse SLA Date", "SLA");
}
}
else
{
jobQueueJob.OnEditSla(null);
Database.SaveChanges();
}
}
private void UpdatePriority(JobQueueJob jobQueueJob, string Priority)
{
if (!jobQueueJob.CanEditPriority())
throw new InvalidOperationException("Editing Priority for job queue job is Denied");
JobQueuePriority priority;
if (!Enum.TryParse<JobQueuePriority>(Priority, out priority))
throw new ArgumentException("Invalid Priority Value", "Priority");
jobQueueJob.OnEditPriority(priority);
Database.SaveChanges();
}
#endregion
#region Actions
[DiscoAuthorizeAny(Claims.Job.Actions.AddAnyQueues, Claims.Job.Actions.AddOwnQueues)]
public virtual ActionResult AddJob(int id, int JobId, string Comment, int? SLAExpiresMinutes, JobQueuePriority Priority)
{
DateTime? SLAExpires = (SLAExpiresMinutes.HasValue && SLAExpiresMinutes.Value > 0) ? DateTime.Now.AddMinutes(SLAExpiresMinutes.Value) : (DateTime?)null;
var jobQueueToken = JobQueueService.GetQueue(id);
if (jobQueueToken == null)
throw new ArgumentException("Invalid Job Queue Id", "id");
var job = Database.Jobs.Include("JobQueues").FirstOrDefault(j => j.Id == JobId);
if (job == null)
throw new ArgumentException("Invalid Job Id", "JobId");
if (!job.CanAddQueue(jobQueueToken.JobQueue))
throw new InvalidOperationException("Adding job to queue is Denied");
var jobQueueJob = job.OnAddQueue(Database, jobQueueToken.JobQueue, CurrentUser, Comment, SLAExpires, Priority);
Database.SaveChanges();
return Redirect(string.Format("{0}#jobDetailTab-Queues", Url.Action(MVC.Job.Show(job.Id))));
}
[DiscoAuthorizeAny(Claims.Job.Actions.RemoveAnyQueues, Claims.Job.Actions.RemoveOwnQueues)]
public virtual ActionResult RemoveJob(int id, string Comment, bool? CloseJob = null)
{
Database.Configuration.LazyLoadingEnabled = true;
var jobQueueJob = Database.JobQueueJobs.Find(id);
if (jobQueueJob == null)
throw new ArgumentException("Invalid Job Queue Job Id", "id");
if (!jobQueueJob.CanRemove())
throw new InvalidOperationException("Removing job from queue is Denied");
var job = Database.Jobs.Find(jobQueueJob.JobId);
if (job == null)
throw new ArgumentException("Invalid Job Id", "JobId");
jobQueueJob.OnRemove(CurrentUser, Comment);
Database.SaveChanges();
if (CloseJob.HasValue && CloseJob.Value && job.CanCloseNormally())
{
job.OnCloseNormally(CurrentUser);
Database.SaveChanges();
}
return Redirect(string.Format("{0}#jobDetailTab-Queues", Url.Action(MVC.Job.Show(job.Id))));
}
#endregion
}
}