Permalink
Browse files

Initial commit

  • Loading branch information...
banker committed Nov 9, 2009
0 parents commit 66c536f0e22022ddea3fb4f99909e2d34c6ef596
Showing 2,096 changed files with 249,638 additions and 0 deletions.
3 .gems
@@ -0,0 +1,3 @@
+bcrypt-ruby '>= 2.1.2'
+mongo
+mongo_mapper
@@ -0,0 +1,10 @@
+*.swp
+*.sw0
+.DS_Store
+**/.DS_Store
+log/*
+tmp/*
+tmp/**/*
+config/database.yml
+coverage/*
+coverage/**/*
4 README
@@ -0,0 +1,4 @@
+== Newsmonger
+=== A simple social news app demonstrating MongoDB with Rails and MongoMapper
+
+
@@ -0,0 +1,10 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
@@ -0,0 +1,23 @@
+# Filters added to this controller apply to all controllers in the application.
+# Likewise, all the methods added will be available for all controllers.
+
+class ApplicationController < ActionController::Base
+ include Authentication
+ helper :all # include all helpers, all the time
+ protect_from_forgery
+
+ before_filter :store_location
+
+ filter_parameter_logging :password
+
+ protected
+
+ def login_required
+ if !current_user
+ flash[:notice] = "Please log in."
+ redirect_to signin_url
+ else
+ return true
+ end
+ end
+end
@@ -0,0 +1,21 @@
+class CommentsController < ApplicationController
+ before_filter :login_required, :except => :index
+ skip_before_filter :verify_authenticity_token
+
+ def index
+ @comments = Comment.find
+ end
+
+ def create
+ @story = Story.find(params[:comment][:story_id])
+ @comment = Comment.new(params[:comment].merge(:user => current_user))
+ @comment.save
+ redirect_to story_url(@story)
+ end
+
+ def upvote
+ @story = Story.find(params[:id])
+ @story.upvote(current_user)
+ render :nothing => true
+ end
+end
@@ -0,0 +1,28 @@
+class SessionsController < ApplicationController
+ skip_before_filter :store_location
+
+ layout 'sessions'
+
+ def new
+ sign_out_keeping_session!
+ @user = User.new
+ render 'new'
+ end
+
+ def create
+ @user = User.new
+ sign_out_keeping_session!
+ if @user = User.authenticate(params[:email], params[:password])
+ session[:user_id] = @user.id
+ redirect_back_or_default root_url
+ else
+ redirect_to :action => :new
+ end
+ end
+
+ def destroy
+ sign_out_killing_session!
+ redirect_to new_session_url
+ end
+end
+
@@ -0,0 +1,34 @@
+class StoriesController < ApplicationController
+
+ before_filter :login_required, :except => [:index, :show]
+
+ def new
+ @story = Story.new
+ end
+
+ def index
+ @page = (params[:page] || 1).to_i
+ @stories = Story.paginate(:page => @page, :per_page => 2)
+ end
+
+ def show
+ @story = Story.find_by_slug(params[:id])
+ @comments = Comment.threaded_with_field(@story)
+ end
+
+ def create
+ @story = Story.new(params[:story])
+ @story.user = current_user
+ if @story.save
+ redirect_to root_url
+ else
+ render :action => :new
+ end
+ end
+
+ def upvote
+ @story = Story.find(params[:id])
+ @story.upvote(current_user)
+ render :nothing => true
+ end
+end
@@ -0,0 +1,18 @@
+class UsersController < ApplicationController
+ layout 'sessions'
+
+ def new
+ @user = User.new
+ render '/sessions/new'
+ end
+
+ def create
+ @user = User.new(params[:user])
+ if @user.save
+ self.current_user = @user
+ redirect_to root_path
+ else
+ render '/sessions/new', :layout => 'sessions'
+ end
+ end
+end
@@ -0,0 +1,3 @@
+# Methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+end
@@ -0,0 +1,2 @@
+module CommentsHelper
+end
@@ -0,0 +1,2 @@
+module StoriesHelper
+end
@@ -0,0 +1,98 @@
+class Comment
+ include MongoMapper::Document
+
+ key :body, String
+ key :voters, Array
+ key :votes, Integer, :default => 0
+ key :depth, Integer, :default => 0
+ key :path, String, :default => ""
+
+ key :parent_id, String
+ key :story_id, String
+ key :user_id, String
+ key :username, String
+ timestamps!
+
+ # Relationships.
+ belongs_to :user
+ belongs_to :story
+
+ # Callbacks.
+ after_create :auto_upvote, :set_path, :increment_story_comment_count
+
+ # Return an array of comments, threaded, from highest to lowest votes.
+ # Sorts by votes descending by default, but could use any other field.
+ # If you want to build out an internal balanced score, pass that field in,
+ # and be sure to index it on the database.
+ def self.threaded_with_field(story, field_name='votes')
+ comments = find(:all, :conditions => {:story_id => story.id}, :order => "path asc, #{field_name} desc")
+ results, map = [], {}
+ comments.each do |comment|
+ if comment.parent_id.blank?
+ results << comment
+ else
+ comment.path =~ /:([\d|\w]+)$/
+ if parent = $1
+ map[parent] ||= []
+ map[parent] << comment
+ end
+ end
+ end
+ assemble(results, map)
+ end
+
+ # Used by Comment#threaded_with_field to assemble the results.
+ def self.assemble(results, map)
+ list = []
+ results.each do |result|
+ if map[result.id]
+ list << result
+ list += assemble(map[result.id], map)
+ else
+ list << result
+ end
+ end
+ list
+ end
+
+ # Upvote this comment.
+ def upvote(user)
+ unless self.voters.include?(user.id)
+ self.voters << user.id
+ self.votes += 1
+ self.save
+ end
+ end
+
+ # Is this a root node?
+ def root?
+ self.depth.zero?
+ end
+
+ def user=(user)
+ self.username = user.username
+ self.user_id = user.id
+ end
+
+ private
+
+ # Comment owner automatically upvoters.
+ def auto_upvote
+ upvote(self.user)
+ end
+
+ def increment_story_comment_count
+ Story.collection.update({"_id" => self.story_id}, {"$inc" => {"comment_count" => 1}})
+ end
+
+ # Store the comment's path.
+ def set_path
+ unless self.parent_id.blank?
+ parent = Comment.find(self.parent_id)
+ self.story_id = parent.story_id
+ self.depth = parent.depth + 1
+ self.path = parent.path + ":" + parent.id
+ end
+ save
+ end
+end
@@ -0,0 +1,92 @@
+class Story
+ include MongoMapper::Document
+
+ key :title, String
+ key :url, String
+ key :slug, String
+ key :voters, Array
+ key :votes, Integer, :default => 0
+ key :relevance, Integer, :default => 0
+
+ # Cached values.
+ key :comment_count, Integer, :default => 0
+ key :username, String
+
+ # Note this: ids are strings, not integers.
+ key :user_id, String
+ timestamps!
+
+ # Relationships.
+ belongs_to :user
+ many :comments
+
+ # Validations.
+ URL_REGEX = /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix
+ validates_presence_of :title, :url, :user_id
+ validates_format_of :url, :with => URL_REGEX
+
+ # Callbacks.
+ after_validation_on_create :store_user_data, :create_slug
+
+ def self.find_by_slug(slug)
+ first(:conditions => {:slug => slug})
+ end
+
+ def to_param
+ self.slug || self.id
+ end
+
+ def self.upvote(story_id, user_id)
+ collection.update({'_id' => story_id, 'voters' => {'$ne' => user_id}},
+ {'$inc' => {'votes' => 1}, '$push' => {'voters' => user_id}})
+ end
+
+ # Upvote this story.
+ def upvote(user)
+ unless self.voters.include?(user.id)
+ self.voters << user.id
+ self.votes += 1
+ self.relevance = calculate_relevance unless new_record?
+ self.save
+ end
+ end
+
+ private
+
+ # Stories are displayed in order of relevance.
+ # Relevance will eventually reach zero, at which point,
+ # stories are displayed in order of date posted and votes.
+ def calculate_relevance
+ return self.votes if self.created_at > 8.hours.ago.utc
+ end
+
+ # Cache username and upvote.
+ def store_user_data
+ upvote(self.user)
+ self.username = self.user.username
+ end
+
+ # Create a slug from the title.
+ # From Sluggable Finder: http://github.com/ismasan/sluggable-finder/
+ def convert_to_slug(str)
+ if defined?(ActiveSupport::Inflector.parameterize)
+ ActiveSupport::Inflector.parameterize(str).to_s
+ else
+ ActiveSupport::Multibyte::Handlers::UTF8Handler.
+ normalize(str,:d).split(//u).reject { |e| e.length > 1 }.join.strip.gsub(/[^a-z0-9]+/i, '-').downcase.gsub(/-+$/, '')
+ end
+ end
+
+ # Note: this slug creation code is vulnerable to race conditions.
+ # Refactoring forthcoming.
+ def create_slug
+ return if self.title.blank?
+ tail, int = "", 1
+ initial = convert_to_slug(self.title)
+ while Story.find_by_slug(initial + tail) do
+ int += 1
+ tail = "-#{int}"
+ end
+ self.slug = initial + tail
+ end
+end
Oops, something went wrong.

0 comments on commit 66c536f

Please sign in to comment.