##App This application is a clone of Stack Overflow (which I created in under two weeks). However, it is designed differently, and will be a one page application. Below is basic overview of the features of this site:
- Answers (which may or may not be accepted)
- Up and Down Voting (On Questions and Answers By Users)
- Edit suggestions (By Users with enough points)
Below is a list of advanced features I may add to this site:
- FriendlyId in Backbone
- User Authentication in Backbone
- Question view counts
- Facebook, Twitter, and other logins (i.e. OmniAuth)
- The ability to open and close questions
- Moderator privileges
##Construction I started this application as a Rails app (getting all the models, controllers, views, and routes set up, along with the API layer) and then started turning it into a one page application with Backbone. In order to have some sample data on this website, I used Nokogiri to scrape Stack Overflow and seeded my database with that data.
While I could use Devise or another authentication gem for me, this would complicate the structure of my project later on. My goal of turning this into a single page application using Backbone (including the sign up and sign in pages) lead me to the conclusion that I need more control over the
Session resources than using a pre-built authentication system would give me.
users table has the following schema:
- email - For Gravatars
- slug - For FriendlyId in Rails
I briefly considered splitting the points part of the schema out into another table (such as a
profiles table that belongs to a
user). However, it seemed simpler (for the moment) to just add one column to the
In addition to the authentication methods necessary for each user, my model has several methods relating to points. See the "Points" section below for more details.
Points are the gatekeepers for the ability to do anything other than ask a question, or answer someone else's question. Methods for dealing with points in the
User model include a basic method for adding points and several methods for checking if user has enough points to do a particular action:
add_points- This takes in a number of points as a parameter.
can_vote_up?(min 15 points)
can_comment?(min 50 points)
can_vote_down?(min 125 points)
Users can get points by their questions and answers being voted up and their
edit_suggestions being accepted. Simply using the
add_points method in the
accept methods for
edit_suggestions was enough to get those actions to add points to a user. However, my method for dealing with votes was more complicated. This led to me adding a class method to the
Vote model called
calculate_user_points that figures out how many points to add to the object's user based on whether the object is a
Question or an
If I get around to moderator and more advanced features, more methods will need to be added to correspond to those extra features.
questions resource is fairly simple compared to the user's resource (at least for now). The database only stores an id, a title, and a body. That said, from this point on, everything relates back to a question and a user, either directy (to the question itself) or inderection through an answer. Moreover, questions end up having a number of methods to deal with Voting and Edit Suggestions. That are mentioned in those respective section below.
answers resource is even simpler than the
questions resource. It has the following schema:
Answer model also has a method called
accept that sets the
accepted attribute to
true and saves the object. Since each question should only have one accepted answer, this necessitated the private validation method
only_one_accepted_answer which (if the current object has
accepted set to
true) performs a SQL query (using
find_by_sql) to determine if there is another accepted answer for the current object's question and adds an error if there is another accepted answer for the same question. Lastly, question also has a method
accepted_answer which finds and returns its accepted answer.
Answers, like questions, can be voted on, commented on, and even edited by other users. Every added method that a question needs to deal with the following resources, an answer needs as well.
comments, while storing a polymorhpic association, is very simple. Each row in the
comments table has the following schema:
Simply adding the polymorphic association was enough to take care of the model level of this resource.
controllers for this resource was more complicated. I ended up making a route for new comments and creating new comments under both the question route and the comments route. This led to my splitting up the controller action for creating a new comment into one of two private methods which either built the comment as a
answer comment using either
params[:answer_id]. This was not particularly difficult and it was fairly easy to adjust the new view accordingly as well.
This is where I started having to make more interesting decisions. Instead of splitting this out into two or four tables, I created one table that had a polymorphic association with
answers as well as a column for the
direction of each vote. This caused the schema to be as follows:
The bigger issue was figuring out how to make sure that a user could unvote if they clicked on the same arrow (voting is through arrows in the view) twice, in addition to being able to reverse their vote by clicking on the opposite arrow. Moreover, I wanted to make sure that the model did all the work. After trying a different method, I created a
parse_vote_request class method for Votes that the controller calls when a user clicks on an arrow. This method takes in a
voteable_id and a
user_id. It then tries to find any previous vote on the same object by the same user and adds, removes, and updates vote accordingly.
Voting also required two different instance methods on
answer objects. Those were:
vote_direction_by_user- This takes in a user_id and sees if and how the user voted on that object.
##Edit Suggestions Broadly, edit suggestions are simply edits on questions or answer bodies which are not saved directly because they are created by a different user than the one who wrote the respective question or answer. This means that, as a resource, the user only perceives that their edit is not directly saved because the edit button is different. I decided that a user should be able to see what they have suggested edits for, and what other users have suggested they change in their own questions and answers via their profile page. I also added a show page for suggested edits, which shows the original body and the suggested revision side by side. If it is your question or answer that is being edited, then you can see and click on buttons to accept the edit or reject the edit.
edit_suggestions schema is fairly simple. It contains the following:
The only thing users can access directly is the body. Everything else is set for them using the available information.
The necessary methods for
edit_suggestions were fairly simple. For the moment I have the following methods:
questions- This figures out which question to which this edit can be traced back.
accept_edit- This changes the associated
editable's body to its own body, saves its revised
editableand changes the
trueand saves everything.
This led to a few associated methods for users. Assuming that I want to show a user all the suggested edits they have for their questions and answers (which I do), I realized that no direct association would give me this list. Moreover, I want users to be able to see whether their edits are pending or accepted on their profile. This led to the following methods:
suggested_edits- This adds together both the
pending_edit_suggestions- This finds the
edit_suggestionsthat have not been accepted
accept_edit_suggestions- This finds the
edit_suggestionsthat have been accepted
Additionally, I was able to use this to add a
contributors instance method to the
Question models which performas a SQL query (using
find_by_sql) to find all the users that have accepted edit suggestions for the object in question.
##Backbone Integration I made the fairly unusual choice to start this application as a Rails multi-page app and, only after I got that working, turn it into a single page application. I had many reasons for doing this.
First, I chose this project because I am interested in the architecture of websites (in case it isn't noticeable that I only talk about data structures up to this point). This meant that I had to make some fairly difficult decisions about how I wanted to implement voting, and the variety of other features present in this app. I did not want to have to make decisions about how to render things in backbone while I was making the decisions about how to get the server to respond to voting correctly, or edits (the response varies for both of these depending on a variety of factors). Building out the application in Rails, without dealing with Backbone initially, allowed me to focus on my model, database, and router concerns in isolation while creating the necessary views somewhat simply.
Second, I wanted to wait until I had all the necessary features and views implemented before building out my API layer. Going in, I was unawared what each view would need in order for me to render it in Backbone, and I was still working on adding new features. By the time I built out my API layer I knew what methods each view called and what I needed to include when I rendered users, questions, answers, and everything else as json.
Lastly, I wanted to get experience adding Backbone to an existing Rails application. In the process of the transition, I had one view that had partials within partials in Rails, and I had to figure out how to render the Rails view with nested partials in Backbone, which does not have partials. I ended up solving this problem by having views within views within views, each of which had an associated template. Another issue I came across was my reliance on
active_support/inflector's pluralize in Rails, when Backbone did not come with any sort of inflector. I solved this issue by downloading a library called
This application was created in under two weeks and, during that time, I was able to get both the Rails part of this application and the Backbone.js part of this application working. That said, this code for this site could be DRY-er (a lot of code is shared between questions and answers in both Rails and Backbone.js) and could use tests. Due to time constraints, I prioritized getting the application working in Rails and in Backbone.js over both of these concerns (since the necessary changes would require creating
ActiveSupport::Concern's for various features and Backbone.js view superclasses for already working code). The next step in working on this application will be adding tests and then refactoring my code.
Data: I was not particularly interested in creating a lot of fake data to populate my database. I used the
faker gem to generate user data. I then used the
nokogiri gem to scrape approximately 75 questions, with their comments and answers, from Stack Overflow to populate my database with an initial data-set.
FriendlyId: This website uses the
Kaminari: This website uses
kaminari for pagination.