diff --git a/Marr.Data/Marr.Data.csproj b/Marr.Data/Marr.Data.csproj index 5018ad393..c30e40535 100644 --- a/Marr.Data/Marr.Data.csproj +++ b/Marr.Data/Marr.Data.csproj @@ -72,6 +72,8 @@ + + diff --git a/Marr.Data/QGen/QueryBuilder.cs b/Marr.Data/QGen/QueryBuilder.cs index f2eb9006b..6646b3a36 100644 --- a/Marr.Data/QGen/QueryBuilder.cs +++ b/Marr.Data/QGen/QueryBuilder.cs @@ -241,7 +241,7 @@ namespace Marr.Data.QGen string queryText = query.Generate(); _db.SqlMode = SqlModes.Text; - int count = (int)_db.ExecuteScalar(queryText); + int count = Convert.ToInt32(_db.ExecuteScalar(queryText)); _db.SqlMode = previousSqlMode; return count; diff --git a/Marr.Data/QGen/QueryFactory.cs b/Marr.Data/QGen/QueryFactory.cs index b2a15248a..6bb130ea1 100644 --- a/Marr.Data/QGen/QueryFactory.cs +++ b/Marr.Data/QGen/QueryFactory.cs @@ -56,6 +56,9 @@ namespace Marr.Data.QGen case DB_SqlCeClient: return new RowCountQueryDecorator(innerQuery); + case DB_SQLiteClient: + return new SqliteRowCountQueryDecorator(innerQuery); + default: throw new NotImplementedException("Row count has not yet been implemented for this provider."); } @@ -74,6 +77,9 @@ namespace Marr.Data.QGen case DB_SqlCeClient: return new PagingQueryDecorator(innerQuery, skip, take); + case DB_SQLiteClient: + return new SqlitePagingQueryDecorator(innerQuery, skip, take); + default: throw new NotImplementedException("Paging has not yet been implemented for this provider."); } diff --git a/Marr.Data/QGen/SortBuilder.cs b/Marr.Data/QGen/SortBuilder.cs index c5b1fc240..a0bfb2492 100644 --- a/Marr.Data/QGen/SortBuilder.cs +++ b/Marr.Data/QGen/SortBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Linq.Expressions; @@ -77,13 +78,13 @@ namespace Marr.Data.QGen internal SortBuilder Order(Type declaringType, string propertyName) { - _sortExpressions.Add(new SortColumn(declaringType, propertyName, SortDirection.Asc)); + _sortExpressions.Add(new SortColumn(declaringType, propertyName, ListSortDirection.Ascending)); return this; } internal SortBuilder OrderByDescending(Type declaringType, string propertyName) { - _sortExpressions.Add(new SortColumn(declaringType, propertyName, SortDirection.Desc)); + _sortExpressions.Add(new SortColumn(declaringType, propertyName, ListSortDirection.Descending)); return this; } @@ -103,25 +104,37 @@ namespace Marr.Data.QGen public virtual SortBuilder OrderBy(Expression> sortExpression) { - _sortExpressions.Add(new SortColumn(sortExpression, SortDirection.Asc)); + _sortExpressions.Add(new SortColumn(sortExpression, ListSortDirection.Ascending)); + return this; + } + + public virtual SortBuilder OrderBy(Expression> sortExpression, ListSortDirection sortDirection) + { + _sortExpressions.Add(new SortColumn(sortExpression, sortDirection)); return this; } public virtual SortBuilder OrderByDescending(Expression> sortExpression) { - _sortExpressions.Add(new SortColumn(sortExpression, SortDirection.Desc)); + _sortExpressions.Add(new SortColumn(sortExpression, ListSortDirection.Descending)); return this; } public virtual SortBuilder ThenBy(Expression> sortExpression) { - _sortExpressions.Add(new SortColumn(sortExpression, SortDirection.Asc)); + _sortExpressions.Add(new SortColumn(sortExpression, ListSortDirection.Ascending)); + return this; + } + + public virtual SortBuilder ThenBy(Expression> sortExpression, ListSortDirection sortDirection) + { + _sortExpressions.Add(new SortColumn(sortExpression, sortDirection)); return this; } public virtual SortBuilder ThenByDescending(Expression> sortExpression) { - _sortExpressions.Add(new SortColumn(sortExpression, SortDirection.Desc)); + _sortExpressions.Add(new SortColumn(sortExpression, ListSortDirection.Descending)); return this; } @@ -198,7 +211,7 @@ namespace Marr.Data.QGen string columnName = DataHelper.GetColumnName(sort.DeclaringType, sort.PropertyName, useAltName); sb.Append(_dialect.CreateToken(string.Format("{0}.{1}", table.Alias, columnName))); - if (sort.Direction == SortDirection.Desc) + if (sort.Direction == ListSortDirection.Descending) sb.Append(" DESC"); } diff --git a/Marr.Data/QGen/SortColumn.cs b/Marr.Data/QGen/SortColumn.cs index 1a38c3c82..bbd9cbd6e 100644 --- a/Marr.Data/QGen/SortColumn.cs +++ b/Marr.Data/QGen/SortColumn.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Linq.Expressions; @@ -9,22 +10,39 @@ namespace Marr.Data.QGen { public class SortColumn { + [Obsolete("Use ListSortDirection instead")] public SortColumn(Expression> sortExpression, SortDirection direction) { MemberExpression me = GetMemberExpression(sortExpression.Body); DeclaringType = me.Expression.Type; PropertyName = me.Member.Name; - Direction = direction; + Direction = GetSortDirection(direction); } + [Obsolete("Use ListSortDirection instead")] public SortColumn(Type declaringType, string propertyName, SortDirection direction) + { + DeclaringType = declaringType; + PropertyName = propertyName; + Direction = GetSortDirection(direction); + } + + public SortColumn(Expression> sortExpression, ListSortDirection direction) + { + MemberExpression me = GetMemberExpression(sortExpression.Body); + DeclaringType = me.Expression.Type; + PropertyName = me.Member.Name; + Direction = direction; + } + + public SortColumn(Type declaringType, string propertyName, ListSortDirection direction) { DeclaringType = declaringType; PropertyName = propertyName; Direction = direction; } - public SortDirection Direction { get; private set; } + public ListSortDirection Direction { get; private set; } public Type DeclaringType { get; private set; } public string PropertyName { get; private set; } @@ -40,6 +58,13 @@ namespace Marr.Data.QGen return me; } + + private ListSortDirection GetSortDirection(SortDirection direction) + { + if (direction == SortDirection.Desc) return ListSortDirection.Descending; + + return ListSortDirection.Ascending; + } } public enum SortDirection diff --git a/Marr.Data/QGen/SqlitePagingQueryDecorator.cs b/Marr.Data/QGen/SqlitePagingQueryDecorator.cs new file mode 100644 index 000000000..a0cb66c0b --- /dev/null +++ b/Marr.Data/QGen/SqlitePagingQueryDecorator.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Marr.Data.Mapping; +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + /// + /// Decorates the SelectQuery by wrapping it in a paging query. + /// + public class SqlitePagingQueryDecorator : IQuery + { + private SelectQuery _innerQuery; + private int _skip; + private int _take; + + public SqlitePagingQueryDecorator(SelectQuery innerQuery, int skip, int take) + { + if (string.IsNullOrEmpty(innerQuery.OrderBy.ToString())) + { + throw new DataMappingException("A paged query must specify an order by clause."); + } + + _innerQuery = innerQuery; + _skip = skip; + _take = take; + } + + public string Generate() + { + StringBuilder sql = new StringBuilder(); + + _innerQuery.BuildSelectClause(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildWhereClause(sql); + _innerQuery.BuildOrderClause(sql); + sql.AppendLine(String.Format(" LIMIT {0},{1}", _skip, _take)); + + return sql.ToString(); + } + + public void BuildSelectClause(StringBuilder sql) + { + List appended = new List(); + + sql.Append("SELECT "); + + int startIndex = sql.Length; + + // COLUMNS + foreach (Table join in _innerQuery.Tables) + { + for (int i = 0; i < join.Columns.Count; i++) + { + var c = join.Columns[i]; + + if (sql.Length > startIndex && sql[sql.Length - 1] != ',') + sql.Append(","); + + if (join is View) + { + string token = _innerQuery.Dialect.CreateToken(string.Concat(join.Alias, ".", _innerQuery.NameOrAltName(c.ColumnInfo))); + if (appended.Contains(token)) + continue; + + sql.Append(token); + appended.Add(token); + } + else + { + string token = string.Concat(join.Alias, ".", c.ColumnInfo.Name); + if (appended.Contains(token)) + continue; + + sql.Append(_innerQuery.Dialect.CreateToken(token)); + + if (_innerQuery.UseAltName && c.ColumnInfo.AltName != null && c.ColumnInfo.AltName != c.ColumnInfo.Name) + { + string altName = c.ColumnInfo.AltName; + sql.AppendFormat(" AS {0}", altName); + } + } + } + } + } + } +} diff --git a/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs b/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs new file mode 100644 index 000000000..396ade6af --- /dev/null +++ b/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Marr.Data.QGen +{ + public class SqliteRowCountQueryDecorator : IQuery + { + private SelectQuery _innerQuery; + + public SqliteRowCountQueryDecorator(SelectQuery innerQuery) + { + _innerQuery = innerQuery; + } + + public string Generate() + { + StringBuilder sql = new StringBuilder(); + + BuildSelectCountClause(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildWhereClause(sql); + + return sql.ToString(); + } + + private void BuildSelectCountClause(StringBuilder sql) + { + sql.AppendLine("SELECT COUNT(*)"); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/AutomapperBootstraper.cs b/NzbDrone.Api/AutomapperBootstraper.cs index 7af24e420..e8d8ad1d7 100644 --- a/NzbDrone.Api/AutomapperBootstraper.cs +++ b/NzbDrone.Api/AutomapperBootstraper.cs @@ -45,7 +45,9 @@ namespace NzbDrone.Api Mapper.CreateMap, PagingResource>(); //History - Mapper.CreateMap(); + Mapper.CreateMap() + .ForMember(dest => dest.Episode, opt => opt.Ignore()) + .ForMember(dest => dest.Series, opt => opt.Ignore()); Mapper.CreateMap, PagingResource>(); } } diff --git a/NzbDrone.Api/History/HistoryModule.cs b/NzbDrone.Api/History/HistoryModule.cs index 36dec0e00..dd572cc39 100644 --- a/NzbDrone.Api/History/HistoryModule.cs +++ b/NzbDrone.Api/History/HistoryModule.cs @@ -69,6 +69,24 @@ namespace NzbDrone.Api.History // Series = series, Indexer = "nzbs.org", Quality = new QualityModel(Quality.HDTV720p) + }, + new Core.History.History + { + Id = 2, + Date = DateTime.UtcNow.AddDays(-1), + // Episode = episode, + // Series = series, + Indexer = "nzbs.org", + Quality = new QualityModel(Quality.SDTV, true) + }, + new Core.History.History + { + Id = 3, + Date = DateTime.UtcNow.AddDays(-5), +// Episode = episode, +// Series = series, + Indexer = "nzbs.org", + Quality = new QualityModel(Quality.WEBDL1080p) } } }; diff --git a/NzbDrone.Api/Missing/MissingModule.cs b/NzbDrone.Api/Missing/MissingModule.cs index 99d0a5443..5c751f733 100644 --- a/NzbDrone.Api/Missing/MissingModule.cs +++ b/NzbDrone.Api/Missing/MissingModule.cs @@ -36,8 +36,8 @@ namespace NzbDrone.Api.Missing if (page == 0) page = 1; var sortKey = PrimitiveExtensions.ToNullSafeString(Request.Query.SortKey) - .Equals("SeriesTitle", StringComparison.InvariantCultureIgnoreCase) - ? "SeriesTitle" + .Equals("Series.Title", StringComparison.InvariantCultureIgnoreCase) + ? "Series.Title" : "AirDate"; var sortDirection = PrimitiveExtensions.ToNullSafeString(Request.Query.SortDir) diff --git a/NzbDrone.Core.Test/Datastore/PagingSpecExtenstionsTests/OrderByClauseFixture.cs b/NzbDrone.Core.Test/Datastore/PagingSpecExtenstionsTests/OrderByClauseFixture.cs new file mode 100644 index 000000000..d8d42c127 --- /dev/null +++ b/NzbDrone.Core.Test/Datastore/PagingSpecExtenstionsTests/OrderByClauseFixture.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Datastore.PagingSpecExtenstionsTests +{ + public class OrderByClauseFixture + { + [Test] + public void Test() + { + var pagingSpec = new PagingSpec + { + Page = 1, + PageSize = 10, + SortDirection = ListSortDirection.Ascending, + SortKey = "AirDate" + }; + + pagingSpec.OrderByClause().Should().NotBeNullOrEmpty(); + } + } +} diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index f80bf9d4b..978d5950b 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -120,6 +120,7 @@ + diff --git a/NzbDrone.Core/Datastore/PagingSpecExtensions.cs b/NzbDrone.Core/Datastore/PagingSpecExtensions.cs new file mode 100644 index 000000000..ae8a7038e --- /dev/null +++ b/NzbDrone.Core/Datastore/PagingSpecExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace NzbDrone.Core.Datastore +{ + public static class PagingSpecExtensions + { + public static Expression> OrderByClause(this PagingSpec pagingSpec) + { + return CreateExpression(pagingSpec.SortKey); + } + + public static int PagingOffset(this PagingSpec pagingSpec) + { + return (pagingSpec.Page - 1)*pagingSpec.PageSize; + } + + private static Expression> CreateExpression(string propertyName) + { + Type type = typeof(TModel); + ParameterExpression parameterExpression = Expression.Parameter(type, "x"); + Expression expressionBody = parameterExpression; + + var splitPropertyName = propertyName.Split('.').ToList(); + + foreach (var property in splitPropertyName) + { + expressionBody = Expression.Property(expressionBody, property); + } + + expressionBody = Expression.Convert(expressionBody, typeof(object)); + return Expression.Lambda>(expressionBody, parameterExpression); + } + } +} + \ No newline at end of file diff --git a/NzbDrone.Core/Datastore/TableMapping.cs b/NzbDrone.Core/Datastore/TableMapping.cs index 30772cfe6..b824d74b8 100644 --- a/NzbDrone.Core/Datastore/TableMapping.cs +++ b/NzbDrone.Core/Datastore/TableMapping.cs @@ -67,10 +67,6 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().MapResultSet(); } - - - - private static void RegisterMappers() { RegisterEmbeddedConverter(); diff --git a/NzbDrone.Core/History/HistoryRepository.cs b/NzbDrone.Core/History/HistoryRepository.cs index acfe76c80..29d121438 100644 --- a/NzbDrone.Core/History/HistoryRepository.cs +++ b/NzbDrone.Core/History/HistoryRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Data; using System.Linq; using NzbDrone.Common.Messaging; @@ -39,5 +40,7 @@ namespace NzbDrone.Core.History return null; } + + //public List GetPagedHistory() } } \ No newline at end of file diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 0f83ea277..d29e7cd9d 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -211,6 +211,7 @@ + diff --git a/NzbDrone.Core/Tv/EpisodeRepository.cs b/NzbDrone.Core/Tv/EpisodeRepository.cs index cf3951828..18f721b01 100644 --- a/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; +using System.Diagnostics; using System.Linq; +using FluentMigrator.Runner; using Marr.Data; using Marr.Data.QGen; using NzbDrone.Common.Messaging; @@ -65,40 +67,25 @@ namespace NzbDrone.Core.Tv public PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec, bool includeSpecials) { - //TODO: Join in the series title so we can do sorting on it - if (!pagingSpec.SortKey.Equals("SeriesTitle", StringComparison.InvariantCultureIgnoreCase) && - !pagingSpec.SortKey.Equals("AirDate", StringComparison.InvariantCultureIgnoreCase)) - { - throw new ArgumentException("Invalid SortKey: " + pagingSpec.SortKey, "pagingSpec"); - } - if (includeSpecials) { throw new NotImplementedException("Including specials is not available"); } - - var orderSql = String.Format("{0} {1}", pagingSpec.SortKey, - pagingSpec.SortDirection == ListSortDirection.Ascending ? "ASC" : "DESC"); - - var limitSql = String.Format("{0},{1}", (pagingSpec.Page - 1) * pagingSpec.PageSize, pagingSpec.PageSize); - + + //This causes an issue if done within the LINQ Query var currentTime = DateTime.UtcNow; - _dataMapper.AddParameter("currentTime", currentTime); - - var sql = String.Format(@"SELECT Episodes.*, Series.Title as SeriesTitle - FROM Episodes - INNER JOIN Series - ON Episodes.SeriesId = Series.Id - WHERE EpisodeFileId = 0 - AND SeasonNumber > 0 - AND AirDate <= @currentTime - ORDER BY {0} - LIMIT {1}", - orderSql, limitSql - ); - - pagingSpec.Records = _dataMapper.Query(sql); - pagingSpec.TotalRecords = Query.Count(e => e.EpisodeFileId == 0 && e.SeasonNumber > 0 && e.AirDate <= currentTime); + + pagingSpec.Records = Query.Join(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) + .Where(e => e.EpisodeFileId == 0) + .AndWhere(e => e.SeasonNumber > 0) + .AndWhere(e => e.AirDate <= currentTime) + .OrderBy(pagingSpec.OrderByClause(), pagingSpec.SortDirection) + .Skip(pagingSpec.PagingOffset()) + .Take(pagingSpec.PageSize) + .ToList(); + + //TODO: Use the same query for count and records + pagingSpec.TotalRecords = Query.Where(e => e.EpisodeFileId == 0 && e.SeasonNumber > 0 && e.AirDate <= currentTime).GetRowCount(); return pagingSpec; } diff --git a/NzbDrone.ncrunchsolution b/NzbDrone.ncrunchsolution index 969da6dd4..6cb47a29a 100644 --- a/NzbDrone.ncrunchsolution +++ b/NzbDrone.ncrunchsolution @@ -1,8 +1,7 @@ 1 - True + False true - true UseDynamicAnalysis Disabled Disabled diff --git a/UI/History/Collection.js b/UI/History/Collection.js index ad2588824..05056689f 100644 --- a/UI/History/Collection.js +++ b/UI/History/Collection.js @@ -1,6 +1,6 @@ "use strict"; define(['app', 'History/Model'], function () { - NzbDrone.Missing.Collection = Backbone.PageableCollection.extend({ + NzbDrone.History.Collection = Backbone.PageableCollection.extend({ url : NzbDrone.Constants.ApiRoot + '/history', model : NzbDrone.History.Model, diff --git a/UI/History/ControlsColumnTemplate.html b/UI/History/ControlsColumnTemplate.html index 2783a2600..2712fb5d5 100644 --- a/UI/History/ControlsColumnTemplate.html +++ b/UI/History/ControlsColumnTemplate.html @@ -1 +1,2 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/UI/History/EpisodeColumnTemplate.html b/UI/History/EpisodeColumnTemplate.html deleted file mode 100644 index 30ab0a188..000000000 --- a/UI/History/EpisodeColumnTemplate.html +++ /dev/null @@ -1,2 +0,0 @@ - -{{seasonNumber}}x{{paddedEpisodeNumber}} \ No newline at end of file diff --git a/UI/History/HistoryLayout.js b/UI/History/HistoryLayout.js index 2b40e1c2c..eed56ac45 100644 --- a/UI/History/HistoryLayout.js +++ b/UI/History/HistoryLayout.js @@ -18,11 +18,18 @@ define([ showTable: function () { var columns = [ + { + name : 'indexer', + label : '', + editable : false, + cell : Backgrid.TemplateBackedCell.extend({ template: 'History/IndexerTemplate' }), + headerCell: 'nzbDrone' + }, { name : 'seriesTitle', label : 'Series Title', editable : false, - cell : Backgrid.TemplateBackedCell.extend({ template: 'History/SeriesTitleTemplate' }), + cell : Backgrid.TemplateBackedCell.extend({ template: 'Missing/SeriesTitleTemplate' }), headerCell: 'nzbDrone' }, { @@ -30,11 +37,11 @@ define([ label : 'Episode', editable : false, sortable : false, - cell : Backgrid.TemplateBackedCell.extend({ template: 'History/EpisodeColumnTemplate' }), + cell : Backgrid.TemplateBackedCell.extend({ template: 'Missing/EpisodeColumnTemplate' }), headerCell: 'nzbDrone' }, { - name : 'title', + name : 'episode.title', label : 'Episode Title', editable : false, sortable : false, @@ -42,8 +49,15 @@ define([ headerCell: 'nzbDrone' }, { - name : 'airDate', - label : 'Air Date', + name : 'quality', + label : 'Quality', + editable : false, + cell : Backgrid.TemplateBackedCell.extend({ template: 'History/QualityTemplate' }), + headerCell: 'nzbDrone' + }, + { + name : 'date', + label : 'Grabbed', editable : false, cell : 'airDate', headerCell: 'nzbDrone' diff --git a/UI/History/IndexerTemplate.html b/UI/History/IndexerTemplate.html new file mode 100644 index 000000000..5f493178b --- /dev/null +++ b/UI/History/IndexerTemplate.html @@ -0,0 +1 @@ +{{indexer}} \ No newline at end of file diff --git a/UI/History/QualityTemplate.html b/UI/History/QualityTemplate.html new file mode 100644 index 000000000..538576448 --- /dev/null +++ b/UI/History/QualityTemplate.html @@ -0,0 +1,5 @@ +{{quality.quality.name}} + +{{#if quality.proper}} + [PROPER] +{{/if}} \ No newline at end of file diff --git a/UI/History/SeriesTitleTemplate.html b/UI/History/SeriesTitleTemplate.html deleted file mode 100644 index 0a8c3ae09..000000000 --- a/UI/History/SeriesTitleTemplate.html +++ /dev/null @@ -1 +0,0 @@ -{{series.title}} \ No newline at end of file diff --git a/UI/Missing/MissingLayout.js b/UI/Missing/MissingLayout.js index 61eabeb12..6f5e3c88e 100644 --- a/UI/Missing/MissingLayout.js +++ b/UI/Missing/MissingLayout.js @@ -20,7 +20,7 @@ define([ var columns = [ { - name : 'seriesTitle', + name : 'series.Title', label : 'Series Title', editable : false, cell : Backgrid.TemplateBackedCell.extend({ template: 'Missing/SeriesTitleTemplate' }),