* dto madness
* api madness * browse page is beginning to not be empty * NetVotes -> Score
This commit is contained in:
parent
5c14400d48
commit
14ca315213
7
Src/VideoGameQuotes.Api/ApiException.cs
Normal file
7
Src/VideoGameQuotes.Api/ApiException.cs
Normal 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) { }
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using Portoa.Persistence;
|
||||
|
||||
namespace VideoGameQuotes.Api {
|
||||
public class Category : Entity<Category, int> {
|
||||
public class Category : Entity<Category, int>, IDtoMappable<CategoryDto> {
|
||||
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; }
|
||||
}
|
||||
}
|
25
Src/VideoGameQuotes.Api/CriterionHandler.cs
Normal file
25
Src/VideoGameQuotes.Api/CriterionHandler.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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<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<Publisher> publishers = new HashedSet<Publisher>();
|
||||
|
||||
@ -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<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; }
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
using Portoa.Persistence;
|
||||
|
||||
namespace VideoGameQuotes.Api {
|
||||
public class GamingSystem : Entity<GamingSystem, int> {
|
||||
public class GamingSystem : Entity<GamingSystem, int>, IDtoMappable<SystemDto> {
|
||||
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; }
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using Portoa.Persistence;
|
||||
|
||||
namespace VideoGameQuotes.Api {
|
||||
public class Publisher : Entity<Publisher, int> {
|
||||
public class Publisher : Entity<Publisher, int>, IDtoMappable<PublisherDto> {
|
||||
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; }
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ using Iesi.Collections.Generic;
|
||||
using Portoa.Persistence;
|
||||
|
||||
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<QuoteFlag> flags = new HashedSet<QuoteFlag>();
|
||||
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 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 {
|
||||
|
@ -65,8 +65,10 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ApiException.cs" />
|
||||
<Compile Include="CannotVoteTwiceException.cs" />
|
||||
<Compile Include="Category.cs" />
|
||||
<Compile Include="CriterionHandler.cs" />
|
||||
<Compile Include="Game.cs" />
|
||||
<Compile Include="ICurrentUserProvider.cs" />
|
||||
<Compile Include="Persistence\IUserRepository.cs" />
|
||||
|
@ -2,7 +2,7 @@
|
||||
using Portoa.Persistence;
|
||||
|
||||
namespace VideoGameQuotes.Api {
|
||||
public class Vote : Entity<Vote, int> {
|
||||
public class Vote : Entity<Vote, int>, IDtoMappable<VoteDto> {
|
||||
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; }
|
||||
}
|
||||
}
|
170
Src/VideoGameQuotes.Web/Controllers/ApiController.cs
Normal file
170
Src/VideoGameQuotes.Web/Controllers/ApiController.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
@ -51,7 +74,7 @@ namespace VideoGameQuotes.Web.Controllers {
|
||||
vote = quoteService.SaveVote(vote);
|
||||
|
||||
var data = new Dictionary<string, string> {
|
||||
{ "netVotes", vote.Quote.NetVotes.ToString() },
|
||||
{ "netVotes", vote.Quote.Score.ToString() },
|
||||
{ "upVotes", vote.Quote.UpVotes.ToString() },
|
||||
{ "downVotes", vote.Quote.DownVotes.ToString() }
|
||||
};
|
||||
|
@ -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<Region, FlagEnumModelBinder>();
|
||||
binders
|
||||
.Add<Region, FlagEnumModelBinder>()
|
||||
.Add<BrowseModel, BrowseModelBinder>()
|
||||
.Add<ApiModel, ApiModelBinder>();
|
||||
}
|
||||
|
||||
protected override void ConfigureUnity() {
|
||||
@ -30,6 +35,7 @@ namespace VideoGameQuotes.Web {
|
||||
.RegisterType<IsValidUserAttribute>(new InjectionProperty<IsValidUserAttribute>(attr => attr.UserProvider))
|
||||
.RegisterType<IUserService, UserService>()
|
||||
.RegisterType<IQuoteService, QuoteService>()
|
||||
.RegisterType<IApiService, ApiService>()
|
||||
.RegisterType<IUserRepository, UserRepository>();
|
||||
}
|
||||
|
||||
@ -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" });
|
||||
|
||||
|
102
Src/VideoGameQuotes.Web/Models/ApiModel.cs
Normal file
102
Src/VideoGameQuotes.Web/Models/ApiModel.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
Src/VideoGameQuotes.Web/Models/BrowseModel.cs
Normal file
32
Src/VideoGameQuotes.Web/Models/BrowseModel.cs
Normal 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
|
||||
}
|
||||
}
|
80
Src/VideoGameQuotes.Web/Models/BrowseModelBinder.cs
Normal file
80
Src/VideoGameQuotes.Web/Models/BrowseModelBinder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
Src/VideoGameQuotes.Web/Models/BrowseViewModel.cs
Normal file
19
Src/VideoGameQuotes.Web/Models/BrowseViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -84,7 +84,12 @@
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Controllers\ApiController.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\QuoteCollectionModel.cs" />
|
||||
<Compile Include="Models\QuoteModel.cs" />
|
||||
@ -123,6 +128,8 @@
|
||||
<Content Include="Views\Home\ContactSuccess.aspx" />
|
||||
<Content Include="Views\Quote\BadPaging.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\Quote.aspx" />
|
||||
<Content Include="Views\Quote\QuoteNotFound.aspx" />
|
||||
|
99
Src/VideoGameQuotes.Web/Views/Quote/DefaultBrowse.aspx
Normal file
99
Src/VideoGameQuotes.Web/Views/Quote/DefaultBrowse.aspx
Normal 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>
|
8
Src/VideoGameQuotes.Web/Views/Quote/QualifiedBrowse.aspx
Normal file
8
Src/VideoGameQuotes.Web/Views/Quote/QualifiedBrowse.aspx
Normal 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>
|
@ -24,7 +24,7 @@
|
||||
<ul class="clearfix menu">
|
||||
<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.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("Submit", "submit", "Quote", null, new { title = "Submit a new quote" }) %></li>
|
||||
<li><%= Html.ActionLink("About", "about", "Home", null, new { title = "About the site" })%></li>
|
||||
|
@ -115,6 +115,14 @@ ul.menu li {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
#browse-default-menu li {
|
||||
margin-bottom: 2px;
|
||||
padding: 3px;
|
||||
}
|
||||
#browse-default-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#header {
|
||||
background-color: #669966;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user