diff --git a/app/controllers/asciicasts_controller.rb b/app/controllers/asciicasts_controller.rb index ef88cae..ec1bd37 100644 --- a/app/controllers/asciicasts_controller.rb +++ b/app/controllers/asciicasts_controller.rb @@ -1,5 +1,4 @@ class AsciicastsController < ApplicationController - PER_PAGE = 15 before_filter :load_resource, :only => [:show, :raw, :edit, :update, :destroy] before_filter :ensure_authenticated!, :only => [:edit, :update, :destroy] @@ -8,23 +7,11 @@ class AsciicastsController < ApplicationController respond_to :html, :json, :js def index - @asciicasts = PaginatingDecorator.new( - Asciicast.newest_paginated(params[:page], PER_PAGE) - ) + asciicasts = AsciicastList.new(params[:category], params[:order]) - @category_name = "All asciicasts" - @current_category = :all - end - - def popular - @asciicasts = PaginatingDecorator.new( - Asciicast.popular_paginated(params[:page], PER_PAGE) - ) - - @category_name = "Popular asciicasts" - @current_category = :popular - - render :index + render locals: { + asciicast_list: AsciicastListDecorator.new(asciicasts, params[:page]) + } end def show diff --git a/app/decorators/asciicast_list_decorator.rb b/app/decorators/asciicast_list_decorator.rb new file mode 100644 index 0000000..4533fe0 --- /dev/null +++ b/app/decorators/asciicast_list_decorator.rb @@ -0,0 +1,23 @@ +class AsciicastListDecorator < ApplicationDecorator + + PER_PAGE = 12 + + attr_reader :page, :per_page + + delegate_all + + def initialize(model, page, per_page = nil) + super(model) + @page = page + @per_page = per_page || PER_PAGE + end + + def category_name + "#{category.to_s.capitalize} asciicasts" + end + + def items + PaginatingDecorator.new(model.items.paginate(page, per_page)) + end + +end diff --git a/app/models/asciicast.rb b/app/models/asciicast.rb index 052d72d..19d5ddc 100644 --- a/app/models/asciicast.rb +++ b/app/models/asciicast.rb @@ -1,5 +1,6 @@ class Asciicast < ActiveRecord::Base - MAX_DELAY = 5.0 + + ORDER_MODES = { recency: 'created_at', popularity: 'views_count' } mount_uploader :stdin_data, StdinDataUploader mount_uploader :stdin_timing, StdinTimingUploader @@ -30,6 +31,20 @@ class Asciicast < ActiveRecord::Base Digest::MD5.hexdigest timestamps.join('/') end + def self.paginate(page, per_page) + page(page).per(per_page) + end + + def self.for_category_ordered(category, order) + collection = all + + if category == :featured + collection = collection.featured + end + + collection.order("#{ORDER_MODES[order]} DESC") + end + def stdout @stdout ||= BufferedStdout.new(stdout_data.decompressed_path, stdout_timing.decompressed_path).lazy diff --git a/app/models/asciicast_list.rb b/app/models/asciicast_list.rb new file mode 100644 index 0000000..b9e0571 --- /dev/null +++ b/app/models/asciicast_list.rb @@ -0,0 +1,15 @@ +class AsciicastList + + attr_reader :category, :order, :repository + + def initialize(category, order, repository = Asciicast) + @category = (category || :all).to_sym + @order = (order || :recency).to_sym + @repository = repository + end + + def items + repository.for_category_ordered(category, order) + end + +end diff --git a/app/views/asciicasts/index.html.slim b/app/views/asciicasts/index.html.slim index 1b7eb68..2f20bdc 100644 --- a/app/views/asciicasts/index.html.slim +++ b/app/views/asciicasts/index.html.slim @@ -1,9 +1,30 @@ -section.supplimental - .wrapper - .main - h1 = @category_name - = render :partial => 'asciicasts/previews', :locals => { :asciicasts => @asciicasts } - = paginate @asciicasts +.asciicasts-list-page + .container + .row + .col-md-3 + = render 'shared/browse_categories', current_category: asciicast_list.category - .extras - = render :partial => 'shared/browse_categories' + .col-md-9 + .row + .col-md-6 + h2 = asciicast_list.category_name + + .col-md-6.text-right + .sorting + span Sort by + .btn-group.text-left + button.btn.btn-default.dropdown-toggle[type="button" data-toggle="dropdown"] + | #{asciicast_list.order} + span.caret + ul.dropdown-menu[role="menu"] + li + a href="?order=recency" + | recency + li + a href="?order=popularity" + | popularity + + .row.asciicast-list + .col-md-12 + = render 'asciicasts/previews', asciicasts: asciicast_list.items, per_row: 2 + = paginate asciicast_list.items diff --git a/app/views/kaminari/_first_page.html.erb b/app/views/kaminari/_first_page.html.erb new file mode 100644 index 0000000..bf23ff0 --- /dev/null +++ b/app/views/kaminari/_first_page.html.erb @@ -0,0 +1,3 @@ +
  • + <%= link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote %> +
  • diff --git a/app/views/kaminari/_gap.html.erb b/app/views/kaminari/_gap.html.erb new file mode 100644 index 0000000..416ebcd --- /dev/null +++ b/app/views/kaminari/_gap.html.erb @@ -0,0 +1,3 @@ +
  • + <%= link_to raw(t 'views.pagination.truncate'), '#' %> +
  • diff --git a/app/views/kaminari/_last_page.html.erb b/app/views/kaminari/_last_page.html.erb new file mode 100644 index 0000000..fb619ea --- /dev/null +++ b/app/views/kaminari/_last_page.html.erb @@ -0,0 +1,3 @@ +
  • + <%= link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} %> +
  • diff --git a/app/views/kaminari/_next_page.html.erb b/app/views/kaminari/_next_page.html.erb new file mode 100644 index 0000000..15e10e4 --- /dev/null +++ b/app/views/kaminari/_next_page.html.erb @@ -0,0 +1,3 @@ +
  • + <%= link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote %> +
  • diff --git a/app/views/kaminari/_page.html.erb b/app/views/kaminari/_page.html.erb new file mode 100644 index 0000000..205a81f --- /dev/null +++ b/app/views/kaminari/_page.html.erb @@ -0,0 +1,3 @@ +
  • + <%= link_to page, page.current? ? '#' : url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} %> +
  • diff --git a/app/views/kaminari/_paginator.html.erb b/app/views/kaminari/_paginator.html.erb new file mode 100644 index 0000000..2c8757b --- /dev/null +++ b/app/views/kaminari/_paginator.html.erb @@ -0,0 +1,15 @@ +<%= paginator.render do -%> + +<% end -%> diff --git a/app/views/kaminari/_prev_page.html.erb b/app/views/kaminari/_prev_page.html.erb new file mode 100644 index 0000000..d94a50a --- /dev/null +++ b/app/views/kaminari/_prev_page.html.erb @@ -0,0 +1,3 @@ +
  • + <%= link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote %> +
  • diff --git a/app/views/shared/_browse_categories.html.erb b/app/views/shared/_browse_categories.html.erb deleted file mode 100644 index 2fae34b..0000000 --- a/app/views/shared/_browse_categories.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -

    Browse

    - - diff --git a/app/views/shared/_browse_categories.html.slim b/app/views/shared/_browse_categories.html.slim new file mode 100644 index 0000000..81fe491 --- /dev/null +++ b/app/views/shared/_browse_categories.html.slim @@ -0,0 +1,5 @@ +h2 Browse + += category_links current_category do |categories| + = categories.link_to 'All', browse_path, :all + = categories.link_to 'Featured', category_path(:featured), :featured diff --git a/config/routes.rb b/config/routes.rb index 115b5ec..2dc0bbe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ Asciinema::Application.routes.draw do get "/browse" => "asciicasts#index", :as => :browse - get "/browse/popular" => "asciicasts#popular", :as => :popular + get "/browse/:category" => "asciicasts#index", :as => :category resources :asciicasts, :path => 'a' do member do diff --git a/spec/controllers/asciicasts_controller_spec.rb b/spec/controllers/asciicasts_controller_spec.rb index 281b761..7119f07 100644 --- a/spec/controllers/asciicasts_controller_spec.rb +++ b/spec/controllers/asciicasts_controller_spec.rb @@ -11,49 +11,33 @@ shared_examples_for 'non-owner user trying to modify' do end describe AsciicastsController do + let(:user) { stub_model(User, :nickname => 'nick') } let(:asciicast) { stub_model(Asciicast, :id => 666) } subject { response } describe '#index' do - let(:asciicasts) { [double('asciicast')] } - let(:page) { double('page').to_s } + let(:asciicast_list) { double('asciicast_list') } + let(:decorated_asciicast_list) { double('decorated_asciicast_list') } before do - Asciicast.should_receive(:newest_paginated). - with(page, an_instance_of(Fixnum)).and_return(asciicasts) + allow(controller).to receive(:render) - PaginatingDecorator.should_receive(:new).with(asciicasts). - and_return(asciicasts) + allow(AsciicastList).to receive(:new). + with('featured', 'recency') { asciicast_list } + allow(AsciicastListDecorator).to receive(:new). + with(asciicast_list, '2') { decorated_asciicast_list } - get :index, :page => page + get :index, category: 'featured', order: 'recency', page: '2' end it { should be_success } - specify { assigns(:asciicasts).should == asciicasts } - specify { assigns(:category_name).should =~ /^All/ } - specify { assigns(:current_category).should == :all } - end - - describe '#popular' do - let(:asciicasts) { [double('asciicast')] } - let(:page) { double('page').to_s } - - before do - Asciicast.should_receive(:popular_paginated). - with(page, an_instance_of(Fixnum)).and_return(asciicasts) - - PaginatingDecorator.should_receive(:new).with(asciicasts). - and_return(asciicasts) - get :popular, :page => page + it 'renders template with asciicast_list' do + expect(controller).to have_received(:render). + with(locals: { asciicast_list: decorated_asciicast_list }) end - - it { should be_success } - specify { assigns(:asciicasts).should == asciicasts } - specify { assigns(:category_name).should =~ /^Popular/ } - specify { assigns(:current_category).should == :popular } end describe '#show' do diff --git a/spec/decorators/asciicast_list_decorator_spec.rb b/spec/decorators/asciicast_list_decorator_spec.rb new file mode 100644 index 0000000..228fed2 --- /dev/null +++ b/spec/decorators/asciicast_list_decorator_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe AsciicastListDecorator do + + let(:decorator) { described_class.new(list, 3, 10) } + let(:list) { double('list', category: :foo, items: items) } + let(:items) { double('items', paginate: paginated) } + let(:paginated) { [Asciicast.new] } + + describe '#category_name' do + subject { decorator.category_name } + + it { should eq('Foo asciicasts') } + end + + describe '#items' do + subject { decorator.items } + + it 'returns the items paginated' do + expect(subject).to eq(paginated) + expect(items).to have_received(:paginate).with(3, 10) + end + + it 'wraps the paginated items in a PaginatingDecorator' do + paginating_decorator = double('paginating_decorator') + + allow(PaginatingDecorator).to receive(:new). + with(paginated) { paginating_decorator } + + expect(subject).to be(paginating_decorator) + end + end + +end diff --git a/spec/features/asciicasts_spec.rb b/spec/features/asciicasts_spec.rb index bad18cd..f21c72c 100644 --- a/spec/features/asciicasts_spec.rb +++ b/spec/features/asciicasts_spec.rb @@ -2,25 +2,26 @@ require 'spec_helper' feature "Asciicast lists" do - let!(:asciicast) { create(:asciicast) } + let!(:asciicast) { create(:asciicast, title: 'foo bar') } + let!(:featured_asciicast) { create(:asciicast, title: 'qux', featured: true) } scenario 'Visiting all' do visit browse_path expect(page).to have_content(/All Asciicasts/i) expect_browse_links - expect(page).to have_link("bashing") - expect(page).to have_selector('.supplimental .play-button') + expect(page).to have_link("foo bar") + expect(page).to have_selector('.asciicast-list .play-button') end - scenario 'Visiting popular' do + scenario 'Visiting featured' do visit asciicast_path(asciicast) - visit popular_path + visit category_path(:featured) - expect(page).to have_content(/Popular Asciicasts/i) + expect(page).to have_content(/Featured Asciicasts/i) expect_browse_links - expect(page).to have_link("bashing") - expect(page).to have_selector('.supplimental .play-button') + expect(page).to have_link("qux") + expect(page).to have_selector('.asciicast-list .play-button') end end diff --git a/spec/models/asciicast_list_spec.rb b/spec/models/asciicast_list_spec.rb new file mode 100644 index 0000000..00dd15d --- /dev/null +++ b/spec/models/asciicast_list_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe AsciicastList do + + let(:list) { described_class.new(category, order, repository) } + + let(:category) { 'featured' } + let(:order) { 'recency' } + let(:repository) { double('repository') } + + describe '#category' do + subject { list.category } + + context "when it was passed as a string" do + let(:category) { 'thecat' } + + it { should eq(:thecat) } + end + + context "when it was passed as nil" do + let(:category) { nil } + + it { should eq(:all) } + end + end + + describe '#order' do + subject { list.order } + + context "when it was passed as a string" do + let(:order) { 'thecat' } + + it { should eq(:thecat) } + end + + context "when it was passed as nil" do + let(:order) { nil } + + it { should eq(:recency) } + end + end + + describe '#items' do + subject { list.items } + + let(:category) { 'foo' } + let(:order) { 'bar' } + let(:asciicasts) { [Asciicast.new] } + + before do + allow(repository).to receive(:for_category_ordered) { asciicasts } + subject + end + + it { should eq(asciicasts) } + + it 'calls for_category_ordered on repository with proper args' do + expect(repository).to have_received(:for_category_ordered). + with(:foo, :bar) + end + end + +end diff --git a/spec/models/asciicast_spec.rb b/spec/models/asciicast_spec.rb index c6d08ed..bd0ef06 100644 --- a/spec/models/asciicast_spec.rb +++ b/spec/models/asciicast_spec.rb @@ -3,6 +3,55 @@ require 'tempfile' describe Asciicast do + describe '.for_category_ordered' do + subject { described_class.for_category_ordered(category, order) } + + let!(:asciicast_1) { create(:asciicast, created_at: 2.hours.ago, + views_count: 10, + featured: false) } + let!(:asciicast_2) { create(:asciicast, created_at: 1.hour.ago, + views_count: 20, + featured: true) } + let!(:asciicast_3) { create(:asciicast, created_at: 4.hours.ago, + views_count: 30, + featured: false) } + let!(:asciicast_4) { create(:asciicast, created_at: 3.hours.ago, + views_count: 40, + featured: true) } + + context "when category is :all" do + let(:category) { :all } + + context "and order is :recency" do + let(:order) { :recency } + + it { should eq([asciicast_2, asciicast_1, asciicast_4, asciicast_3]) } + end + + context "and order is :popularity" do + let(:order) { :popularity } + + it { should eq([asciicast_4, asciicast_3, asciicast_2, asciicast_1]) } + end + end + + context "when category is :featured" do + let(:category) { :featured } + + context "and order is :recency" do + let(:order) { :recency } + + it { should eq([asciicast_2, asciicast_4]) } + end + + context "and order is :popularity" do + let(:order) { :popularity } + + it { should eq([asciicast_4, asciicast_2]) } + end + end + end + let(:asciicast) { described_class.new } describe '#stdout' do diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index 477639e..1a6da5c 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -3,7 +3,7 @@ module Asciinema def expect_browse_links expect(page).to have_link('All') - expect(page).to have_link('Popular') + expect(page).to have_link('Featured') end def expect_doc_links