diff --git a/Src/VideoGameQuotes.Api/ApiException.cs b/Src/VideoGameQuotes.Api/ApiException.cs new file mode 100644 index 0000000..4d83db2 --- /dev/null +++ b/Src/VideoGameQuotes.Api/ApiException.cs @@ -0,0 +1,7 @@ +using System; + +namespace VideoGameQuotes.Api { + public class ApiException : Exception { + public ApiException(string message = null, Exception innerException = null) : base(message, innerException) { } + } +} \ No newline at end of file diff --git a/Src/VideoGameQuotes.Api/Category.cs b/Src/VideoGameQuotes.Api/Category.cs index da58c35..4e4ed16 100644 --- a/Src/VideoGameQuotes.Api/Category.cs +++ b/Src/VideoGameQuotes.Api/Category.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using Portoa.Persistence; namespace VideoGameQuotes.Api { - public class Category : Entity { + public class Category : Entity, IDtoMappable { public Category() { Created = DateTime.UtcNow; } @@ -11,5 +11,19 @@ namespace VideoGameQuotes.Api { public virtual DateTime Created { get; set; } [Required, StringLength(255)] 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; } } } \ No newline at end of file diff --git a/Src/VideoGameQuotes.Api/CriterionHandler.cs b/Src/VideoGameQuotes.Api/CriterionHandler.cs new file mode 100644 index 0000000..28728a4 --- /dev/null +++ b/Src/VideoGameQuotes.Api/CriterionHandler.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Portoa.Persistence; + +namespace VideoGameQuotes.Api { + public abstract class CriterionHandler where T : Entity { + public IEnumerable> HandleCriterion(IEnumerable values) { + foreach (var value in values) { + if (value is int) { + yield return HandleInteger((int)value); + } else { + yield return HandleString((string)value); + } + } + } + + protected virtual Func HandleInteger(int value) { + throw new ApiException(string.Format("The value \"{0}\" is invalid", value)); + } + + protected virtual Func HandleString(string value) { + throw new ApiException(string.Format("The value \"{0}\" is invalid", value)); + } + } +} \ No newline at end of file diff --git a/Src/VideoGameQuotes.Api/Game.cs b/Src/VideoGameQuotes.Api/Game.cs index e1471cd..e47a90c 100644 --- a/Src/VideoGameQuotes.Api/Game.cs +++ b/Src/VideoGameQuotes.Api/Game.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using Iesi.Collections.Generic; using JetBrains.Annotations; using Portoa.Persistence; namespace VideoGameQuotes.Api { - public class Game : Entity { + public class Game : Entity, IDtoMappable { private readonly Iesi.Collections.Generic.ISet systems = new HashedSet(); private readonly Iesi.Collections.Generic.ISet publishers = new HashedSet(); @@ -42,5 +43,49 @@ namespace VideoGameQuotes.Api { publishers.Clear(); } #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 { + protected override Func HandleInteger(int value) { + return game => game.Systems.Any(system => system.Id == value); + } + + protected override Func HandleString(string value) { + return game => game.Systems.Any(system => system.Name == value); + } + } + + public sealed class PublisherCriterionHandler : CriterionHandler { + protected override Func HandleInteger(int value) { + return game => game.Publishers.Any(publisher => publisher.Id == value); + } + + protected override Func 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 Systems { get; set; } + public string Name { get; set; } + public IEnumerable Publishers { get; set; } + public Region Region { get; set; } } } \ No newline at end of file diff --git a/Src/VideoGameQuotes.Api/GamingSystem.cs b/Src/VideoGameQuotes.Api/GamingSystem.cs index c2127de..f395fcb 100644 --- a/Src/VideoGameQuotes.Api/GamingSystem.cs +++ b/Src/VideoGameQuotes.Api/GamingSystem.cs @@ -2,7 +2,7 @@ using Portoa.Persistence; namespace VideoGameQuotes.Api { - public class GamingSystem : Entity { + public class GamingSystem : Entity, IDtoMappable { public GamingSystem() { Created = DateTime.UtcNow; } @@ -11,5 +11,23 @@ namespace VideoGameQuotes.Api { public virtual DateTime ReleaseDate { get; set; } public virtual string Name { 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; } } } \ No newline at end of file diff --git a/Src/VideoGameQuotes.Api/Publisher.cs b/Src/VideoGameQuotes.Api/Publisher.cs index 4331123..942bbd6 100644 --- a/Src/VideoGameQuotes.Api/Publisher.cs +++ b/Src/VideoGameQuotes.Api/Publisher.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using Portoa.Persistence; namespace VideoGameQuotes.Api { - public class Publisher : Entity { + public class Publisher : Entity, IDtoMappable { public Publisher() { Created = DateTime.UtcNow; } @@ -12,5 +12,21 @@ namespace VideoGameQuotes.Api { public virtual string Name { get; set; } public virtual string Website { 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; } } } \ No newline at end of file diff --git a/Src/VideoGameQuotes.Api/Quote.cs b/Src/VideoGameQuotes.Api/Quote.cs index 0c81665..4e7e57c 100644 --- a/Src/VideoGameQuotes.Api/Quote.cs +++ b/Src/VideoGameQuotes.Api/Quote.cs @@ -6,7 +6,7 @@ using Iesi.Collections.Generic; using Portoa.Persistence; namespace VideoGameQuotes.Api { - public class Quote : Entity { + public class Quote : Entity, IDtoMappable { private readonly Iesi.Collections.Generic.ISet votes = new HashedSet(); private readonly Iesi.Collections.Generic.ISet flags = new HashedSet(); private readonly Iesi.Collections.Generic.ISet categories = new HashedSet(); @@ -81,7 +81,32 @@ namespace VideoGameQuotes.Api { 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 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 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 { diff --git a/Src/VideoGameQuotes.Api/VideoGameQuotes.Api.csproj b/Src/VideoGameQuotes.Api/VideoGameQuotes.Api.csproj index 52a8676..010aea4 100644 --- a/Src/VideoGameQuotes.Api/VideoGameQuotes.Api.csproj +++ b/Src/VideoGameQuotes.Api/VideoGameQuotes.Api.csproj @@ -65,8 +65,10 @@ + + diff --git a/Src/VideoGameQuotes.Api/Vote.cs b/Src/VideoGameQuotes.Api/Vote.cs index 9bf4879..6c13b85 100644 --- a/Src/VideoGameQuotes.Api/Vote.cs +++ b/Src/VideoGameQuotes.Api/Vote.cs @@ -2,7 +2,7 @@ using Portoa.Persistence; namespace VideoGameQuotes.Api { - public class Vote : Entity { + public class Vote : Entity, IDtoMappable { public Vote() { Created = DateTime.UtcNow; } @@ -15,5 +15,19 @@ namespace VideoGameQuotes.Api { public static explicit operator int(Vote vote) { 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; } } } \ No newline at end of file diff --git a/Src/VideoGameQuotes.Web/Controllers/ApiController.cs b/Src/VideoGameQuotes.Web/Controllers/ApiController.cs new file mode 100644 index 0000000..3ac35d4 --- /dev/null +++ b/Src/VideoGameQuotes.Web/Controllers/ApiController.cs @@ -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 GetGames(ApiModel model); + IEnumerable GetSystems(ApiModel model); + IEnumerable GetCategories(ApiModel model); + IEnumerable GetPublishers(ApiModel model); + IEnumerable GetQuotes(ApiModel model); + } + + public class ApiService : IApiService { + private readonly IRepository gameRepository; + private readonly IRepository systemRepository; + private readonly IRepository categoryRepository; + private readonly IRepository publisherRepository; + private readonly IRepository quoteRepository; + + private readonly IDictionary> categoryHandlers = new Dictionary>(); + private readonly IDictionary> systemHandlers = new Dictionary>(); + private readonly IDictionary> publisherHandlers = new Dictionary>(); + private readonly IDictionary> quoteHandlers = new Dictionary>(); + private readonly IDictionary> gameHandlers = new Dictionary> { + { "system", new Game.SystemCriterionHandler() }, + { "publisher", new Game.PublisherCriterionHandler() } + }; + + public ApiService( + IRepository gameRepository, + IRepository systemRepository, + IRepository categoryRepository, + IRepository publisherRepository, + IRepository quoteRepository + ) { + this.gameRepository = gameRepository; + this.systemRepository = systemRepository; + this.categoryRepository = categoryRepository; + this.publisherRepository = publisherRepository; + this.quoteRepository = quoteRepository; + } + + private static IEnumerable SetSortMethod(IEnumerable records, SortMethod sortMethod, SortOrder sortOrder, Func propertySelector) where T : Entity { + 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 GetRecords( + ApiModel model, + IRepository repository, + Func sortSelector, + IDictionary> criterionHandlers + ) + where T : Entity + where TDto : new() { + IEnumerable records; + if (model.FetchAll) { + records = SetSortMethod(repository.Records, model.SortMethod, model.SortOrder, sortSelector); + var expressionBuilder = new List>(); + 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()); + } + + [UnitOfWork] + public IEnumerable GetGames(ApiModel model) { + return GetRecords(model, gameRepository, game => game.Name, gameHandlers); + } + + [UnitOfWork] + public IEnumerable GetSystems(ApiModel model) { + return GetRecords(model, systemRepository, system => system.Name, systemHandlers); + } + + [UnitOfWork] + public IEnumerable GetCategories(ApiModel model) { + return GetRecords(model, categoryRepository, category => category.Name, categoryHandlers); + } + + [UnitOfWork] + public IEnumerable GetPublishers(ApiModel model) { + return GetRecords(model, publisherRepository, publisher => publisher.Name, publisherHandlers); + } + + [UnitOfWork] + public IEnumerable GetQuotes(ApiModel model) { + return GetRecords(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(Func> recordGetter, string key) { + if (!ModelState.IsValid) { + return Error(HttpStatusCode.BadRequest); + } + + try { + return Json(this.CreateJsonResponse(data: new Dictionary { { 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"); + } + } +} \ No newline at end of file diff --git a/Src/VideoGameQuotes.Web/Controllers/QuoteController.cs b/Src/VideoGameQuotes.Web/Controllers/QuoteController.cs index adbdb96..b5b2f15 100644 --- a/Src/VideoGameQuotes.Web/Controllers/QuoteController.cs +++ b/Src/VideoGameQuotes.Web/Controllers/QuoteController.cs @@ -22,6 +22,29 @@ namespace VideoGameQuotes.Web.Controllers { 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] public JsonResult Report(ReportModel model) { if (!ModelState.IsValid) { @@ -47,11 +70,11 @@ namespace VideoGameQuotes.Web.Controllers { try { var vote = quoteService.GetVoteOrCreateNew(quoteService.GetQuote(model.QuoteId), currentUserProvider.CurrentUser); vote.Direction = model.Direction; - + vote = quoteService.SaveVote(vote); var data = new Dictionary { - { "netVotes", vote.Quote.NetVotes.ToString() }, + { "netVotes", vote.Quote.Score.ToString() }, { "upVotes", vote.Quote.UpVotes.ToString() }, { "downVotes", vote.Quote.DownVotes.ToString() } }; @@ -63,9 +86,9 @@ namespace VideoGameQuotes.Web.Controllers { } public ActionResult Recent() { - return View(new QuoteCollectionModel { - Quotes = quoteService.GetMostRecentQuotes(10), - User = currentUserProvider.CurrentUser + return View(new QuoteCollectionModel { + Quotes = quoteService.GetMostRecentQuotes(10), + User = currentUserProvider.CurrentUser }); } @@ -74,8 +97,8 @@ namespace VideoGameQuotes.Web.Controllers { return new StatusOverrideResult(View("BadPaging")) { StatusCode = HttpStatusCode.BadRequest }; } - return View(new PagedQuoteCollectionModel { - Quotes = quoteService.GetBestQuotes(start, end), + return View(new PagedQuoteCollectionModel { + Quotes = quoteService.GetBestQuotes(start, end), User = currentUserProvider.CurrentUser, Start = start, End = end diff --git a/Src/VideoGameQuotes.Web/Global.asax.cs b/Src/VideoGameQuotes.Web/Global.asax.cs index 0138f7a..dfff90b 100644 --- a/Src/VideoGameQuotes.Web/Global.asax.cs +++ b/Src/VideoGameQuotes.Web/Global.asax.cs @@ -7,6 +7,8 @@ using Portoa.Web.Unity; using UnityGenerics; using VideoGameQuotes.Api; using VideoGameQuotes.Api.Persistence; +using VideoGameQuotes.Web.Controllers; +using VideoGameQuotes.Web.Models; using VideoGameQuotes.Web.Security; using VideoGameQuotes.Web.Services; @@ -14,7 +16,10 @@ namespace VideoGameQuotes.Web { public class MvcApplication : MvcApplicationBase { protected override void ConfigureModelBinders(ModelBinderDictionary binders) { - binders.Add(); + binders + .Add() + .Add() + .Add(); } protected override void ConfigureUnity() { @@ -30,6 +35,7 @@ namespace VideoGameQuotes.Web { .RegisterType(new InjectionProperty(attr => attr.UserProvider)) .RegisterType() .RegisterType() + .RegisterType() .RegisterType(); } @@ -37,9 +43,14 @@ namespace VideoGameQuotes.Web { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 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("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("create-category", "category/create", new { controller = "Quote", action = "CreateCategory" }); diff --git a/Src/VideoGameQuotes.Web/Models/ApiModel.cs b/Src/VideoGameQuotes.Web/Models/ApiModel.cs new file mode 100644 index 0000000..0dc7bd6 --- /dev/null +++ b/Src/VideoGameQuotes.Web/Models/ApiModel.cs @@ -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>(); + } + + public bool FetchAll { get; set; } + public int Id { get; set; } + public SortMethod SortMethod { get; set; } + public SortOrder SortOrder { get; set; } + public IDictionary> 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 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"; + } + } + } +} \ No newline at end of file diff --git a/Src/VideoGameQuotes.Web/Models/BrowseModel.cs b/Src/VideoGameQuotes.Web/Models/BrowseModel.cs new file mode 100644 index 0000000..2168cfa --- /dev/null +++ b/Src/VideoGameQuotes.Web/Models/BrowseModel.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace VideoGameQuotes.Web.Models { + public class BrowseModel { + public BrowseModel() { + GameIds = Enumerable.Empty(); + SystemIds = Enumerable.Empty(); + CategoryIds = Enumerable.Empty(); + PublisherIds = Enumerable.Empty(); + } + + public IEnumerable GameIds { get; set; } + public IEnumerable SystemIds { get; set; } + public IEnumerable CategoryIds { get; set; } + public IEnumerable 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 + } +} \ No newline at end of file diff --git a/Src/VideoGameQuotes.Web/Models/BrowseModelBinder.cs b/Src/VideoGameQuotes.Web/Models/BrowseModelBinder.cs new file mode 100644 index 0000000..c325ece --- /dev/null +++ b/Src/VideoGameQuotes.Web/Models/BrowseModelBinder.cs @@ -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 ParseCommaSeparatedIntegers(string value) { + foreach (var id in value.Split(',')) { + int result; + if (int.TryParse(id, out result)) { + yield return result; + } + } + } + } +} \ No newline at end of file diff --git a/Src/VideoGameQuotes.Web/Models/BrowseViewModel.cs b/Src/VideoGameQuotes.Web/Models/BrowseViewModel.cs new file mode 100644 index 0000000..d6dadce --- /dev/null +++ b/Src/VideoGameQuotes.Web/Models/BrowseViewModel.cs @@ -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(); + Systems = Enumerable.Empty(); + Categories = Enumerable.Empty(); + Publishers = Enumerable.Empty(); + } + + public IEnumerable Games { get; set; } + public IEnumerable Systems { get; set; } + public IEnumerable Categories { get; set; } + public IEnumerable Publishers { get; set; } + } +} \ No newline at end of file diff --git a/Src/VideoGameQuotes.Web/VideoGameQuotes.Web.csproj b/Src/VideoGameQuotes.Web/VideoGameQuotes.Web.csproj index 4f93c51..05a5e6e 100644 --- a/Src/VideoGameQuotes.Web/VideoGameQuotes.Web.csproj +++ b/Src/VideoGameQuotes.Web/VideoGameQuotes.Web.csproj @@ -84,7 +84,12 @@ + + + + + @@ -123,6 +128,8 @@ + + diff --git a/Src/VideoGameQuotes.Web/Views/Quote/DefaultBrowse.aspx b/Src/VideoGameQuotes.Web/Views/Quote/DefaultBrowse.aspx new file mode 100644 index 0000000..4930c5e --- /dev/null +++ b/Src/VideoGameQuotes.Web/Views/Quote/DefaultBrowse.aspx @@ -0,0 +1,99 @@ +<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage" MasterPageFile="~/Views/Shared/Site.Master" %> +Browse + +

+ Browse by: +

+ +
+ +
+ +
+

+ Back +

+ +
+
+
+ + + + diff --git a/Src/VideoGameQuotes.Web/Views/Quote/QualifiedBrowse.aspx b/Src/VideoGameQuotes.Web/Views/Quote/QualifiedBrowse.aspx new file mode 100644 index 0000000..c995d51 --- /dev/null +++ b/Src/VideoGameQuotes.Web/Views/Quote/QualifiedBrowse.aspx @@ -0,0 +1,8 @@ +<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage" MasterPageFile="~/Views/Shared/Site.Master" %> +Browse + + +

+ This is the qualified browse view. +

+
\ No newline at end of file diff --git a/Src/VideoGameQuotes.Web/Views/Shared/Site.Master b/Src/VideoGameQuotes.Web/Views/Shared/Site.Master index d38b7c6..70108d4 100644 --- a/Src/VideoGameQuotes.Web/Views/Shared/Site.Master +++ b/Src/VideoGameQuotes.Web/Views/Shared/Site.Master @@ -24,7 +24,7 @@