Merge branch 'private-asciicasts'

element
Marcin Kulik 10 years ago
commit c6a0ae187b

@ -19,4 +19,8 @@ $(function() {
$('input[data-behavior=focus]:first').focus().select();
$('[data-toggle="popover"]').popover({ html: true });
if ($('meta[name=referrer][content=none]').length > 0) {
$('a[href*=http]').attr('rel', 'noreferrer');
}
});

@ -19,7 +19,7 @@ module Api
end
def show
@asciicast = Asciicast.find(params[:id])
@asciicast = Asciicast.find_by_id_or_secret_token!(params[:id])
respond_with(asciicast) do |format|
format.html do

@ -69,7 +69,7 @@ class AsciicastsController < ApplicationController
private
def load_resource
@asciicast = Asciicast.find(params[:id])
@asciicast = Asciicast.find_by_id_or_secret_token!(params[:id])
end
def view_counter

@ -26,7 +26,7 @@ class AsciicastDecorator < ApplicationDecorator
end
def title
model.title.presence || command || "asciicast:#{id}"
model.title.presence || command || "asciicast:#{to_param}"
end
def command

@ -24,10 +24,19 @@ class Asciicast < ActiveRecord::Base
scope :featured, -> { where(featured: true) }
scope :by_recency, -> { order("created_at DESC") }
scope :by_random, -> { order("RANDOM()") }
scope :latest_limited, -> (n) { by_recency.limit(n).includes(:user) }
scope :random_featured_limited, -> (n) {
featured.by_random.limit(n).includes(:user)
}
scope :non_private, -> { where(private: false) }
scope :homepage_latest, -> { non_private.by_recency.limit(6).includes(:user) }
scope :homepage_featured, -> { non_private.featured.by_random.limit(6).includes(:user) }
before_create :generate_secret_token
def self.find_by_id_or_secret_token!(thing)
if thing.size == 25
find_by_secret_token!(thing)
else
non_private.find(thing)
end
end
def self.cache_key
timestamps = scoped.select(:updated_at).map { |o| o.updated_at.to_i }
@ -39,7 +48,7 @@ class Asciicast < ActiveRecord::Base
end
def self.for_category_ordered(category, order, page = nil, per_page = nil)
collection = all
collection = non_private
if category == :featured
collection = collection.featured
@ -62,6 +71,18 @@ class Asciicast < ActiveRecord::Base
value ? super(value.strip[0...255]) : super
end
def self.generate_secret_token
SecureRandom.hex.to_i(16).to_s(36).rjust(25, '0')
end
def to_param
if private?
secret_token
else
id.to_s
end
end
def stdout
return @stdout if @stdout
@stdout = Stdout::Buffered.new(get_stdout)
@ -86,6 +107,10 @@ class Asciicast < ActiveRecord::Base
!image.file || (image.file.filename != image_filename)
end
def owner?(user)
user && self.user == user
end
private
def get_stdout
@ -103,4 +128,10 @@ class Asciicast < ActiveRecord::Base
Digest::SHA1.hexdigest(input)
end
def generate_secret_token
begin
self.secret_token = self.class.generate_secret_token
end while self.class.exists?(secret_token: secret_token)
end
end

@ -103,16 +103,20 @@ class User < ActiveRecord::Base
end
end
def public_asciicast_count
asciicasts.non_private.count
end
def asciicast_count
asciicasts.count
end
def asciicasts_excluding(asciicast, limit)
asciicasts.where('id <> ?', asciicast.id).order('RANDOM()').limit(limit)
def other_asciicasts(asciicast, limit)
asciicasts.non_private.where('id <> ?', asciicast.id).order('RANDOM()').limit(limit)
end
def paged_asciicasts(page, per_page)
asciicasts.
def paged_asciicasts(page, per_page, include_private)
asciicasts_scope(include_private).
includes(:user).
order("created_at DESC").
paginate(page, per_page)
@ -131,7 +135,15 @@ class User < ActiveRecord::Base
def generate_auth_token
begin
self[:auth_token] = self.class.generate_auth_token
end while User.exists?(auth_token: self[:auth_token])
end while self.class.exists?(auth_token: self[:auth_token])
end
def asciicasts_scope(include_private)
if include_private
asciicasts
else
asciicasts.non_private
end
end
end

@ -7,9 +7,10 @@ class AsciicastPolicy < ApplicationPolicy
end
def permitted_attributes
if user.admin? || record.user == user
if user.admin? || record.owner?(user)
attrs = [:title, :description, :theme_name, :snapshot_at]
attrs << :featured if user.admin?
attrs << :featured if change_featured?
attrs << :private if change_visibility?
attrs
else
@ -20,22 +21,22 @@ class AsciicastPolicy < ApplicationPolicy
def update?
return false unless user
user.admin? || record.user == user
user.admin? || record.owner?(user)
end
def destroy?
return false unless user
user.admin? || record.user == user
user.admin? || record.owner?(user)
end
def feature?
def change_featured?
return false unless user
user.admin?
end
def unfeature?
def change_visibility?
return false unless user
user.admin?

@ -87,11 +87,19 @@ class AsciicastPagePresenter
end
def show_set_featured_link?
!asciicast.featured? && policy.feature?
!asciicast.featured? && policy.change_featured?
end
def show_unset_featured_link?
asciicast.featured? && policy.unfeature?
asciicast.featured? && policy.change_featured?
end
def show_make_private_link?
!asciicast.private? && policy.change_visibility?
end
def show_make_public_link?
asciicast.private? && policy.change_visibility?
end
def show_description?
@ -110,12 +118,8 @@ class AsciicastPagePresenter
end
end
def show_other_asciicasts_by_author?
author.asciicast_count > 1
end
def other_asciicasts_by_author
author.asciicasts_excluding(asciicast, 3).decorate
@other_asciicasts_by_author ||= author.other_asciicasts(asciicast, 3).decorate
end
def asciicast_oembed_url(format)

@ -18,7 +18,7 @@ class BareAsciicastPagePresenter
end
def asciicast_id
asciicast.id
asciicast.to_param
end
end

@ -11,11 +11,11 @@ class HomePagePresenter
end
def latest_asciicasts
Asciicast.latest_limited(6).decorate
Asciicast.homepage_latest.decorate
end
def featured_asciicasts
Asciicast.random_featured_limited(6).decorate
Asciicast.homepage_featured.decorate
end
def install_script_url

@ -39,15 +39,17 @@ class UserPagePresenter
def asciicast_count_text(h)
if current_users_profile?
if user.asciicast_count > 0
count = h.pluralize(user.asciicast_count, 'asciicast')
count = user.asciicast_count
if count > 0
count = h.pluralize(count, 'asciicast')
"You have recorded #{count}"
else
"Record your first asciicast"
end
else
if user.asciicast_count > 0
count = h.pluralize(user.asciicast_count, 'asciicast')
count = user.public_asciicast_count
if count > 0
count = h.pluralize(count, 'asciicast')
"#{count} by #{user.display_name}"
else
"#{user.display_name} hasn't recorded anything yet"
@ -70,7 +72,8 @@ class UserPagePresenter
private
def get_asciicasts
PaginatingDecorator.new(user.paged_asciicasts(page, per_page))
asciicasts = user.paged_asciicasts(page, per_page, current_users_profile?)
PaginatingDecorator.new(asciicasts)
end
end

@ -4,4 +4,9 @@ class AsciicastSerializer < ActiveModel::Serializer
attributes :id, :duration, :stdout_frames_url, :snapshot
attribute :terminal_columns, key: :width
attribute :terminal_lines, key: :height
def private?
object.private?
end
end

@ -9,7 +9,7 @@ javascript:
if (typeof target != "undefined" && window !== window.parent) {
var w = $('.asciinema-player').width();
var h = $(document).height();
target.postMessage(['asciicast:size', { id: #{page.asciicast_id}, width: w, height: h }], '*');
target.postMessage(['asciicast:size', { id: '#{page.asciicast_id}', width: w, height: h }], '*');
}
function onMessage(e) {

@ -1,3 +1,10 @@
<% if asciicast.private? %>
<% content_for(:head) do %>
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="none">
<% end %>
<% end %>
<div class="player"></div>
<p class="processing-info" style="display: none">

@ -5,12 +5,12 @@ p
Some pilots get picked and become television programs. Some don't, become
nothing. She starred in one of the ones that became nothing.
script[type="text/javascript" src=asciicast_url(@asciicast, format: 'js') id="asciicast-#{@asciicast.id}" async data-speed="2"]
script[type="text/javascript" src=asciicast_url(@asciicast, format: 'js') id="asciicast-#{@asciicast.to_param}" async data-speed="2"]
p
' And now again. There should be an embedded player below this paragraph.
script[type="text/javascript" src=asciicast_url(@asciicast, format: 'js') id="asciicast-#{@asciicast.id}" async data-speed="2"]
script[type="text/javascript" src=asciicast_url(@asciicast, format: 'js') id="asciicast-#{@asciicast.to_param}" async data-speed="2"]
p
' This is at the bottom of the page, below all players.

@ -55,6 +55,16 @@
= link_to(asciicast_path(page.asciicast, 'asciicast[featured]' => 0), method: :put) do
span.glyphicon.glyphicon-eye-close
' Make not featured
- if page.show_make_public_link?
li
= link_to(asciicast_path(page.asciicast, 'asciicast[private]' => 0), method: :put) do
span.glyphicon.glyphicon-eye-open
' Make public
- if page.show_make_private_link?
li
= link_to(asciicast_path(page.asciicast, 'asciicast[private]' => 1), method: :put) do
span.glyphicon.glyphicon-eye-close
' Make private
- if page.show_delete_link?
li
= link_to(asciicast_path(page.asciicast), method: :delete, data: { confirm: 'Really delete this asciicast?' }) do
@ -72,7 +82,7 @@
.container
.content = page.description
- if page.show_other_asciicasts_by_author?
- unless page.other_asciicasts_by_author.empty?
section.even
.container
.other-asciicasts

@ -6,5 +6,6 @@ html[lang="en"]
title = page_title
= stylesheet_link_tag 'embed', :media => 'all'
= javascript_include_tag 'embed'
= content_for(:head)
body.iframe
= yield

@ -4,5 +4,6 @@ html[lang="en"]
meta[charset="utf-8"]
= screenshot_javascript_tag
= screenshot_stylesheet_tag
= content_for(:head)
body.screenshot
= yield

@ -0,0 +1,11 @@
class AddSecretTokenToAsciicasts < ActiveRecord::Migration
def change
add_column :asciicasts, :secret_token, :string
Asciicast.find_each do |asciicast|
asciicast.update_attribute(:secret_token, Asciicast.generate_secret_token)
end
change_column :asciicasts, :secret_token, :string, null: false
end
end

@ -0,0 +1,5 @@
class AddPrivateToAsciicasts < ActiveRecord::Migration
def change
add_column :asciicasts, :private, :boolean, null: false, default: false
end
end

@ -0,0 +1,5 @@
class AddIndexOnAsciicastsPrivate < ActiveRecord::Migration
def change
add_index :asciicasts, :private
end
end

@ -0,0 +1,5 @@
class AddUniqueIndexToAsciicastsSecretToken < ActiveRecord::Migration
def change
add_index :asciicasts, :secret_token, unique: true
end
end

@ -59,11 +59,15 @@ ActiveRecord::Schema.define(version: 20150401161102) do
t.string "image"
t.integer "image_width"
t.integer "image_height"
t.string "secret_token", null: false
t.boolean "private", default: false, null: false
end
add_index "asciicasts", ["created_at"], name: "index_asciicasts_on_created_at", using: :btree
add_index "asciicasts", ["featured"], name: "index_asciicasts_on_featured", using: :btree
add_index "asciicasts", ["likes_count"], name: "index_asciicasts_on_likes_count", using: :btree
add_index "asciicasts", ["private"], name: "index_asciicasts_on_private", using: :btree
add_index "asciicasts", ["secret_token"], name: "index_asciicasts_on_secret_token", unique: true, using: :btree
add_index "asciicasts", ["user_id"], name: "index_asciicasts_on_user_id", using: :btree
add_index "asciicasts", ["views_count"], name: "index_asciicasts_on_views_count", using: :btree

@ -1,5 +1,24 @@
require 'rails_helper'
shared_examples_for "asciicast iframe response" do
it "responds with status 200" do
expect(response.status).to eq(200)
end
it "responds with html content type" do
expect(response.headers['Content-Type']).to match('text/html')
end
it "responds without X-Frame-Options header" do
pending "the header is added back by Rails only in tests O_o"
expect(response.headers).to_not have_key('Content-Type')
end
it "responds with player page using iframe layout" do
expect(response.body).to have_selector('body.iframe div.player')
end
end
describe "Asciicast retrieval" do
let(:asciicast) { create(:asciicast) }
@ -18,24 +37,15 @@ describe "Asciicast retrieval" do
include Capybara::RSpecMatchers
before do
get "/api/asciicasts/#{asciicast.id}", format: 'html'
end
it "responds with status 200" do
expect(response.status).to eq(200)
get "/api/asciicasts/#{asciicast.to_param}", format: 'html'
end
it "responds with html content type" do
expect(response.headers['Content-Type']).to match('text/html')
end
it_behaves_like "asciicast iframe response"
it "responds without X-Frame-Options header" do
pending "the header is added back by Rails only in tests O_o"
expect(response.headers).to_not have_key('Content-Type')
end
context "for private asciicast" do
let(:asciicast) { create(:asciicast, private: true) }
it "responds with player page using iframe layout" do
expect(response.body).to have_selector('body.iframe div.player')
it_behaves_like "asciicast iframe response"
end
end

@ -41,7 +41,7 @@ describe AsciicastsController do
before do
allow(controller).to receive(:view_counter) { view_counter }
expect(Asciicast).to receive(:find).and_return(asciicast)
expect(Asciicast).to receive(:find_by_id_or_secret_token!).and_return(asciicast)
end
let(:asciicast_presenter) { double('asciicast_presenter') }
@ -74,7 +74,7 @@ describe AsciicastsController do
let(:make_request) { get :edit, :id => asciicast.id }
before do
expect(Asciicast).to receive(:find).and_return(asciicast)
expect(Asciicast).to receive(:find_by_id_or_secret_token!).and_return(asciicast)
asciicast.user = user
end
@ -114,7 +114,7 @@ describe AsciicastsController do
before do
allow(controller).to receive(:asciicast_updater) { asciicast_updater }
expect(Asciicast).to receive(:find).and_return(asciicast)
expect(Asciicast).to receive(:find_by_id_or_secret_token!).and_return(asciicast)
asciicast.user = user
end
@ -165,7 +165,7 @@ describe AsciicastsController do
let(:make_request) { delete :destroy, :id => asciicast.id }
before do
expect(Asciicast).to receive(:find).and_return(asciicast)
expect(Asciicast).to receive(:find_by_id_or_secret_token!).and_return(asciicast)
asciicast.user = user
end

@ -15,4 +15,15 @@ feature "Asciicast page", :js => true do
expect(page).to have_selector('.cinema .play-button')
end
scenario 'Visiting as guest when asciicast is private' do
asciicast.update(private: true)
visit asciicast_path(asciicast)
expect(page).to have_content('the title')
expect(page).to have_link('aaron')
expect(page).to have_link('Embed')
expect(page).to have_selector('.cinema .play-button')
end
end

@ -1,8 +1,6 @@
require 'rails_helper'
describe 'Asciicast playback', :js => true, :slow => true do
let(:asciicast) { create(:asciicast) }
describe 'Asciicast playback', js: true, slow: true do
describe "from fixture" do
before do
@ -14,10 +12,24 @@ describe 'Asciicast playback', :js => true, :slow => true do
Capybara.default_wait_time = @old_wait_time
end
it "is successful" do
visit asciicast_path(asciicast, speed: 5)
find(".start-prompt .play-button").click
expect(page).to have_css('.time-remaining', visible: false, text: '-00:0')
context "for public asciicast" do
let(:asciicast) { create(:asciicast, private: false) }
it "is successful" do
visit asciicast_path(asciicast, speed: 5)
find(".start-prompt .play-button").click
expect(page).to have_css('.time-remaining', visible: false, text: '-00:0')
end
end
context "for private asciicast" do
let(:asciicast) { create(:asciicast, private: true) }
it "is successful" do
visit asciicast_path(asciicast, speed: 5)
find(".start-prompt .play-button").click
expect(page).to have_css('.time-remaining', visible: false, text: '-00:0')
end
end
end

@ -3,6 +3,50 @@ require 'tempfile'
describe Asciicast do
describe '.find_by_id_or_secret_token!' do
subject { Asciicast.find_by_id_or_secret_token!(thing) }
context 'for public asciicast' do
let(:asciicast) { create(:asciicast, private: false) }
context 'when looked up by id' do
let(:thing) { asciicast.id }
it { should eq(asciicast) }
end
context 'when looked up by secret token' do
let(:thing) { asciicast.secret_token }
it { should eq(asciicast) }
end
end
context 'for private asciicast' do
let(:asciicast) { create(:asciicast, private: true) }
context 'when looked up by id' do
let(:thing) { asciicast.id }
it 'raises RecordNotFound' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'when looked up by secret token' do
let(:thing) { asciicast.secret_token }
it { should eq(asciicast) }
end
end
end
describe '.generate_secret_token' do
subject { Asciicast.generate_secret_token }
it { should match(/^[a-z0-9]{25}$/) }
end
describe '.for_category_ordered' do
subject { described_class.for_category_ordered(category, order) }
@ -18,6 +62,7 @@ describe Asciicast do
let!(:asciicast_4) { create(:asciicast, created_at: 3.hours.ago,
views_count: 40,
featured: true) }
let!(:asciicast_5) { create(:asciicast, private: true) }
context "when category is :all" do
let(:category) { :all }
@ -52,7 +97,27 @@ describe Asciicast do
end
end
let(:asciicast) { described_class.new }
describe '#to_param' do
subject { asciicast.to_param }
let(:asciicast) { Asciicast.new(id: 123, secret_token: 'sekrit') }
context 'for public asciicast' do
before do
asciicast.private = false
end
it { should eq('123') }
end
context 'for private asciicast' do
before do
asciicast.private = true
end
it { should eq('sekrit') }
end
end
describe '#stdout' do
context 'for single-file, JSON asciicast' do

@ -148,8 +148,8 @@ describe User do
it { should eq(2) }
end
describe '#asciicasts_excluding' do
subject { user.asciicasts_excluding(asciicast, 1) }
describe '#other_asciicasts' do
subject { user.other_asciicasts(asciicast, 1) }
let(:user) { create(:user) }
let(:asciicast) { create(:asciicast, user: user) }

@ -13,7 +13,7 @@ describe AsciicastPolicy do
let(:user) { stub_model(User, admin?: true) }
it "includes form fields + featured" do
expect(subject).to eq([:title, :description, :theme_name, :snapshot_at, :featured])
expect(subject).to eq([:title, :description, :theme_name, :snapshot_at, :featured, :private])
end
end
@ -27,7 +27,7 @@ describe AsciicastPolicy do
context "and is creator of the asciicast" do
let(:asciicast) { Asciicast.new(user: user) }
it "includes form field, but no featured" do
it "doesn't include featured but includes private" do
expect(subject).to eq([:title, :description, :theme_name, :snapshot_at])
end
end
@ -74,23 +74,7 @@ describe AsciicastPolicy do
end
end
permissions :feature? do
it "denies access if user is nil" do
expect(subject).not_to permit(nil, Asciicast.new)
end
it "grants access if user is admin" do
user = stub_model(User, admin?: true)
expect(subject).to permit(user, Asciicast.new)
end
it "denies access if user isn't admin" do
user = stub_model(User, admin?: false)
expect(subject).not_to permit(user, Asciicast.new)
end
end
permissions :unfeature? do
permissions :change_featured? do
it "denies access if user is nil" do
expect(subject).not_to permit(nil, Asciicast.new)
end

@ -155,26 +155,6 @@ describe AsciicastPagePresenter do
it { should eq('i am description') }
end
describe '#show_other_asciicasts_by_author?' do
subject { presenter.show_other_asciicasts_by_author? }
before do
allow(author).to receive(:asciicast_count) { count }
end
context "when user has more than 1 asciicast" do
let(:count) { 2 }
it { should be(true) }
end
context "when user doesn't have more than 1 asciicasts" do
let(:count) { 1 }
it { should be(false) }
end
end
describe '#other_asciicasts_by_author' do
subject { presenter.other_asciicasts_by_author }
@ -182,7 +162,7 @@ describe AsciicastPagePresenter do
let(:decorated_others) { double('decorated_others') }
before do
allow(author).to receive(:asciicasts_excluding).
allow(author).to receive(:other_asciicasts).
with(asciicast, 3) { others }
end

@ -25,7 +25,7 @@ describe BareAsciicastPagePresenter do
describe '#asciicast_id' do
subject { presenter.asciicast_id }
it { should eq(123) }
it { should eq('123') }
end
end

@ -43,7 +43,7 @@ describe HomePagePresenter do
let(:decorated_latest) { double('decorated_latest') }
before do
allow(Asciicast).to receive(:latest_limited) { latest }
allow(Asciicast).to receive(:homepage_latest) { latest }
end
it "returns decorated latest asciicasts" do
@ -58,10 +58,10 @@ describe HomePagePresenter do
let(:decorated_featured) { double('decorated_featured') }
before do
allow(Asciicast).to receive(:random_featured_limited) { featured }
allow(Asciicast).to receive(:homepage_featured) { featured }
end
it "returns decorated random featured asciicasts" do
it "returns decorated featured asciicasts" do
expect(subject).to be(decorated_featured)
end
end

@ -117,11 +117,23 @@ describe UserPagePresenter do
describe '#asciicast_count_text' do
subject { presenter.asciicast_count_text(view_context) }
before do
allow(user).to receive(:asciicast_count) { 3 }
context 'for non author' do
before do
allow(user).to receive(:public_asciicast_count) { 2 }
end
it { should match(/2.+cartman/) }
end
it { should eq('3 asciicasts by cartman') }
context 'for author' do
let(:current_user) { user }
before do
allow(user).to receive(:asciicast_count) { 3 }
end
it { should match(/you.+3/i) }
end
end
describe '#user_username' do
@ -143,7 +155,7 @@ describe UserPagePresenter do
it "gets user's asciicasts paged" do
subject
expect(user).to have_received(:paged_asciicasts).with(2, 5)
expect(user).to have_received(:paged_asciicasts).with(2, 5, false)
end
it "wraps the asciicasts with paginating decorator" do

Loading…
Cancel
Save