Merge branch 'master' into comments

* master:
  Display asciicast author and creation time
  Claiming asciicasts
  User#add_user_token
  Asciicast.assign_user
  User tokens for assigning asciicasts to users
  Better styles + pagination

Conflicts:
	app/models/asciicast.rb
	db/schema.rb
	spec/factories/asciicasts.rb
	spec/factories/users.rb
This commit is contained in:
Marcin Kulik 2012-03-04 21:14:35 +01:00
commit c4f40322ed
31 changed files with 486 additions and 134 deletions

View File

@ -10,6 +10,7 @@ gem 'omniauth'
gem 'omniauth-twitter'
gem 'omniauth-github'
gem 'bzip2-ruby'
gem 'kaminari'
# Gems used only for assets and not required
# in production environments by default.

View File

@ -98,6 +98,10 @@ GEM
thor (~> 0.14)
json (1.6.5)
json_pure (1.6.5)
kaminari (0.13.0)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
railties (>= 3.0.0)
libnotify (0.7.2)
linecache19 (0.5.12)
ruby_core_source (>= 0.1.4)
@ -221,6 +225,7 @@ DEPENDENCIES
jasmine
jasminerice
jquery-rails
kaminari
libnotify
mysql2
omniauth

View File

@ -4,5 +4,6 @@
* the top of the compiled file, but it's generally better to create a new file per style scope.
*= require_self
*= require reset
*= require main
*= require_tree .
*/

View File

@ -2,9 +2,31 @@
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
$color1: #00A0B0;
$color2: #6A4A3C;
$color3: #CC333F;
$color4: #EB6841;
$color5: #EDC951;
.asciicasts {
list-style: none;
.asciicast-preview {
h3 {
font-size: 16px;
a {
color: $color1;
}
}
margin-bottom: 30px;
}
}
.asciicast {
width: 100%;
background-color: #222;
// width: 100%;
// background-color: #222;
.player {
/* border: 1px solid #777;*/
@ -12,7 +34,7 @@
float: left;
display: block;
padding: 0px;
margin: 0px 0px 30px 20px;
// margin: 0px 0px 30px 20px;
position: relative;
.terminal {
@ -22,7 +44,7 @@
font-family: 'Droid Sans Mono', Monospace;
white-space: pre;
background-color: black;
line-height: 1.2em;
// line-height: 1.2em;
color: #ccc;
overflow: auto;
overflow-x: hidden;
@ -53,37 +75,39 @@
bottom: 0px;
height: 30px;
color: white;
font-weight: bold;
font-family: monospace;
-webkit-transition: opacity 0.3s ease-in-out;
.toggle {
background-color: blue;
background-color: $color1;
width: 30px;
height: 100%;
float: left;
cursor: pointer;
&.paused {
background-color: orange;
background-color: $color4;
}
}
.progress {
background-color: black;
background-color: $color2;
width: 60%;
height: 100%;
float: left;
cursor: pointer;
// cursor: pointer;
.gutter {
background-color: #333;
background-color: $color5;
width: 0px;
height: 100%;
}
}
.time {
background-color: green;
background-color: $color3;
width: 30%;
height: 100%;
float: left;
@ -635,14 +659,19 @@
.bg254 { background-color: #e4e4e4 }
.bg255 { background-color: #eeeeee }
.description {
color: #666;
font-family: arial, helvetica;
font-size: 16px;
margin: 20px;
.meta {
.avatar {
width: 24px;
padding: 1px;
background-color: white;
border: 1px solid #ccc;
}
}
.description p {
margin-bottom: 20px;
.description {
color: #3e3e3e;
font-family: arial, helvetica;
font-size: 18px;
margin: 20px 0;
}
}

View File

@ -4,86 +4,109 @@
visibility: hidden;
}
// color pallette: http://www.colourlovers.com/palette/580974/Adrift_in_Dreams
$color1: #00A0B0;
$color2: #6A4A3C;
$color3: #CC333F;
$color4: #EB6841;
$color5: #EDC951;
body {
background-color: #E7E7DE;
background-color: #f0f0f0;
margin: 0;
padding: 0;
}
body, div, p {
font-family: arial, helvetica;
font-size: 12px;
font-size: 14px;
}
/* 325A66 */
/* DEA140 */
/* A32B26 */
/* 590D0B */
header {
background-color: $color5;
height: 40px;
h1 {
color: #3e3e3e;
font-family: 'Trebuchet MS';
font-size: 42px;
padding: 15px 0;
nav {
height: 100%;
h1 {
height: 100%;
float: left;
background-color: black;
font-family: 'Trebuchet MS';
font-size: 26px;
padding-right: 10px;
a {
color: white;
vertical-align: middle;
margin: 6px 0 0 0;
display: block;
}
}
ul {
list-style: none;
height: 100%;
float: right;
background-color: $color1;
li {
color: #eee;
display: inline-block;
margin-right: 20px;
height: 100%;
}
.avatar {
height: 100%;
}
}
}
}
a {
text-decoration: none;
color: $color2;
}
p {
margin-bottom: 20px;
}
// a.logout {
// background-image: url(image-path("logout.png"));
// background-repeat: no-repeat;
// }
.flash {
vertical-align: middle;
padding: 10px 0 10px 10px;
font-weight: bold;
}
.flash#notice {
color: black;
background-color: white;
}
.flash#alert {
color: red;
background-color: white;
}
h2 {
color: #3e3e3e;
font-family: 'Trebuchet MS';
font-size: 26px;
padding: 15px 15px 40px 15px;
// color: #3e3e3e;
color: $color3;
font-family: monospace;
font-size: 20px;
padding: 15px 0px 30px 0px;
}
#top {
position: relative;
height: 80px;
}
#logo {
display: block;
width: 300px;
position: absolute;
top: 0;
left: 0;
}
#topnav {
display: block;
width: 300px;
position: absolute;
top: 0;
right: 0;
font-size: 11px;
font-family: Verdana;
}
#session-info {
background-color: #172322;
list-style: none;
padding: 10px;
border-bottom-left-radius: 10px;
li {
color: #eee;
display: inline-block;
margin-right: 20px;
}
.avatar {
width: 24px;
height: 24px;
}
}
#all {
margin: 0 auto;
width: 980px;
#content {
width: 960px;
text-align: left;
}
#main {
background-color: white;
width: 980px;
font-size: 11px;
border-top-left-radius: 10px;
font-size: 12px;
padding: 20px;
}

View File

@ -1,8 +1,10 @@
class AsciicastsController < ApplicationController
PER_PAGE = 20
respond_to :html, :json
def index
@asciicasts = Asciicast.order("created_at DESC")
@asciicasts = Asciicast.order("created_at DESC").page(params[:page]).per(PER_PAGE)
end
def show

View File

@ -0,0 +1,21 @@
class UserTokensController < ApplicationController
# before_filter :ensure_authenticated!
def create
ut = current_user.add_user_token(params[:user_token])
if ut.valid?
claimed_num = Asciicast.assign_user(ut.token, current_user)
if claimed_num > 0
notice = "Claimed #{claimed_num} asciicasts, yay!"
else
notice = "Authenticated successfully, yippie!"
end
redirect_to root_path, :notice => notice
else
render :error
end
end
end

View File

@ -1,5 +1,25 @@
module AsciicastsHelper
def asciicast_title(asciicast)
if asciicast.title.present?
asciicast.title
elsif asciicast.command.present?
"$ #{asciicast.command}"
else
"##{asciicast.id}"
end
end
def asciicast_author(asciicast)
if asciicast.user
link_to avatar_img(asciicast.user) + " #{asciicast.user.nickname}", '#'
end
end
def asciicast_time(asciicast)
time_ago_in_words(asciicast.created_at) + " ago"
end
def player_script(asciicast)
return <<EOS.html_safe
<script>

View File

@ -7,11 +7,22 @@ class Asciicast < ActiveRecord::Base
validates :stdout, :stdout_timing, :presence => true
validates :terminal_columns, :terminal_lines, :duration, :presence => true
belongs_to :user
has_many :comments, :order => :created_at
before_create :assign_user, :unless => :user
attr_reader :description
def self.assign_user(user_token, user)
where(:user_id => nil, :user_token => user_token).
update_all(:user_id => user.id, :user_token => nil)
end
def meta=(file)
data = JSON.parse(file.tempfile.read)
self.user_token = data['user_token']
self.duration = data['duration']
self.recorded_at = data['recorded_at']
self.title = data['title']
@ -45,4 +56,13 @@ class Asciicast < ActiveRecord::Base
nil
end
end
def assign_user
if user_token.present?
if ut = UserToken.find_by_token(user_token)
self.user = ut.user
self.user_token = nil
end
end
end
end

View File

@ -4,6 +4,8 @@ class User < ActiveRecord::Base
validate :uid, :presence => true
validate :nickname, :presence => true
has_many :user_tokens
def self.create_with_omniauth(auth)
create! do |user|
user.provider = auth["provider"]
@ -14,4 +16,7 @@ class User < ActiveRecord::Base
end
end
def add_user_token(token)
user_tokens.find_or_create_by_token(token)
end
end

5
app/models/user_token.rb Normal file
View File

@ -0,0 +1,5 @@
class UserToken < ActiveRecord::Base
belongs_to :user
validates :user, :token, :presence => true
end

View File

@ -0,0 +1,6 @@
<li class="asciicast-preview">
<h3><%= link_to asciicast_title(asciicast), asciicast %></h3>
<% if asciicast.description.present? %>
<p class="description"><%= asciicast.description %></p>
<% end %>
</li>

View File

@ -1,9 +1,7 @@
<h2>Some title</h2>
<h2>Recent asciicasts</h2>
<ul class="asciicasts">
<% @asciicasts.each do |asciicast| %>
<li class="asciicast">
<a href="/<%= asciicast.id %>"><%= asciicast.id %></a>
</li>
<% end %>
<%= render :partial => "asciicasts/preview", :collection => @asciicasts, :as => :asciicast %>
</ul>
<%= paginate @asciicasts %>

View File

@ -1,16 +1,19 @@
<h2>#<%= @asciicast.id %> <%= @asciicast.title %></h2>
<div class="asciicast">
<div class="player">
<%# <pre class="term"></pre> %>
<%# <div class="hud">--------------========</div> %>
</div>
<div class="clear"></div>
</div>
<h2><%= asciicast_title(@asciicast) %></h2>
<p class="meta">
<%= asciicast_author(@asciicast) %>
<%= asciicast_time(@asciicast) %>
</p>
<div class="description">
<p>Ive been doing some reflecting this week on how I can work smarter (instead of harder), and one of the things I came up with was adding a few more tools to my Vim repertoire. I spend more than half of my engineering time in Vim (the other half usually being in a web browser), so I figured that a few minutes here and there would eventually add up in a big way.</p>
<p>But like anything else with Vim, there are always multiple ways of accomplishing the very same thing, so I make no guarantees that there arent simpler ways of getting this done — but I can say that this way gets the job done, and is pretty easy to get working on your own system.</p>
<div class="wrapper">
<div class="player"></div>
<div class="clear"></div>
</div>
<div class="description">
<p>Ive been doing some reflecting this week on how I can work smarter (instead of harder), and one of the things I came up with was adding a few more tools to my Vim repertoire. I spend more than half of my engineering time in Vim (the other half usually being in a web browser), so I figured that a few minutes here and there would eventually add up in a big way.</p>
<p>But like anything else with Vim, there are always multiple ways of accomplishing the very same thing, so I make no guarantees that there arent simpler ways of getting this done — but I can say that this way gets the job done, and is pretty easy to get working on your own system.</p>
</div>
</div>
<%= player_script(@asciicast) %>

View File

@ -2,7 +2,7 @@
<% if current_user %>
<li>
<%= avatar_img current_user %>
<%= link_to "Log out", logout_path %>
<%= link_to "Log out", logout_path, :class => "logout" %>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>

View File

@ -10,27 +10,26 @@
</head>
<body>
<div id="all">
<div id="top">
<div id="logo">
<h1>ascii.io</h1>
</div>
<div id="topnav">
<%= render :partial => 'layouts/session_info' %>
</div>
</div>
<div id="main">
<% if notice %>
<p id="notice" ><%= notice %></p>
<% elsif alert %>
<p id="alert" ><%= alert %></p>
<% end %>
<header>
<nav>
<h1>
<%= link_to "ascii.io", "/", :class => "home" %>
</h1>
<%= render :partial => 'layouts/session_info' %>
</nav>
</header>
<%= yield %>
<div id="content">
<% if notice %>
<p id="notice" class="flash"><%= notice %></p>
<% elsif alert %>
<p id="alert" class="flash"><%= alert %></p>
<% end %>
<div class="clear"></div>
</div>
<%= yield %>
</div>
<footer>
</footer>
</body>
</html>

View File

@ -0,0 +1,3 @@
<h2>Oops.</h2>
<p>Seems like the token is incorrect. Please make sure you've pasted it correctly.</p>

View File

@ -17,5 +17,7 @@ AsciiIo::Application.routes.draw do
match "/login" => "sessions#new"
match "/logout" => "sessions#destroy"
match "/connect/:user_token" => "user_tokens#create"
root :to => 'asciicasts#index'
end

View File

@ -0,0 +1,7 @@
class AddUserTokenToAsciicast < ActiveRecord::Migration
def change
add_column :asciicasts, :user_token, :string
add_index :asciicasts, :user_token
end
end

View File

@ -0,0 +1,13 @@
class CreateUserTokens < ActiveRecord::Migration
def change
create_table :user_tokens do |t|
t.integer :user_id, :null => false
t.string :token, :null => false
t.timestamps
end
add_index :user_tokens, :user_id
add_index :user_tokens, :token
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20120227230718) do
ActiveRecord::Schema.define(:version => 20120304162005) do
create_table "asciicasts", :force => true do |t|
t.integer "user_id"
@ -24,17 +24,29 @@ ActiveRecord::Schema.define(:version => 20120227230718) do
t.string "command"
t.string "shell"
t.string "uname"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.datetime "created_at"
t.datetime "updated_at"
t.string "stdin"
t.string "stdin_timing"
t.string "stdout"
t.string "stdout_timing"
t.string "user_token"
end
add_index "asciicasts", ["created_at"], :name => "index_asciicasts_on_created_at"
add_index "asciicasts", ["recorded_at"], :name => "index_asciicasts_on_recorded_at"
add_index "asciicasts", ["user_id"], :name => "index_asciicasts_on_user_id"
add_index "asciicasts", ["user_token"], :name => "index_asciicasts_on_user_token"
create_table "user_tokens", :force => true do |t|
t.integer "user_id", :null => false
t.string "token", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
add_index "user_tokens", ["token"], :name => "index_user_tokens_on_token"
add_index "user_tokens", ["user_id"], :name => "index_user_tokens_on_user_id"
create_table "comments", :force => true do |t|
t.text "body"

View File

@ -0,0 +1,49 @@
require 'spec_helper'
describe UserTokensController do
describe '#create' do
let(:user) { Factory(:user) }
let(:user_token) { Factory.build(:user_token, :user => nil) }
before do
@controller.stub!(:current_user => user)
user.stub!(:add_user_token => user_token)
end
context 'when given token is valid' do
before do
user_token.stub!(:valid? => true)
end
it 'calls Asciicast.assign_user' do
Asciicast.should_receive(:assign_user).with(user_token.token, user).and_return(1)
post :create, :user_token => user_token.token
end
it 'redirects to root_path' do
post :create, :user_token => user_token.token
response.should redirect_to(root_path)
end
end
context 'when given token is invalid' do
before do
user_token.stub!(:valid? => false)
end
it 'calls Asciicast.assign_user' do
Asciicast.should_not_receive(:assign_user)
post :create, :user_token => user_token.token
end
it 'renders :error' do
post :create, :user_token => user_token.token
response.should render_template(:error)
end
end
end
end

View File

@ -3,17 +3,16 @@ include ActionDispatch::TestProcess
FactoryGirl.define do
factory :asciicast do
user_id 1
association :user
title "bashing"
duration 100
recorded_at "2011-11-23 22:06:07"
terminal_type "xterm"
terminal_columns 80
terminal_lines 25
shell "/bin/bash"
uname "uname"
stdout { fixture_file_upload("spec/fixtures/stdout", "application/octet-stream") }
stdout_timing { fixture_file_upload("spec/fixtures/stdout", "application/octet-stream") }
title "MyString"
duration 1
recorded_at "2011-11-23 22:06:07"
terminal_type "MyString"
terminal_columns 1
terminal_lines 1
command "MyString"
shell "MyString"
uname "MyString"
end
end

View File

@ -0,0 +1,8 @@
# Read about factories at http://github.com/thoughtbot/factory_girl
FactoryGirl.define do
factory :user_token do
association :user
sequence(:token) { |n| "2b4b4e02-6613-11e1-9be5-#{Kernel.format('%012i', n)}" }
end
end

14
spec/fixtures/asciicasts/1/meta.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"command": null,
"duration": 5.914434194564819,
"recorded_at": "Sun, 04 Mar 2012 16:43:52 +0000",
"shell": "/bin/zsh",
"term": {
"columns": 89,
"lines": 27,
"type": "rxvt-unicode-256color"
},
"title": null,
"uname": "Linux 3.2.6-3.fc16.x86_64 #1 SMP Mon Feb 13 20:35:42 UTC 2012 x86_64",
"user_token": "2b4b4e02-6613-11e1-9be5-00215c6bbb11"
}

BIN
spec/fixtures/asciicasts/1/stdout vendored Normal file

Binary file not shown.

BIN
spec/fixtures/asciicasts/1/stdout.time vendored Normal file

Binary file not shown.

View File

@ -1,5 +1,70 @@
require 'spec_helper'
describe Asciicast do
pending "add some examples to (or delete) #{__FILE__}"
it "has valid factory" do
Factory.build(:asciicast).should be_valid
end
describe '.assign_user' do
let(:user) { Factory(:user) }
let(:token) { 'token' }
let!(:asciicast) { Factory(:asciicast, :user => nil, :user_token => token) }
subject { Asciicast.assign_user(token, user) }
it 'returns number of updated records' do
subject.should == 1
end
it 'assigns user to matching asciicasts' do
subject
asciicast.reload.user.should == user
end
end
describe '#save' do
let(:asciicast) { Factory.build(:asciicast, :user => user) }
context 'when no user given' do
let(:user) { nil }
it 'calls #assign_user' do
asciicast.should_receive(:assign_user)
asciicast.save
end
end
context 'when user given' do
let(:user) { Factory.build(:user) }
it "doesn't call #assign_user" do
asciicast.should_not_receive(:assign_user)
asciicast.save
end
end
end
describe '#assign_user' do
let(:user) { Factory(:user) }
let(:asciicast) { Factory(:asciicast, :user => nil, :user_token => user_token) }
context 'when user exists with given token' do
let(:user_token) { Factory(:user_token, :user => user).token }
it 'assigns user and resets user_token' do
asciicast.assign_user
asciicast.user.should == user
asciicast.user_token.should be(nil)
end
end
context 'when there is no user with given token' do
let(:user_token) { 'some-foo-bar' }
it 'assigns user' do
asciicast.assign_user
asciicast.user.should be(nil)
end
end
end
end

View File

@ -47,4 +47,28 @@ describe User do
end
end
describe '#add_user_token' do
before { user.save }
context "when user doesn't have given token" do
let(:token) { Factory.attributes_for(:user_token)[:token] }
it 'returns created UserToken' do
ut = user.add_user_token(token)
ut.should be_kind_of(UserToken)
ut.id.should_not be(nil)
end
end
context "when user doesn't have given token" do
let(:existing_token) { Factory(:user_token, :user => user) }
let(:token) { existing_token.token }
it 'returns existing UserToken' do
ut = user.add_user_token(token)
ut.should == existing_token
end
end
end
end

View File

@ -0,0 +1,7 @@
require 'spec_helper'
describe UserToken do
it "has valid factory" do
Factory.build(:user_token).should be_valid
end
end

View File

@ -0,0 +1,11 @@
require 'spec_helper'
describe 'connect routing' do
it 'routes /connect/:user_token to user_tokens#create for user_token' do
{ :get => '/connect/jolka-misio' }.should route_to(
:controller => 'user_tokens',
:action => 'create',
:user_token => 'jolka-misio'
)
end
end