implemented voting for quotes

This commit is contained in:
tmont 2011-02-15 00:14:24 +00:00
parent 7d51b80dcf
commit cdd93afeff
10 changed files with 137 additions and 7 deletions

View File

@ -22,6 +22,30 @@ namespace VideoGameQuotes.Web.Controllers {
this.currentUserProvider = currentUserProvider; this.currentUserProvider = currentUserProvider;
} }
[HttpPost, IsValidUser]
public JsonResult Vote(VoteModel model) {
if (!ModelState.IsValid) {
return Json(this.CreateJsonErrorResponse("Invalid request"));
}
try {
var vote = quoteService.GetVoteOrCreateNew(quoteService.GetQuote(model.QuoteId), currentUserProvider.CurrentUser);
vote.Direction = model.Direction;
vote = quoteService.SaveVote(vote);
var data = new Dictionary<string, string> {
{ "netVotes", vote.Quote.NetVotes.ToString() },
{ "upVotes", vote.Quote.UpVotes.ToString() },
{ "downVotes", vote.Quote.DownVotes.ToString() }
};
return Json(this.CreateJsonResponse(null, data));
} catch {
return Json(this.CreateJsonErrorResponse("An error occurred while trying to process your vote"));
}
}
public ActionResult Recent() { public ActionResult Recent() {
//get last 10 submitted quotes //get last 10 submitted quotes
var quotes = quoteService.GetMostRecentQuotes(10); var quotes = quoteService.GetMostRecentQuotes(10);

View File

@ -39,7 +39,7 @@ namespace VideoGameQuotes.Web {
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" }); routes.MapRoute("quote", "{action}", new { controller = "Quote" }, new { action = "submit|search|recent|random|best|browse|vote" });
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

@ -4,7 +4,6 @@ using JetBrains.Annotations;
using VideoGameQuotes.Api; using VideoGameQuotes.Api;
namespace VideoGameQuotes.Web.Models { namespace VideoGameQuotes.Web.Models {
public class QuoteCollectionModel { public class QuoteCollectionModel {
public IEnumerable<Quote> Quotes { get; set; } public IEnumerable<Quote> Quotes { get; set; }
public User User { get; set; } public User User { get; set; }

View File

@ -0,0 +1,11 @@
using Portoa.Validation.DataAnnotations;
using VideoGameQuotes.Api;
namespace VideoGameQuotes.Web.Models {
public class VoteModel {
[GreaterThanZero]
public int QuoteId { get; set; }
public VoteDirection Direction { get; set; }
}
}

View File

@ -22,6 +22,9 @@ namespace VideoGameQuotes.Web.Services {
[CanBeNull] [CanBeNull]
Quote GetRandomQuote(); Quote GetRandomQuote();
IEnumerable<Quote> GetBestQuotes(int start, int end); IEnumerable<Quote> GetBestQuotes(int start, int end);
Vote SaveVote(Vote vote);
[NotNull]
Vote GetVoteOrCreateNew(Quote quote, User voter);
} }
public class QuoteService : IQuoteService { public class QuoteService : IQuoteService {
@ -30,15 +33,19 @@ namespace VideoGameQuotes.Web.Services {
private readonly IRepository<GamingSystem> systemRepository; private readonly IRepository<GamingSystem> systemRepository;
private readonly IRepository<Publisher> publisherRepository; private readonly IRepository<Publisher> publisherRepository;
private readonly IRepository<Category> categoryRepository; private readonly IRepository<Category> categoryRepository;
private readonly IRepository<Vote> voteRepository;
public QuoteService( public QuoteService(
IRepository<Quote> quoteRepository, IRepository<Quote> quoteRepository,
IRepository<Game> gameRepository, IRepository<Game> gameRepository,
IRepository<GamingSystem> systemRepository, IRepository<GamingSystem> systemRepository,
IRepository<Publisher> publisherRepository, IRepository<Category> categoryRepository IRepository<Publisher> publisherRepository,
IRepository<Category> categoryRepository,
IRepository<Vote> voteRepository
) { ) {
this.quoteRepository = quoteRepository; this.quoteRepository = quoteRepository;
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
this.voteRepository = voteRepository;
this.gameRepository = gameRepository; this.gameRepository = gameRepository;
this.systemRepository = systemRepository; this.systemRepository = systemRepository;
this.publisherRepository = publisherRepository; this.publisherRepository = publisherRepository;
@ -122,9 +129,24 @@ namespace VideoGameQuotes.Web.Services {
var records = quoteRepository.Records.ToArray(); var records = quoteRepository.Records.ToArray();
return records return records
.OrderByDescending(quote => quote.UpVotes) .OrderByDescending(quote => quote.NetVotes)
.ThenByDescending(quote => quote.UpVotes)
.Skip(start) .Skip(start)
.Take(end - start + 1); .Take(end - start + 1);
} }
[UnitOfWork]
public Vote SaveVote(Vote vote) {
return voteRepository.Save(vote);
}
[UnitOfWork]
public Vote GetVoteOrCreateNew(Quote quote, User voter) {
var vote = voteRepository.Records.SingleOrDefault(v => v.Quote == quote && v.Voter == voter);
return vote ?? new Vote {
Quote = quote,
Voter = voter
};
}
} }
} }

View File

@ -86,6 +86,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="Controllers\HomeController.cs" /> <Compile Include="Controllers\HomeController.cs" />
<Compile Include="Models\QuoteModel.cs" /> <Compile Include="Models\QuoteModel.cs" />
<Compile Include="Models\VoteModel.cs" />
<Compile Include="Validation\NonEmptyText.cs" /> <Compile Include="Validation\NonEmptyText.cs" />
<Compile Include="Security\IsValidUserAttribute.cs" /> <Compile Include="Security\IsValidUserAttribute.cs" />
<Compile Include="Controllers\QuoteController.cs" /> <Compile Include="Controllers\QuoteController.cs" />
@ -113,9 +114,11 @@
<Content Include="media\css\reset.css" /> <Content Include="media\css\reset.css" />
<Content Include="media\images\favicon.png" /> <Content Include="media\images\favicon.png" />
<Content Include="media\images\search.png" /> <Content Include="media\images\search.png" />
<Content Include="media\js\vgquotes.js" />
<Content Include="Views\Home\About.aspx" /> <Content Include="Views\Home\About.aspx" />
<Content Include="Views\Home\Contact.aspx" /> <Content Include="Views\Home\Contact.aspx" />
<Content Include="Views\Home\ContactSuccess.aspx" /> <Content Include="Views\Home\ContactSuccess.aspx" />
<Content Include="Views\Quote\BadPaging.aspx" />
<Content Include="Views\Quote\Best.aspx" /> <Content Include="Views\Quote\Best.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" />
@ -142,7 +145,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="App_Data\" /> <Folder Include="App_Data\" />
<Folder Include="media\js\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\VideoGameQuotes.Api\VideoGameQuotes.Api.csproj"> <ProjectReference Include="..\VideoGameQuotes.Api\VideoGameQuotes.Api.csproj">

View File

@ -0,0 +1,10 @@
<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage" MasterPageFile="~/Views/Shared/Site.Master" %>
<asp:Content runat="server" ID="Title" ContentPlaceHolderID="TitleContent">Invalid Paging Request</asp:Content>
<asp:Content runat="server" ID="Main" ContentPlaceHolderID="MainContent">
<h2>Invalid Paging Request</h2>
<p>
Since you&rsquo;re here, you were probably mucking around with the query string.
Please stop.
</p>
</asp:Content>

View File

@ -2,6 +2,7 @@
<%@ Import Namespace="VideoGameQuotes.Api" %> <%@ Import Namespace="VideoGameQuotes.Api" %>
<div class="quote-container"> <div class="quote-container">
<input type="hidden" class="quote-id" value="<%= Model.Quote.Id %>" />
<div class="quote-data clearfix"> <div class="quote-data clearfix">
<div class="quote-score-container"> <div class="quote-score-container">
<div class="vote-container"> <div class="vote-container">
@ -29,7 +30,7 @@
<div class="quote-details"> <div class="quote-details">
<dl> <dl>
<dt>Game</dt> <dt>Game</dt>
<dd><%= Html.ActionLink(Model.Quote.Game.Name, "Index", "Quote", new { game = Model.Quote.Game.Id }, null) %></dd> <dd><%= Html.ActionLink(Model.Quote.Game.Name, "browse", "Quote", new { game = Model.Quote.Game.Id }, null) %></dd>
<dt>Added</dt> <dt>Added</dt>
<dd><%: Model.Quote.GetHumanReadableTimeSinceCreated() %></dd> <dd><%: Model.Quote.GetHumanReadableTimeSinceCreated() %></dd>
</dl> </dl>

View File

@ -50,12 +50,13 @@
<div id="footer"> <div id="footer">
<div class="content-container"> <div class="content-container">
&copy; <%= DateTime.UtcNow.Year %> <a href="http://tommymontgomery.com/" title="Who is this man?">Tommy Montgomery</a><br /> &copy; <%= DateTime.UtcNow.Year %> <a href="http://tommymontgomery.com/" title="Who is this man?">Tommy Montgomery</a><br />
If you steal something, I&#39;ll murder your family. If you steal something, I&rsquo;ll murder your family.
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.min.js"></script> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.min.js"></script>
<script type="text/javascript" src="/media/js/vgquotes.js"></script>
<asp:ContentPlaceHolder ID="DeferrableScripts" runat="server" /> <asp:ContentPlaceHolder ID="DeferrableScripts" runat="server" />
</body> </body>
</html> </html>

View File

@ -0,0 +1,60 @@
(function($, window, undefined){
$(document).ready(function() {
var voting = false;
$(".vote-for, .vote-against").live("click", function() {
if (voting) {
alert("Please wait for the current vote to process before voting again");
return false;
}
voting = true;
var $votingLink = $(this);
var $container = $votingLink.parents(".quote-container");
var direction = $votingLink.hasClass("vote-for") ? 1 : 0;
var quoteId = $container.find("input.quote-id").val();
$.ajax("/vote", {
type: "POST",
data: {
QuoteId: quoteId,
Direction: direction
},
success: function(data, status, $xhr) {
if (data.Error !== null) {
alert(data.Error);
return;
}
$container
.find(".quote-score")
.attr("title", "+" + data.Data.upVotes + ", -" + data.Data.downVotes)
.text(data.Data.netVotes);
//remove the voting arrow, and add the other one if needed
$votingLink.remove();
if (direction === 1) {
if ($container.find(".vote-against").length === 0) {
$("<span/>")
.addClass("vote-against")
.attr("title", "I hate this quote")
.text(String.fromCharCode(0x25BC))
.appendTo($container.find(".vote-container:last"));
}
} else {
if ($container.find(".vote-for").length === 0) {
$("<span/>")
.addClass("vote-for")
.attr("title", "I like this quote")
.text(String.fromCharCode(0x25B2))
.appendTo($container.find(".vote-container:first"));
}
}
},
complete: function() { voting = false; }
});
return false;
});
});
}(jQuery, window));