* dto madness

* api madness
* browse page is beginning to not be empty
* NetVotes -> Score
This commit is contained in:
tmont 2011-02-16 02:48:11 +00:00
parent 5c14400d48
commit 14ca315213
21 changed files with 742 additions and 17 deletions

View File

@ -0,0 +1,7 @@
using System;
namespace VideoGameQuotes.Api {
public class ApiException : Exception {
public ApiException(string message = null, Exception innerException = null) : base(message, innerException) { }
}
}

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
using Portoa.Persistence; using Portoa.Persistence;
namespace VideoGameQuotes.Api { namespace VideoGameQuotes.Api {
public class Category : Entity<Category, int> { public class Category : Entity<Category, int>, IDtoMappable<CategoryDto> {
public Category() { public Category() {
Created = DateTime.UtcNow; Created = DateTime.UtcNow;
} }
@ -11,5 +11,19 @@ namespace VideoGameQuotes.Api {
public virtual DateTime Created { get; set; } public virtual DateTime Created { get; set; }
[Required, StringLength(255)] [Required, StringLength(255)]
public virtual string Name { get; set; } public virtual string Name { get; set; }
public virtual CategoryDto ToDto() {
return new CategoryDto {
Id = Id,
Created = Created,
Name = Name
};
}
}
public class CategoryDto {
public int Id { get; set; }
public DateTime Created { get; set; }
public string Name { get; set; }
} }
} }

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using Portoa.Persistence;
namespace VideoGameQuotes.Api {
public abstract class CriterionHandler<T> where T : Entity<T, int> {
public IEnumerable<Func<T, bool>> HandleCriterion(IEnumerable<object> values) {
foreach (var value in values) {
if (value is int) {
yield return HandleInteger((int)value);
} else {
yield return HandleString((string)value);
}
}
}
protected virtual Func<T, bool> HandleInteger(int value) {
throw new ApiException(string.Format("The value \"{0}\" is invalid", value));
}
protected virtual Func<T, bool> HandleString(string value) {
throw new ApiException(string.Format("The value \"{0}\" is invalid", value));
}
}
}

View File

@ -1,11 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Iesi.Collections.Generic; using Iesi.Collections.Generic;
using JetBrains.Annotations; using JetBrains.Annotations;
using Portoa.Persistence; using Portoa.Persistence;
namespace VideoGameQuotes.Api { namespace VideoGameQuotes.Api {
public class Game : Entity<Game, int> { public class Game : Entity<Game, int>, IDtoMappable<GameDto> {
private readonly Iesi.Collections.Generic.ISet<GamingSystem> systems = new HashedSet<GamingSystem>(); private readonly Iesi.Collections.Generic.ISet<GamingSystem> systems = new HashedSet<GamingSystem>();
private readonly Iesi.Collections.Generic.ISet<Publisher> publishers = new HashedSet<Publisher>(); private readonly Iesi.Collections.Generic.ISet<Publisher> publishers = new HashedSet<Publisher>();
@ -42,5 +43,49 @@ namespace VideoGameQuotes.Api {
publishers.Clear(); publishers.Clear();
} }
#endregion #endregion
public virtual GameDto ToDto() {
return new GameDto {
Id = Id,
Website = Website,
Created = Created,
Systems = Systems.Select(system => system.ToDto()),
Name = Name,
Region = Region,
Publishers = Publishers.Select(publisher => publisher.ToDto())
};
}
#region criterion handlers
public sealed class SystemCriterionHandler : CriterionHandler<Game> {
protected override Func<Game, bool> HandleInteger(int value) {
return game => game.Systems.Any(system => system.Id == value);
}
protected override Func<Game, bool> HandleString(string value) {
return game => game.Systems.Any(system => system.Name == value);
}
}
public sealed class PublisherCriterionHandler : CriterionHandler<Game> {
protected override Func<Game, bool> HandleInteger(int value) {
return game => game.Publishers.Any(publisher => publisher.Id == value);
}
protected override Func<Game, bool> HandleString(string value) {
return game => game.Publishers.Any(publisher => publisher.Name == value);
}
}
#endregion
}
public class GameDto {
public int Id { get; set; }
public string Website { get; set; }
public DateTime Created { get; set; }
public IEnumerable<SystemDto> Systems { get; set; }
public string Name { get; set; }
public IEnumerable<PublisherDto> Publishers { get; set; }
public Region Region { get; set; }
} }
} }

View File

@ -2,7 +2,7 @@
using Portoa.Persistence; using Portoa.Persistence;
namespace VideoGameQuotes.Api { namespace VideoGameQuotes.Api {
public class GamingSystem : Entity<GamingSystem, int> { public class GamingSystem : Entity<GamingSystem, int>, IDtoMappable<SystemDto> {
public GamingSystem() { public GamingSystem() {
Created = DateTime.UtcNow; Created = DateTime.UtcNow;
} }
@ -11,5 +11,23 @@ namespace VideoGameQuotes.Api {
public virtual DateTime ReleaseDate { get; set; } public virtual DateTime ReleaseDate { get; set; }
public virtual string Name { get; set; } public virtual string Name { get; set; }
public virtual string Abbreviation { get; set; } public virtual string Abbreviation { get; set; }
public virtual SystemDto ToDto() {
return new SystemDto {
Id = Id,
Created = Created,
ReleaseDate = ReleaseDate,
Name = Name,
Abbreviation = Abbreviation
};
}
}
public class SystemDto {
public int Id { get; set; }
public DateTime Created { get; set; }
public DateTime ReleaseDate { get; set; }
public string Name { get; set; }
public string Abbreviation { get; set; }
} }
} }

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
using Portoa.Persistence; using Portoa.Persistence;
namespace VideoGameQuotes.Api { namespace VideoGameQuotes.Api {
public class Publisher : Entity<Publisher, int> { public class Publisher : Entity<Publisher, int>, IDtoMappable<PublisherDto> {
public Publisher() { public Publisher() {
Created = DateTime.UtcNow; Created = DateTime.UtcNow;
} }
@ -12,5 +12,21 @@ namespace VideoGameQuotes.Api {
public virtual string Name { get; set; } public virtual string Name { get; set; }
public virtual string Website { get; set; } public virtual string Website { get; set; }
public virtual DateTime Created { get; set; } public virtual DateTime Created { get; set; }
public virtual PublisherDto ToDto() {
return new PublisherDto {
Id = Id,
Name = Name,
Website = Website,
Created = Created
};
}
}
public class PublisherDto {
public int Id { get; set; }
public string Name { get; set; }
public string Website { get; set; }
public DateTime Created { get; set; }
} }
} }

View File

@ -6,7 +6,7 @@ using Iesi.Collections.Generic;
using Portoa.Persistence; using Portoa.Persistence;
namespace VideoGameQuotes.Api { namespace VideoGameQuotes.Api {
public class Quote : Entity<Quote, int> { public class Quote : Entity<Quote, int>, IDtoMappable<QuoteDto> {
private readonly Iesi.Collections.Generic.ISet<Vote> votes = new HashedSet<Vote>(); private readonly Iesi.Collections.Generic.ISet<Vote> votes = new HashedSet<Vote>();
private readonly Iesi.Collections.Generic.ISet<QuoteFlag> flags = new HashedSet<QuoteFlag>(); private readonly Iesi.Collections.Generic.ISet<QuoteFlag> flags = new HashedSet<QuoteFlag>();
private readonly Iesi.Collections.Generic.ISet<Category> categories = new HashedSet<Category>(); private readonly Iesi.Collections.Generic.ISet<Category> categories = new HashedSet<Category>();
@ -81,7 +81,32 @@ namespace VideoGameQuotes.Api {
public virtual int UpVotes { get { return Votes.Count(vote => vote.Direction == VoteDirection.Up); } } public virtual int UpVotes { get { return Votes.Count(vote => vote.Direction == VoteDirection.Up); } }
public virtual int DownVotes { get { return Votes.Count(vote => vote.Direction == VoteDirection.Down); } } public virtual int DownVotes { get { return Votes.Count(vote => vote.Direction == VoteDirection.Down); } }
public virtual int NetVotes { get { return Votes.Sum(vote => (int)vote); } } public virtual int Score { get { return Votes.Sum(vote => (int)vote); } }
public virtual QuoteDto ToDto() {
return new QuoteDto {
Id = Id,
Text = Text,
Game = Game.ToDto(),
Created = Created,
Categories = Categories.Select(category => category.ToDto()),
TotalVotes = Votes.Count(),
UpVotes = UpVotes,
DownVotes = DownVotes,
Score = Score
};
}
}
public class QuoteDto {
public int Id { get; set; }
public string Text { get; set; }
public GameDto Game { get; set; }
public DateTime Created { get; set; }
public IEnumerable<CategoryDto> Categories { get; set; }
public int TotalVotes { get; set; }
public int UpVotes { get; set; }
public int DownVotes { get; set; }
public int Score { get; set; }
} }
public static class QuoteExtensions { public static class QuoteExtensions {

View File

@ -65,8 +65,10 @@
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="ApiException.cs" />
<Compile Include="CannotVoteTwiceException.cs" /> <Compile Include="CannotVoteTwiceException.cs" />
<Compile Include="Category.cs" /> <Compile Include="Category.cs" />
<Compile Include="CriterionHandler.cs" />
<Compile Include="Game.cs" /> <Compile Include="Game.cs" />
<Compile Include="ICurrentUserProvider.cs" /> <Compile Include="ICurrentUserProvider.cs" />
<Compile Include="Persistence\IUserRepository.cs" /> <Compile Include="Persistence\IUserRepository.cs" />

View File

@ -2,7 +2,7 @@
using Portoa.Persistence; using Portoa.Persistence;
namespace VideoGameQuotes.Api { namespace VideoGameQuotes.Api {
public class Vote : Entity<Vote, int> { public class Vote : Entity<Vote, int>, IDtoMappable<VoteDto> {
public Vote() { public Vote() {
Created = DateTime.UtcNow; Created = DateTime.UtcNow;
} }
@ -15,5 +15,19 @@ namespace VideoGameQuotes.Api {
public static explicit operator int(Vote vote) { public static explicit operator int(Vote vote) {
return vote.Direction == VoteDirection.Up ? 1 : -1; return vote.Direction == VoteDirection.Up ? 1 : -1;
} }
public virtual VoteDto ToDto() {
return new VoteDto {
Id = Id,
Created = Created,
Direction = Direction
};
}
}
public class VoteDto {
public int Id { get; set; }
public DateTime Created { get; set; }
public VoteDirection Direction { get; set; }
} }
} }

View File

@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Mvc;
using Portoa.Persistence;
using Portoa.Web.Controllers;
using Portoa.Web.Results;
using VideoGameQuotes.Api;
using VideoGameQuotes.Web.Models;
namespace VideoGameQuotes.Web.Controllers {
public interface IApiService {
IEnumerable<GameDto> GetGames(ApiModel model);
IEnumerable<SystemDto> GetSystems(ApiModel model);
IEnumerable<CategoryDto> GetCategories(ApiModel model);
IEnumerable<PublisherDto> GetPublishers(ApiModel model);
IEnumerable<QuoteDto> GetQuotes(ApiModel model);
}
public class ApiService : IApiService {
private readonly IRepository<Game> gameRepository;
private readonly IRepository<GamingSystem> systemRepository;
private readonly IRepository<Category> categoryRepository;
private readonly IRepository<Publisher> publisherRepository;
private readonly IRepository<Quote> quoteRepository;
private readonly IDictionary<string, CriterionHandler<Category>> categoryHandlers = new Dictionary<string, CriterionHandler<Category>>();
private readonly IDictionary<string, CriterionHandler<GamingSystem>> systemHandlers = new Dictionary<string, CriterionHandler<GamingSystem>>();
private readonly IDictionary<string, CriterionHandler<Publisher>> publisherHandlers = new Dictionary<string, CriterionHandler<Publisher>>();
private readonly IDictionary<string, CriterionHandler<Quote>> quoteHandlers = new Dictionary<string, CriterionHandler<Quote>>();
private readonly IDictionary<string, CriterionHandler<Game>> gameHandlers = new Dictionary<string, CriterionHandler<Game>> {
{ "system", new Game.SystemCriterionHandler() },
{ "publisher", new Game.PublisherCriterionHandler() }
};
public ApiService(
IRepository<Game> gameRepository,
IRepository<GamingSystem> systemRepository,
IRepository<Category> categoryRepository,
IRepository<Publisher> publisherRepository,
IRepository<Quote> quoteRepository
) {
this.gameRepository = gameRepository;
this.systemRepository = systemRepository;
this.categoryRepository = categoryRepository;
this.publisherRepository = publisherRepository;
this.quoteRepository = quoteRepository;
}
private static IEnumerable<T> SetSortMethod<T, TKey>(IEnumerable<T> records, SortMethod sortMethod, SortOrder sortOrder, Func<T, TKey> propertySelector) where T : Entity<T, int> {
switch (sortMethod) {
case SortMethod.Alphabetical:
return sortOrder == SortOrder.Descending ? records.OrderByDescending(propertySelector) : records.OrderBy(propertySelector);
case SortMethod.Date:
return sortOrder == SortOrder.Descending ? records.OrderByDescending(propertySelector) : records.OrderBy(propertySelector);
default:
return records;
}
}
private static IEnumerable<TDto> GetRecords<T, TDto>(
ApiModel model,
IRepository<T> repository,
Func<T, object> sortSelector,
IDictionary<string,
CriterionHandler<T>> criterionHandlers
)
where T : Entity<T, int>
where TDto : new() {
IEnumerable<T> records;
if (model.FetchAll) {
records = SetSortMethod(repository.Records, model.SortMethod, model.SortOrder, sortSelector);
var expressionBuilder = new List<Func<T, bool>>();
foreach (var kvp in model.Criteria) {
if (!criterionHandlers.ContainsKey(kvp.Key)) {
throw new ApiException(string.Format("Unknown criterion: \"{0}\"", kvp.Key));
}
expressionBuilder.AddRange(criterionHandlers[kvp.Key].HandleCriterion(kvp.Value));
}
if (expressionBuilder.Count > 0) {
records = records.Where(game => expressionBuilder.Aggregate(false, (current, next) => current || next(game)));
}
} else {
records = new[] { repository.FindById(model.Id) };
}
return records.ToArray().Select(entity => entity.ToDto<T, int, TDto>());
}
[UnitOfWork]
public IEnumerable<GameDto> GetGames(ApiModel model) {
return GetRecords<Game, GameDto>(model, gameRepository, game => game.Name, gameHandlers);
}
[UnitOfWork]
public IEnumerable<SystemDto> GetSystems(ApiModel model) {
return GetRecords<GamingSystem, SystemDto>(model, systemRepository, system => system.Name, systemHandlers);
}
[UnitOfWork]
public IEnumerable<CategoryDto> GetCategories(ApiModel model) {
return GetRecords<Category, CategoryDto>(model, categoryRepository, category => category.Name, categoryHandlers);
}
[UnitOfWork]
public IEnumerable<PublisherDto> GetPublishers(ApiModel model) {
return GetRecords<Publisher, PublisherDto>(model, publisherRepository, publisher => publisher.Name, publisherHandlers);
}
[UnitOfWork]
public IEnumerable<QuoteDto> GetQuotes(ApiModel model) {
return GetRecords<Quote, QuoteDto>(model, quoteRepository, quote => quote.Id, quoteHandlers);
}
}
public class ApiController : Controller {
private readonly IApiService apiService;
public ApiController(IApiService apiService) {
this.apiService = apiService;
}
protected new JsonResult Json(object data) {
return Json(data, JsonRequestBehavior.AllowGet);
}
private ActionResult Error(HttpStatusCode statusCode, string message = null) {
return new StatusOverrideResult(Json(this.CreateJsonErrorResponse(message ?? "Invalid request"))) { StatusCode = statusCode };
}
private ActionResult GetRecords<TDto>(Func<IApiService, IEnumerable<TDto>> recordGetter, string key) {
if (!ModelState.IsValid) {
return Error(HttpStatusCode.BadRequest);
}
try {
return Json(this.CreateJsonResponse(data: new Dictionary<string, object> { { key, recordGetter(apiService) } }));
} catch (ApiException e) {
return Error(HttpStatusCode.BadRequest, e.Message);
} catch {
return Error(HttpStatusCode.InternalServerError, "An error occurred while trying to fulfill your stupid request");
}
}
public ActionResult Game(ApiModel model) {
return GetRecords(service => service.GetGames(model), "games");
}
public ActionResult System(ApiModel model) {
return GetRecords(service => service.GetSystems(model), "systems");
}
public ActionResult Category(ApiModel model) {
return GetRecords(service => service.GetCategories(model), "categories");
}
public ActionResult Publisher(ApiModel model) {
return GetRecords(service => service.GetPublishers(model), "publishers");
}
public ActionResult Quote(ApiModel model) {
return GetRecords(service => service.GetQuotes(model), "quotes");
}
}
}

View File

@ -22,6 +22,29 @@ namespace VideoGameQuotes.Web.Controllers {
this.currentUserProvider = currentUserProvider; this.currentUserProvider = currentUserProvider;
} }
public ActionResult Browse(BrowseModel model) {
var viewModel = new BrowseViewModel();
if (model.GameIds.Any()) {
viewModel.Games = model.GameIds.Select(id => quoteService.GetGame(id));
}
if (model.SystemIds.Any()) {
viewModel.Systems = model.SystemIds.Select(id => quoteService.GetSystem(id));
}
if (model.PublisherIds.Any()) {
viewModel.Publishers = model.PublisherIds.Select(id => quoteService.GetPublisher(id));
}
if (model.CategoryIds.Any()) {
viewModel.Categories = model.CategoryIds.Select(id => quoteService.GetCategory(id));
}
if (!viewModel.Games.Any() && !viewModel.Systems.Any() && !viewModel.Categories.Any() && !viewModel.Publishers.Any()) {
return View("DefaultBrowse");
}
return View("QualifiedBrowse", viewModel);
}
[HttpPost, IsValidUser] [HttpPost, IsValidUser]
public JsonResult Report(ReportModel model) { public JsonResult Report(ReportModel model) {
if (!ModelState.IsValid) { if (!ModelState.IsValid) {
@ -47,11 +70,11 @@ namespace VideoGameQuotes.Web.Controllers {
try { try {
var vote = quoteService.GetVoteOrCreateNew(quoteService.GetQuote(model.QuoteId), currentUserProvider.CurrentUser); var vote = quoteService.GetVoteOrCreateNew(quoteService.GetQuote(model.QuoteId), currentUserProvider.CurrentUser);
vote.Direction = model.Direction; vote.Direction = model.Direction;
vote = quoteService.SaveVote(vote); vote = quoteService.SaveVote(vote);
var data = new Dictionary<string, string> { var data = new Dictionary<string, string> {
{ "netVotes", vote.Quote.NetVotes.ToString() }, { "netVotes", vote.Quote.Score.ToString() },
{ "upVotes", vote.Quote.UpVotes.ToString() }, { "upVotes", vote.Quote.UpVotes.ToString() },
{ "downVotes", vote.Quote.DownVotes.ToString() } { "downVotes", vote.Quote.DownVotes.ToString() }
}; };
@ -63,9 +86,9 @@ namespace VideoGameQuotes.Web.Controllers {
} }
public ActionResult Recent() { public ActionResult Recent() {
return View(new QuoteCollectionModel { return View(new QuoteCollectionModel {
Quotes = quoteService.GetMostRecentQuotes(10), Quotes = quoteService.GetMostRecentQuotes(10),
User = currentUserProvider.CurrentUser User = currentUserProvider.CurrentUser
}); });
} }
@ -74,8 +97,8 @@ namespace VideoGameQuotes.Web.Controllers {
return new StatusOverrideResult(View("BadPaging")) { StatusCode = HttpStatusCode.BadRequest }; return new StatusOverrideResult(View("BadPaging")) { StatusCode = HttpStatusCode.BadRequest };
} }
return View(new PagedQuoteCollectionModel { return View(new PagedQuoteCollectionModel {
Quotes = quoteService.GetBestQuotes(start, end), Quotes = quoteService.GetBestQuotes(start, end),
User = currentUserProvider.CurrentUser, User = currentUserProvider.CurrentUser,
Start = start, Start = start,
End = end End = end

View File

@ -7,6 +7,8 @@ using Portoa.Web.Unity;
using UnityGenerics; using UnityGenerics;
using VideoGameQuotes.Api; using VideoGameQuotes.Api;
using VideoGameQuotes.Api.Persistence; using VideoGameQuotes.Api.Persistence;
using VideoGameQuotes.Web.Controllers;
using VideoGameQuotes.Web.Models;
using VideoGameQuotes.Web.Security; using VideoGameQuotes.Web.Security;
using VideoGameQuotes.Web.Services; using VideoGameQuotes.Web.Services;
@ -14,7 +16,10 @@ namespace VideoGameQuotes.Web {
public class MvcApplication : MvcApplicationBase { public class MvcApplication : MvcApplicationBase {
protected override void ConfigureModelBinders(ModelBinderDictionary binders) { protected override void ConfigureModelBinders(ModelBinderDictionary binders) {
binders.Add<Region, FlagEnumModelBinder>(); binders
.Add<Region, FlagEnumModelBinder>()
.Add<BrowseModel, BrowseModelBinder>()
.Add<ApiModel, ApiModelBinder>();
} }
protected override void ConfigureUnity() { protected override void ConfigureUnity() {
@ -30,6 +35,7 @@ namespace VideoGameQuotes.Web {
.RegisterType<IsValidUserAttribute>(new InjectionProperty<IsValidUserAttribute>(attr => attr.UserProvider)) .RegisterType<IsValidUserAttribute>(new InjectionProperty<IsValidUserAttribute>(attr => attr.UserProvider))
.RegisterType<IUserService, UserService>() .RegisterType<IUserService, UserService>()
.RegisterType<IQuoteService, QuoteService>() .RegisterType<IQuoteService, QuoteService>()
.RegisterType<IApiService, ApiService>()
.RegisterType<IUserRepository, UserRepository>(); .RegisterType<IUserRepository, UserRepository>();
} }
@ -37,9 +43,14 @@ namespace VideoGameQuotes.Web {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("media/{*anything}"); routes.IgnoreRoute("media/{*anything}");
routes.MapRoute("api", "api/{action}/{id}/{*criteria}", new { controller = "Api" }, new { action = "game", id = @"\d+|all" });
routes.MapRoute("home", "{action}", new { controller = "Home", action = "Index" }, new { action = "about|contact" }); routes.MapRoute("home", "{action}", new { controller = "Home", action = "Index" }, new { action = "about|contact" });
routes.MapRoute("best", "best/{start}-{end}/", new { controller = "Quote", action = "Best" }, new { start = @"\d+", end = @"\d+" }); routes.MapRoute("best", "best/{start}-{end}/", new { controller = "Quote", action = "Best" }, new { start = @"\d+", end = @"\d+" });
routes.MapRoute("quote", "{action}", new { controller = "Quote" }, new { action = "submit|search|recent|random|best|browse|vote|report" }); routes.MapRoute("browse", "browse/{*qualifiers}", new { controller = "Quote", action = "Browse" });
routes.MapRoute("quote", "{action}", new { controller = "Quote" }, new { action = "submit|search|recent|random|best|vote|report" });
routes.MapRoute("individual-quote", "quote/{id}/{*text}", new { controller = "Quote", action = "Quote" }, new { id = @"\d+" }); routes.MapRoute("individual-quote", "quote/{id}/{*text}", new { controller = "Quote", action = "Quote" }, new { id = @"\d+" });
routes.MapRoute("create-category", "category/create", new { controller = "Quote", action = "CreateCategory" }); routes.MapRoute("create-category", "category/create", new { controller = "Quote", action = "CreateCategory" });

View File

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Portoa.Web;
namespace VideoGameQuotes.Web.Models {
public class ApiModel {
public ApiModel() {
Criteria = new Dictionary<string, IEnumerable<object>>();
}
public bool FetchAll { get; set; }
public int Id { get; set; }
public SortMethod SortMethod { get; set; }
public SortOrder SortOrder { get; set; }
public IDictionary<string, IEnumerable<object>> Criteria { get; set; }
}
public class ApiModelBinder : IModelBinder {
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
var model = new ApiModel {
SortMethod = SortMethod.None,
SortOrder = SortOrder.Ascending
};
ParseId(controllerContext, model);
ParseSort(controllerContext, model);
ParseCriteria(controllerContext, model);
return model;
}
private static void ParseCriteria(ControllerContext controllerContext, ApiModel model) {
var criteria = (controllerContext.RouteData.Values["criteria"] ?? string.Empty).ToString().Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < criteria.Length; i++) {
switch (criteria[i]) {
case "system":
if (i < criteria.Length - 1) {
model.Criteria["system"] = ParseCommaSeparatedCriterion(criteria[i + 1]);
i++;
} else {
controllerContext.AddModelError("criteria", "Unable to parse criteria for key \"system\"");
}
break;
default:
controllerContext.AddModelError("criteria", string.Format("Unable to parse criteria for key \"{0}\"", criteria[i]));
break;
}
}
}
private static IEnumerable<object> ParseCommaSeparatedCriterion(string csv) {
var values = csv.Split(',');
foreach (var value in values) {
int result;
if (int.TryParse(value, out result)) {
yield return result;
} else {
yield return value;
}
}
}
private static void ParseSort(ControllerContext controllerContext, ApiModel model) {
var sortValue = controllerContext.HttpContext.Request.QueryString["sort"];
if (string.IsNullOrWhiteSpace(sortValue)) {
return;
}
var sortValues = sortValue.Split('|');
try {
model.SortMethod = (SortMethod)Enum.Parse(typeof(SortMethod), sortValues[0], ignoreCase: true);
} catch {
controllerContext.AddModelError("sort", string.Format("The sort method \"{0}\" is invalid", sortValues[0]));
}
if (sortValues.Length > 1) {
try {
model.SortOrder = (SortOrder)Enum.Parse(typeof(SortOrder), sortValues[1], ignoreCase: true);
} catch {
controllerContext.AddModelError("sort", string.Format("The sort order \"{0}\" is invalid", sortValues[1]));
}
}
}
private static void ParseId(ControllerContext controllerContext, ApiModel model) {
var idValue = (controllerContext.RouteData.Values["id"] ?? string.Empty).ToString();
int id;
if (int.TryParse(idValue, out id)) {
if (id <= 0) {
controllerContext.AddModelError("id", "Invalid value for id");
} else {
model.Id = id;
}
} else {
model.FetchAll = idValue == "all";
}
}
}
}

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Linq;
namespace VideoGameQuotes.Web.Models {
public class BrowseModel {
public BrowseModel() {
GameIds = Enumerable.Empty<int>();
SystemIds = Enumerable.Empty<int>();
CategoryIds = Enumerable.Empty<int>();
PublisherIds = Enumerable.Empty<int>();
}
public IEnumerable<int> GameIds { get; set; }
public IEnumerable<int> SystemIds { get; set; }
public IEnumerable<int> CategoryIds { get; set; }
public IEnumerable<int> PublisherIds { get; set; }
public SortMethod SortMethod { get; set; }
public SortOrder SortOrder { get; set; }
}
public enum SortMethod {
None,
Alphabetical,
Rating,
Date
}
public enum SortOrder {
Ascending,
Descending
}
}

View File

@ -0,0 +1,80 @@
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Portoa.Web;
namespace VideoGameQuotes.Web.Models {
public class BrowseModelBinder : IModelBinder {
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
var model = new BrowseModel {
SortMethod = SortMethod.Alphabetical,
SortOrder = SortOrder.Ascending
};
var qualifierText = (controllerContext.RouteData.Values["qualifiers"] ?? string.Empty).ToString();
if (string.IsNullOrEmpty(qualifierText)) {
return model;
}
var segments = qualifierText.Split('/').ToArray();
for (var i = 0; i < segments.Length; i++) {
switch (segments[i]) {
case "game":
case "system":
case "category":
case "publisher":
if (i < segments.Length - 1) {
var ids = ParseCommaSeparatedIntegers(segments[i + 1]);
switch (segments[i]) {
case "game":
model.GameIds = ids;
break;
case "system":
model.SystemIds = ids;
break;
case "category":
model.CategoryIds = ids;
break;
case "publisher":
model.PublisherIds = ids;
break;
}
i++; //loop unroll ftw
}
break;
case "alphabetical":
model.SortMethod = SortMethod.Alphabetical;
break;
case "date":
model.SortMethod = SortMethod.Date;
break;
case "rating":
model.SortMethod = SortMethod.Rating;
break;
case "asc":
model.SortOrder = SortOrder.Ascending;
break;
case "desc":
model.SortOrder = SortOrder.Descending;
break;
default:
controllerContext.AddModelError("qualifiers", "Unknown qualifier: " + segments[i]);
break;
}
}
return model;
}
private static IEnumerable<int> ParseCommaSeparatedIntegers(string value) {
foreach (var id in value.Split(',')) {
int result;
if (int.TryParse(id, out result)) {
yield return result;
}
}
}
}
}

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using VideoGameQuotes.Api;
namespace VideoGameQuotes.Web.Models {
public class BrowseViewModel {
public BrowseViewModel() {
Games = Enumerable.Empty<Game>();
Systems = Enumerable.Empty<GamingSystem>();
Categories = Enumerable.Empty<Category>();
Publishers = Enumerable.Empty<Publisher>();
}
public IEnumerable<Game> Games { get; set; }
public IEnumerable<GamingSystem> Systems { get; set; }
public IEnumerable<Category> Categories { get; set; }
public IEnumerable<Publisher> Publishers { get; set; }
}
}

View File

@ -84,7 +84,12 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Controllers\ApiController.cs" />
<Compile Include="Controllers\HomeController.cs" /> <Compile Include="Controllers\HomeController.cs" />
<Compile Include="Models\ApiModel.cs" />
<Compile Include="Models\BrowseModel.cs" />
<Compile Include="Models\BrowseModelBinder.cs" />
<Compile Include="Models\BrowseViewModel.cs" />
<Compile Include="Models\PagedQuoteCollectionModel.cs" /> <Compile Include="Models\PagedQuoteCollectionModel.cs" />
<Compile Include="Models\QuoteCollectionModel.cs" /> <Compile Include="Models\QuoteCollectionModel.cs" />
<Compile Include="Models\QuoteModel.cs" /> <Compile Include="Models\QuoteModel.cs" />
@ -123,6 +128,8 @@
<Content Include="Views\Home\ContactSuccess.aspx" /> <Content Include="Views\Home\ContactSuccess.aspx" />
<Content Include="Views\Quote\BadPaging.aspx" /> <Content Include="Views\Quote\BadPaging.aspx" />
<Content Include="Views\Quote\Best.aspx" /> <Content Include="Views\Quote\Best.aspx" />
<Content Include="Views\Quote\QualifiedBrowse.aspx" />
<Content Include="Views\Quote\DefaultBrowse.aspx" />
<Content Include="Views\Quote\NoQuotes.aspx" /> <Content Include="Views\Quote\NoQuotes.aspx" />
<Content Include="Views\Quote\Quote.aspx" /> <Content Include="Views\Quote\Quote.aspx" />
<Content Include="Views\Quote\QuoteNotFound.aspx" /> <Content Include="Views\Quote\QuoteNotFound.aspx" />

View File

@ -0,0 +1,99 @@
<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage" MasterPageFile="~/Views/Shared/Site.Master" %>
<asp:Content runat="server" ID="Title" ContentPlaceHolderID="TitleContent">Browse</asp:Content>
<asp:Content runat="server" ID="Main" ContentPlaceHolderID="MainContent">
<p>
Browse by:
</p>
<div id="browse-default-menu">
<ul>
<li><a href="#" id="browse-game">Game</a></li>
<li><a href="#" id="browse-system">System</a></li>
<li><a href="#" id="browse-category">Category</a></li>
<li><a href="#" id="browse-publisher">Publisher</a></li>
</ul>
</div>
<div id="browse-default-container">
<p>
<a href="#" id="show-default-menu">Back</a>
</p>
<div id="browse-default-content"></div>
</div>
</asp:Content>
<asp:Content ContentPlaceHolderID="DeferrableScripts" runat="server">
<script type="text/javascript">//<![CDATA[
$(document).ready(function() {
var $browseMenu = $("#browse-default-menu");
var $container = $("#browse-default-container");
var $content = $("#browse-default-content");
var games = [], systems = [], publishers = [], categories = [];
var renderData = function(data, cellRenderer, cellsPerRow) {
var $table = $("<table/>"), $row = $("<tr/>");
for (var i = 0, len = data.length; i < len; i++) {
if (i % cellsPerRow === 0) {
if (i > 0) {
$table.append($row);
}
$row = $("<tr/>");
}
$row.append($("<td/>").html(cellRenderer(data[i], i)));
}
if (data.length > 0) {
$table.append($row);
}
$content.append($table);
};
var gameCellRenderer = function() {
var $template = $("<a/>");
return function(game, count) {
return $template
.clone()
.attr("href", "/browse/game/" + game.Id)
.text(game.Name);
};
}();
$("#show-default-menu").click(function() {
$content.empty();
$container.hide();
$browseMenu.show();
return false;
});
$("#browse-game").click(function() {
$browseMenu.hide();
$container.show();
var cellsPerRow = 8;
if (games.length === 0) {
$.ajax("/api/game/all", {
data: { sort: "alphabetical" },
success: function(data, status, $xhr) {
if (data.Error !== null) {
alert(data.Error);
return;
}
games = data.Data.games;
renderData(games, gameCellRenderer, cellsPerRow);
}
});
} else {
renderData(games, gameCellRenderer, cellsPerRow);
}
return false;
});
});
//]]></script>
</asp:Content>

View File

@ -0,0 +1,8 @@
<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage<VideoGameQuotes.Web.Models.BrowseViewModel>" MasterPageFile="~/Views/Shared/Site.Master" %>
<asp:Content runat="server" ID="Title" ContentPlaceHolderID="TitleContent">Browse</asp:Content>
<asp:Content runat="server" ID="Main" ContentPlaceHolderID="MainContent">
<p>
This is the qualified browse view.
</p>
</asp:Content>

View File

@ -24,7 +24,7 @@
<ul class="clearfix menu"> <ul class="clearfix menu">
<li><%= Html.ActionLink("Recent", "recent", "Quote", null, new { title = "View most recently submitted quotes" })%></li> <li><%= Html.ActionLink("Recent", "recent", "Quote", null, new { title = "View most recently submitted quotes" })%></li>
<li><%= Html.RouteLink("Best", "quote", new { action = "best" }, new { title = "View the top rated quotes" })%></li> <li><%= Html.RouteLink("Best", "quote", new { action = "best" }, new { title = "View the top rated quotes" })%></li>
<li><%= Html.ActionLink("Browse", "browse", "Quote", null, new { title = "Browse the quote database" })%></li> <li><%= Html.ActionLink("Browse", "browse", "Quote", new { qualifiers = "" }, new { title = "Browse the quote database" })%></li>
<li><%= Html.ActionLink("Random", "random", "Quote", null, new { title = "View a random quote" })%></li> <li><%= Html.ActionLink("Random", "random", "Quote", null, new { title = "View a random quote" })%></li>
<li><%= Html.ActionLink("Submit", "submit", "Quote", null, new { title = "Submit a new quote" }) %></li> <li><%= Html.ActionLink("Submit", "submit", "Quote", null, new { title = "Submit a new quote" }) %></li>
<li><%= Html.ActionLink("About", "about", "Home", null, new { title = "About the site" })%></li> <li><%= Html.ActionLink("About", "about", "Home", null, new { title = "About the site" })%></li>

View File

@ -115,6 +115,14 @@ ul.menu li {
width: 600px; width: 600px;
} }
#browse-default-menu li {
margin-bottom: 2px;
padding: 3px;
}
#browse-default-container {
display: none;
}
#header { #header {
background-color: #669966; background-color: #669966;
} }