Commit 35b6af41 by tady

Merge branch 'master' of github.com:tadyjp/rendezvous

parents 51262d4e e64d04b5
source 'https://rubygems.org' source 'https://rubygems.org'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 4' gem 'rails', '~> 4.1'
# Use SCSS for stylesheets # Use SCSS for stylesheets
gem 'sass-rails' gem 'sass-rails'
...@@ -17,9 +17,6 @@ gem 'jquery-rails' ...@@ -17,9 +17,6 @@ gem 'jquery-rails'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes # See https://github.com/sstephenson/execjs#readme for more supported runtimes
gem 'therubyracer', platforms: :ruby gem 'therubyracer', platforms: :ruby
# Use jquery as the JavaScript library
# gem 'jquery-rails'
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
# gem 'turbolinks' # gem 'turbolinks'
...@@ -48,9 +45,6 @@ gem 'mysql2' ...@@ -48,9 +45,6 @@ gem 'mysql2'
gem 'devise' gem 'devise'
# http://d.hatena.ne.jp/tkawa/20130812/p1
gem 'devise-better_routes'
gem 'omniauth-google-oauth2' gem 'omniauth-google-oauth2'
# Markdown # Markdown
...@@ -93,6 +87,7 @@ group :development, :test do ...@@ -93,6 +87,7 @@ group :development, :test do
gem 'teaspoon' gem 'teaspoon'
gem 'guard-teaspoon' gem 'guard-teaspoon'
gem 'byebug'
end end
group :test do group :test do
......
...@@ -11,4 +11,6 @@ $ -> ...@@ -11,4 +11,6 @@ $ ->
# TODO # TODO
prettyPrint() prettyPrint()
$(document).on 'ajax:success', '.ajax_link', (data, res, xhr) ->
console.log(res)
$('#yield').html(res)
...@@ -21,3 +21,5 @@ ...@@ -21,3 +21,5 @@
_.mixin(_.string.exports()); _.mixin(_.string.exports());
// $('[data-toggle="tooltip"]').tooltip();
// $('[data-toggle="popover"]').popover();
...@@ -8,14 +8,22 @@ $.extend ...@@ -8,14 +8,22 @@ $.extend
settings.$input.fileupload settings.$input.fileupload
dataType: 'json' dataType: 'json'
done: (e, data) -> done: (e, data) ->
$.each data.result.files, (index, _file) -> $.each data.result.files, (index, file) ->
settings.$textarea.val(settings.$textarea.val() + "![" + _file.name + "](" + _file.url + ")\n") # TODO: カーソル位置に挿入
if file.type is 'image'
# image ![file-name](file-url)
settings.$textarea.val(settings.$textarea.val() + "\n![" + file.name + "](" + file.url + ")\n")
else if file.type is 'slide'
# slide !slide!(file-url)
settings.$textarea.val(settings.$textarea.val() + "\n!slide!(" + file.url + ")\n")
settings.$textarea.trigger("change") settings.$textarea.trigger("change")
# $('<p/>').text(file.name).appendTo('#files') # TODO # $('<p/>').text(file.name).appendTo('#files') # TODO
progressall: (e, data) -> # progressall: (e, data) ->
progress = parseInt(data.loaded / data.total * 100, 10) # progress = parseInt(data.loaded / data.total * 100, 10)
$('.progress-bar').css # $('.progress-bar').css
width: progress + '%' # width: progress + '%'
settings.$input.prop('disabled', !$.support.fileInput) settings.$input.prop('disabled', !$.support.fileInput)
.parent().addClass($.support.fileInput ? undefined : 'disabled'); .parent().addClass($.support.fileInput ? undefined : 'disabled');
......
...@@ -26,3 +26,7 @@ a .text-link { ...@@ -26,3 +26,7 @@ a .text-link {
a:visited .text-link { a:visited .text-link {
color: #609; color: #609;
} }
.popover {
max-width: 400px;
}
// Place all the styles related to the templates controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the watchings controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
...@@ -7,11 +7,11 @@ class ApisController < ApplicationController ...@@ -7,11 +7,11 @@ class ApisController < ApplicationController
def markdown_preview def markdown_preview
# TODO: not to use # TODO: not to use
render text: h_application_format_markdown(params[:text] || '') render text: MarkdownRenderer.new(params[:text]).render
end end
# Receive file and upload to S3
def file_receiver def file_receiver
s3 = AWS::S3.new s3 = AWS::S3.new
bucket_name = "#{Settings.s3.bucket_name}/1/#{current_user.id}" bucket_name = "#{Settings.s3.bucket_name}/1/#{current_user.id}"
# bucket_name = "1/#{current_user.id}" # bucket_name = "1/#{current_user.id}"
...@@ -22,12 +22,21 @@ class ApisController < ApplicationController ...@@ -22,12 +22,21 @@ class ApisController < ApplicationController
params[:files].each do |file| params[:files].each do |file|
basename = File.basename(file.path) basename = File.basename(file.path)
next unless file.original_filename =~ /\.(jpe?g|png|gif|pdf)\Z/
object_file_name = "#{Digest::MD5.file(file.path).to_s}#{File.extname(file.original_filename)}" object_file_name = "#{Digest::MD5.file(file.path).to_s}#{File.extname(file.original_filename)}"
obj = bucket.objects[object_file_name] obj = bucket.objects[object_file_name]
res = obj.write(file: file.path, acl: :public_read) res = obj.write(file: file.path, acl: :public_read)
s3_files << { name: file.original_filename, url: res.public_url.to_s } file_type = case file.original_filename
when /\.(jpe?g|png|gif)\Z/
:image
when /\.pdf\Z/
:slide
end
s3_files << { name: file.original_filename, url: res.public_url.to_s, type: file_type }
end end
render json: { status: 'OK', files: s3_files } render json: { status: 'OK', files: s3_files }
......
...@@ -41,8 +41,8 @@ module RV::Mailer ...@@ -41,8 +41,8 @@ module RV::Mailer
html_body = template html_body = template
.sub('__POST_URL__', Settings.rendezvous.app_host + post.decorate.show_path) .sub('__POST_URL__', Settings.rendezvous.app_host + post.decorate.show_path)
.sub('__HTML_TITLE__', h_application_format_markdown(post.title)) .sub('__HTML_TITLE__', MarkdownRenderer.new(post.title).render)
.sub('__HTML_BODY__', h_application_format_markdown(post.body)) .sub('__HTML_BODY__', MarkdownRenderer.new(post.body).render)
.sub('__RV_URL__', Settings.rendezvous.app_host + '/') .sub('__RV_URL__', Settings.rendezvous.app_host + '/')
premailer = Premailer.new(html_body, with_html_string: true, adapter: :nokogiri) premailer = Premailer.new(html_body, with_html_string: true, adapter: :nokogiri)
......
require 'nkf' require 'nkf'
class PostsController < ApplicationController class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :destroy, :slideshow] before_action :set_post, only: [:show, :edit, :update, :destroy, :slideshow, :watch]
include RV::Mailer include RV::Mailer
# GET /posts/1 # GET /posts/1
# GET /posts/1.json # GET /posts/1.json
def show def show
current_user.visit_post!(@post)
@post.tags.each do |_tag| @post.tags.each do |_tag|
add_breadcrumb("##{_tag.name}", _tag.decorate.show_path) add_breadcrumb("##{_tag.name}", _tag.decorate.show_path)
end end
...@@ -105,6 +107,19 @@ class PostsController < ApplicationController ...@@ -105,6 +107,19 @@ class PostsController < ApplicationController
render layout: 'slideshow' render layout: 'slideshow'
end end
def watch
if current_user.watching?(post: @post)
current_user.unwatch!(post: @post)
else
current_user.watch!(post: @post)
end
respond_to do |format|
format.html { render action: :show, layout: false }
format.json { head :no_content }
end
end
private private
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
......
class TemplatesController < ApplicationController
def show
end
end
class MeController < ApplicationController class UsersController < ApplicationController
before_action :set_user, only: [:edit, :update] before_action :set_user, only: [:edit, :update]
def edit def edit
...@@ -6,21 +7,20 @@ class MeController < ApplicationController ...@@ -6,21 +7,20 @@ class MeController < ApplicationController
def update def update
respond_to do |format| respond_to do |format|
if @me.update(user_params) if @user.update(user_params)
format.html { redirect_to edit_me_path, flash: { notice: 'Post was successfully updated.' } } format.html { redirect_to edit_user_path, flash: { notice: 'Post was successfully updated.' } }
format.json { head :no_content } format.json { head :no_content }
else else
format.html { render action: 'edit' } format.html { render action: 'edit' }
format.json { render json: @me.errors, status: :unprocessable_entity } format.json { render json: @user.errors, status: :unprocessable_entity }
end end
end end
end end
private private
# Use callbacks to share common setup or constraints between actions.
def set_user def set_user
@me = current_user @user = current_user
end end
def user_params def user_params
......
class WatchingsController < ApplicationController
def show
@posts = current_user.watching_posts.order(updated_at: :desc).page(params[:page]).decorate
end
end
...@@ -27,6 +27,14 @@ class PostDecorator < Draper::Decorator ...@@ -27,6 +27,14 @@ class PostDecorator < Draper::Decorator
end end
end end
def created_date
model.created_at.strftime('%Y-%m-%d')
end
def updated_date
model.updated_at.strftime('%Y-%m-%d')
end
def display_specified_date def display_specified_date
if model.specified_date if model.specified_date
model.specified_date.strftime('%Y-%m-%d') model.specified_date.strftime('%Y-%m-%d')
......
class TemplateDecorator < Draper::Decorator
delegate_all
# Define presentation-specific methods here. Helpers are accessed through
# `helpers` (aka `h`). You can override attributes, for example:
#
# def created_at
# helpers.content_tag :span, class: 'time' do
# object.created_at.strftime("%a %m/%d/%y")
# end
# end
end
...@@ -4,5 +4,4 @@ class UserDecorator < Draper::Decorator ...@@ -4,5 +4,4 @@ class UserDecorator < Draper::Decorator
def draft_count def draft_count
model.posts.where(is_draft: true).count model.posts.where(is_draft: true).count
end end
end end
class WatchingDecorator < Draper::Decorator
delegate_all
# Define presentation-specific methods here. Helpers are accessed through
# `helpers` (aka `h`). You can override attributes, for example:
#
# def created_at
# helpers.content_tag :span, class: 'time' do
# object.created_at.strftime("%a %m/%d/%y")
# end
# end
end
module ApplicationHelper module ApplicationHelper
def h_application_format_markdown(text) def h_application_format_markdown(text)
raise "deplicated error"
text = GitHub::Markdown.render_gfm(text) text = GitHub::Markdown.render_gfm(text)
text.html_safe text.html_safe
end end
......
module TemplatesHelper
end
module WatchingsHelper
end
...@@ -11,19 +11,39 @@ ...@@ -11,19 +11,39 @@
# #
class Comment < ActiveRecord::Base class Comment < ActiveRecord::Base
######################################################################
# Associations
######################################################################
belongs_to :author, class_name: 'User' belongs_to :author, class_name: 'User'
belongs_to :post belongs_to :post
######################################################################
# Validations
######################################################################
validates :author_id, presence: true validates :author_id, presence: true
validates :post_id, presence: true validates :post_id, presence: true
validates :body, presence: true validates :body, presence: true
### Callback ### ######################################################################
after_save :notify_author # Callback
######################################################################
after_save :set_watcher!
after_save :notify_watchers!
######################################################################
# Instance method
######################################################################
private private
def notify_author def notify_watchers!
post.author.push_notification(post.decorate.show_path, "#{author.name}さんがあなたの投稿にコメントしました") post.watchers.each do |watcher|
next if watcher == author
watcher.push_notification(post.decorate.show_path, "#{author.name}さんが「#{post.title}」にコメントしました。")
end
end
def set_watcher!
author.watch!(post: post)
end end
end end
# == Schema Information
#
# Table name: footprints
#
# id :integer not null, primary key
# user_id :integer not null
# post_id :integer not null
# created_at :datetime
# updated_at :datetime
#
class Footprint < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
class MarkdownRenderer
def initialize(text)
@text = text || ''
end
# pdf viewerの変換
# !slide!(file-url) -> %%slide:0%% -> <iframe>...</iframe>
def render
# slideのurlを一時保管
slide_urls = []
text = @text.gsub(/!slide!\(([^\)]+)\)/) do |_|
slide_urls << %Q{
<div class="embed-responsive embed-responsive-4by3">
<iframe style="text-align:center;" src="/ViewerJS/##{$1}" width="400" height="300" allowfullscreen="true" webkitallowfullscreen="true"></iframe>
</div>
}
"%%slide:#{slide_urls.size - 1}%%"
end
text = GitHub::Markdown.render_gfm(text)
# 保管したslide urlを取り出す
text = text.gsub(/%%slide:(\d+)%%/) do |_|
slide_urls[$1.to_i]
end
text.html_safe
end
end
# == Schema Information
#
# Table name: notifications
#
# id :integer not null, primary key
# user_id :integer
# read_at :datetime
# is_read :boolean default(FALSE), not null
# detail_path :string(255)
# body :text
# created_at :datetime
# updated_at :datetime
#
class Notification < ActiveRecord::Base class Notification < ActiveRecord::Base
belongs_to :user belongs_to :user
......
...@@ -16,12 +16,17 @@ ...@@ -16,12 +16,17 @@
require 'date' require 'date'
class Post < ActiveRecord::Base class Post < ActiveRecord::Base
######################################################################
# Associations
######################################################################
has_many :post_tags has_many :post_tags
has_many :tags, through: :post_tags has_many :tags, through: :post_tags
belongs_to :author, class_name: 'User' belongs_to :author, class_name: 'User'
has_many :comments has_many :comments
has_many :footprints
# default_scope { where(is_draft: false).order(:updated_at => :desc) } has_many :watches, :as => :watchable, :dependent => :destroy
has_many :watchers, :through => :watches
###################################################################### ######################################################################
# validations # validations
...@@ -30,6 +35,12 @@ class Post < ActiveRecord::Base ...@@ -30,6 +35,12 @@ class Post < ActiveRecord::Base
validates :body, presence: true validates :body, presence: true
###################################################################### ######################################################################
# Callback
######################################################################
after_save :set_watcher!
after_save :notify_watchers!
######################################################################
# Named scope # Named scope
###################################################################### ######################################################################
scope :search, (lambda do |query| scope :search, (lambda do |query|
...@@ -68,17 +79,21 @@ class Post < ActiveRecord::Base ...@@ -68,17 +79,21 @@ class Post < ActiveRecord::Base
order(updated_at: :desc).limit(limit) order(updated_at: :desc).limit(limit)
} }
######################################################################
# Instance method
######################################################################
# generate forked post (not saved) # generate forked post (not saved)
def generate_fork(user) def generate_fork(user)
# `id`以外をコピーする # `id`以外をコピーする
_forked_post = Post.new(self.attributes.except('id')) _forked_post = Post.new(self.attributes.except('id'))
# `%Name`をユーザー名に置換 # `%name`をユーザー名に置換
_forked_post.title = _forked_post.title.gsub(/%Name/, user.name) _forked_post.title = _forked_post.title.gsub(/%name/, user.name)
# `%Y`などを日付に変換 # `%Y`などを日付に変換
_forked_post.title = Time.now.strftime(_forked_post.title) # TODO _forked_post.title = Time.now.strftime(_forked_post.title) # TODO
_forked_post.title = _forked_post.title + ' のコピー' _forked_post.title = _forked_post.title
_forked_post.tag_ids = self.tag_ids _forked_post.tag_ids = self.tag_ids
_forked_post.author = user _forked_post.author = user
...@@ -91,4 +106,29 @@ class Post < ActiveRecord::Base ...@@ -91,4 +106,29 @@ class Post < ActiveRecord::Base
def body_for_slideshow def body_for_slideshow
self.body.gsub(/^#/, "---\n\n#") self.body.gsub(/^#/, "---\n\n#")
end end
def visited_user_count
footprints.select(:user_id).uniq.count
end
# FIXME:
# has_many :watchers, :through => :watches
# 正常に動作しないため動作しないため一時的にメソッドを作成
# def watchers
# watches.map { |watch| watch.watcher }
# end
private
def notify_watchers!
watchers.each do |watcher|
next if watcher == author
watcher.push_notification(decorate.show_path, "#{author.name}さんが「#{title}」を編集しました")
end
end
def set_watcher!
author.watch!(post: self)
end
end end
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
# updated_at :datetime # updated_at :datetime
# ancestry :string(255) # ancestry :string(255)
# body :text # body :text
# posts_count :integer default(0), not null
# #
class Tag < ActiveRecord::Base class Tag < ActiveRecord::Base
......
...@@ -33,11 +33,15 @@ class User < ActiveRecord::Base ...@@ -33,11 +33,15 @@ class User < ActiveRecord::Base
devise :omniauthable, omniauth_providers: [:google_oauth2] devise :omniauthable, omniauth_providers: [:google_oauth2]
###################################################################### ######################################################################
# association # Associations
###################################################################### ######################################################################
has_many :posts, foreign_key: 'author_id' has_many :posts, foreign_key: 'author_id'
has_many :comments, foreign_key: 'author_id' has_many :comments, foreign_key: 'author_id'
has_many :notifications has_many :notifications
has_many :footprints
has_many :watchings, class_name: 'Watch', foreign_key: 'watcher_id'
has_many :watching_posts, :through => :watchings, :source => :watchable, :source_type => "Post"
###################################################################### ######################################################################
# scope # scope
...@@ -52,7 +56,7 @@ class User < ActiveRecord::Base ...@@ -52,7 +56,7 @@ class User < ActiveRecord::Base
###################################################################### ######################################################################
# validations # Validations
###################################################################### ######################################################################
validates :name, presence: true validates :name, presence: true
validates :email, presence: true validates :email, presence: true
...@@ -80,6 +84,9 @@ class User < ActiveRecord::Base ...@@ -80,6 +84,9 @@ class User < ActiveRecord::Base
user user
end end
######################################################################
# instance methods
######################################################################
# check if google oauth token is expired # check if google oauth token is expired
def google_oauth_token_expired? def google_oauth_token_expired?
...@@ -111,5 +118,51 @@ class User < ActiveRecord::Base ...@@ -111,5 +118,51 @@ class User < ActiveRecord::Base
notifications.create(detail_path: detail_path, body: body, is_read: false) notifications.create(detail_path: detail_path, body: body, is_read: false)
end end
# record footprint
def visit_post!(post)
footprints.create!(post: post)
end
def watch!(hash)
if hash[:post]
watching_posts << hash[:post] unless watching_posts.include?(hash[:post])
elsif hash[:tag]
raise 'Not Implemented.'
elsif hash[:user]
raise 'Not Implemented.'
else
raise 'No hash argument set.'
end
end
def unwatch!(hash)
if hash[:post]
hash[:post].watches.where(watcher: self).destroy_all
elsif hash[:tag]
raise 'Not Implemented.'
elsif hash[:user]
raise 'Not Implemented.'
else
raise 'No hash argument set.'
end
end
# check if user watching post/tag/user
# TODO: tag/user
def watching?(hash)
if hash[:post]
hash[:post].watches.where(watcher: self).exists?
elsif hash[:tag]
raise 'Not Implemented.'
elsif hash[:user]
raise 'Not Implemented.'
else
raise 'No hash argument set.'
end
end
# def watching_posts
# ids = watching_items.where(resource_type: "Post").pluck(:resource_id)
# Post.where(id: ids)
# end
end end
class Watch < ActiveRecord::Base
######################################################################
# Associations
######################################################################
belongs_to :watcher, class_name: 'User'
belongs_to :watchable, polymorphic: true
######################################################################
# Validations
######################################################################
validates :watcher_id, uniqueness: { scope: [:watchable_type, :watchable_id] }
end
...@@ -27,5 +27,3 @@ ...@@ -27,5 +27,3 @@
a.list-group-item data-tag-id=tag.id href=search_path(q: "##{tag.name}") a.list-group-item data-tag-id=tag.id href=search_path(q: "##{tag.name}")
= tag.name = tag.name
span.badge = tag.posts_count span.badge = tag.posts_count
...@@ -4,11 +4,12 @@ html lang="ja" ...@@ -4,11 +4,12 @@ html lang="ja"
head head
title Rendezvous title Rendezvous
meta content="width=device-width, initial-scale=1.0" name="viewport" / meta content="width=device-width, initial-scale=1.0" name="viewport" /
link href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css" rel="stylesheet" / link href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet" /
link href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap-theme.min.css" rel="stylesheet" / link href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css" rel="stylesheet" /
link href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.css" rel="stylesheet" / link href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.css" rel="stylesheet" /
link href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.print.css" rel="stylesheet" / link href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.print.css" rel="stylesheet" /
link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet" link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet"
script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"
= stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "application", media: "all"
= render_style = render_style
= csrf_meta_tags = csrf_meta_tags
...@@ -16,13 +17,12 @@ html lang="ja" ...@@ -16,13 +17,12 @@ html lang="ja"
= render partial: 'partials/header_notifications' = render partial: 'partials/header_notifications'
- if params[:controller] != 'welcome' - if params[:controller] != 'welcome'
= render partial: 'partials/app_header' = render partial: 'partials/app_header'
.container.container-main .container.container-main#yield
= yield = yield
script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js" script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"
script src="//cdnjs.cloudflare.com/ajax/libs/underscore.string/2.3.3/underscore.string.min.js" script src="//cdnjs.cloudflare.com/ajax/libs/underscore.string/2.3.3/underscore.string.min.js"
script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js" script src="//netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"
script src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js"
script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js" script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"
script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.min.js" script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/1.6.4/fullcalendar.min.js"
javascript: javascript:
......
...@@ -18,12 +18,20 @@ nav.navbar.navbar-default.navbar-fixed-top role="navigation" ...@@ -18,12 +18,20 @@ nav.navbar.navbar-default.navbar-fixed-top role="navigation"
li class=('active' if current_page?(flow_path)) li class=('active' if current_page?(flow_path))
a href=flow_path title="Frow" a href=flow_path title="Frow"
| Flow | Flow
li class=('active' if current_page?(templates_path))
a href=templates_path title="Templates"
| Templates
ul.nav.navbar-nav.navbar-right ul.nav.navbar-nav.navbar-right
li li
a#notifications data-container="body" data-toggle="popover" data-placement="bottom"
span.glyphicon.glyphicon-flag
- if current_user.notifications.unread.any?
span.badge = current_user.notifications.unread.count
li
form form
a.btn.btn-primary.navbar-btn href=new_post_path a.btn.btn-primary.navbar-btn href=new_post_path
| Post&nbsp;&nbsp; | New Post&nbsp;&nbsp;
span.glyphicon.glyphicon-pencil span.glyphicon.glyphicon-pencil
li.dropdown li.dropdown
a.dropdown-toggle data-toggle="dropdown" a.dropdown-toggle data-toggle="dropdown"
...@@ -37,25 +45,30 @@ nav.navbar.navbar-default.navbar-fixed-top role="navigation" ...@@ -37,25 +45,30 @@ nav.navbar.navbar-default.navbar-fixed-top role="navigation"
| 下書き | 下書き
span.badge.pull-right = current_user.decorate.draft_count span.badge.pull-right = current_user.decorate.draft_count
li li
a href=edit_me_path マイページ a href=edit_user_path マイページ
li
a href=watching_path Watchings
li.divider li.divider
li li
a href=me_session_path data-method="delete" rel="nofollow" SignOut a href=destroy_user_session_path data-method="delete" rel="nofollow" SignOut
li.dropdown
a.dropdown-toggle data-toggle="dropdown" script#notification-content type="text/template"
| 通知
- if current_user.notifications.unread.any?
span.badge = current_user.notifications.unread.count
b.caret
ul.dropdown-menu
- if current_user.notifications.unread.any? - if current_user.notifications.unread.any?
h4 通知一覧
.list-group
- current_user.notifications.unread.each do |notification| - current_user.notifications.unread.each do |notification|
li a.list-group-item href=notification_bridge_path(notification.id)
a href=notification_bridge_path(notification.id) spen.small
| [#{notification.created_at.strftime('%m/%d %H:%M')}]&nbsp;
= notification.body = notification.body
- else - else
li h4 通知はありません
a 通知はありません
- content_for :footer_js do
javascript:
$('#notifications').popover({
html: true,
content: $('#notification-content').html()
});
...@@ -67,7 +67,7 @@ input#fileupload data-url="/apis/file_receiver" multiple="" name="files[]" style ...@@ -67,7 +67,7 @@ input#fileupload data-url="/apis/file_receiver" multiple="" name="files[]" style
- content_for :footer_js do - content_for :footer_js do
javascript: javascript:
// $.setConfirmUnload(); $.setConfirmUnload();
$('#post-form').mod_mdEditor({end_point: '/apis/markdown_preview'}); $('#post-form').mod_mdEditor({end_point: '/apis/markdown_preview'});
......
...@@ -8,8 +8,7 @@ a.list-group-item.post-list.mod-hover-hidden data-post-id=post.id href=post_path ...@@ -8,8 +8,7 @@ a.list-group-item.post-list.mod-hover-hidden data-post-id=post.id href=post_path
h4.text-link #{post.title} h4.text-link #{post.title}
.col-xs-3 .col-xs-3
span.label.label-danger = post.display_specified_date if post.specified_date span.label.label-danger = post.display_specified_date if post.specified_date
small.pull-right small.pull-right ##{post.id}
##{post.id}
.row .row
.col-xs-8 .col-xs-8
small.text-success small.text-success
...@@ -21,14 +20,15 @@ a.list-group-item.post-list.mod-hover-hidden data-post-id=post.id href=post_path ...@@ -21,14 +20,15 @@ a.list-group-item.post-list.mod-hover-hidden data-post-id=post.id href=post_path
| &nbsp; | &nbsp;
.col-xs-4 .col-xs-4
small.pull-right small.pull-right
span.glyphicon.glyphicon-time span.glyphicon.glyphicon-time title="読了時間"
span.mod-hover-hidden-item
| 読了時間
| &nbsp;#{post.read_time}&nbsp;&nbsp; | &nbsp;#{post.read_time}&nbsp;&nbsp;
span.glyphicon.glyphicon-comment
span.mod-hover-hidden-item span.glyphicon.glyphicon-comment title="コメント"
| コメント
| &nbsp;#{post.comments.count}&nbsp;&nbsp; | &nbsp;#{post.comments.count}&nbsp;&nbsp;
span.glyphicon.glyphicon-eye-open title="閲覧者数"
| &nbsp;#{post.visited_user_count}&nbsp;&nbsp;
.row .row
.col-xs-12 .col-xs-12
small.text-shadow small.text-shadow
......
= render_breadcrumbs
.row .row
.panel.panel-default .col-xs-9
.panel.panel-info
.panel-heading .panel-heading
h3.panel-title h3.panel-title
a href=post_path(@post) = @post.title a href=post_path(@post) = @post.title
ul.list-group .panel-body.viewer.github
li.list-group-item = MarkdownRenderer.new(@post.body).render
- @post.tags.each do |tag|
span.label.label-success .col-xs-3
a href=tag.decorate.show_path .btn-group
| ##{tag.name}
| &nbsp;
span.label.label-info
a href=(search_path(q: "@#{@post.author.name}"))
| @#{@post.author.name}
| &nbsp;
span.label.label-danger
a href=(search_path(q: "date:#{@post.display_date}")) = @post.display_date
.btn-group.pull-right style=("margin: -7px -12px 0 0;")
a.btn.btn-primary href=edit_post_path(@post) a.btn.btn-primary href=edit_post_path(@post)
| 編集&nbsp;
span.glyphicon.glyphicon-pencil span.glyphicon.glyphicon-pencil
button.btn.btn-default.dropdown-toggle data-toggle="dropdown" type="button" button.btn.btn-default.dropdown-toggle data-toggle="dropdown" type="button"
span.caret span.caret
...@@ -35,8 +27,51 @@ ...@@ -35,8 +27,51 @@
a data-target="#myModal" data-toggle="modal" href="#" Mail to... a data-target="#myModal" data-toggle="modal" href="#" Mail to...
li.divider li.divider
li= link_to 'Delete', post_path(@post), method: :delete, data: { confirm: 'Are you sure?' } li= link_to 'Delete', post_path(@post), method: :delete, data: { confirm: 'Are you sure?' }
.panel-body.viewer.github
= h_application_format_markdown(@post.body) | &nbsp;
.btn-group
- if current_user.watching?(post: @post)
= link_to 'Watching <span class="glyphicon glyphicon-eye-open"></span>'.html_safe, watch_post_path, :remote => true, :'data-type' => :html, :class => 'btn btn-warning ajax_link'
- else
= link_to 'Watch <span class="glyphicon glyphicon-eye-open"></span>'.html_safe, watch_post_path, :remote => true, :'data-type' => :html, :class => 'btn btn-default ajax_link'
.well style="margin-top:20px"
dl
dt 作成者
dd
a href=(search_path(q: "@#{@post.author.name}"))
| @#{@post.author.name}
dt タグ
dd
- @post.tags.each do |tag|
span.label.label-success
a href=tag.decorate.show_path
| ##{tag.name}
| &nbsp;
dt 指定日
dd
a href=(search_path(q: "date:#{@post.display_date}")) = @post.display_date
dt 作成日
dd
= @post.created_date
dt 最終更新日
dd
= @post.updated_date
dt 閲覧者数
dd
= @post.visited_user_count
dt コメント数
dd
= @post.comments.count
.row
.panel.panel-success .panel.panel-success
.panel-heading .panel-heading
h3.panel-title Comments h3.panel-title Comments
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
|#{@tag.name}」とは |#{@tag.name}」とは
.panel-body .panel-body
- if @tag.body.present? - if @tag.body.present?
= h_application_format_markdown(@tag.body) = MarkdownRenderer.new(@tag.body).render
- else - else
| まだ「#{@tag.name}」の情報がありません。 | まだ「#{@tag.name}」の情報がありません。
.col-xs-4 .col-xs-4
......
/ locals:
/ post {Post}
.list-group-item.post-list.mod-hover-hidden
.container-fluid
.row
.col-xs-9
h4.text-link #{post.title}
.col-xs-3
small.pull-right ##{post.id}
.row
.col-xs-8
p.small.text-success
| #{post.author.name} posted&nbsp;
abbr.js-time-ago data-time-ago-at=post.updated_at
|.&nbsp;&nbsp;
- post.tags.each do |tag|
span.label.label-success ##{tag.name}
| &nbsp;
.col-xs-4
span.pull-right.label.label-danger = post.display_specified_date if post.specified_date
.btn-group.mod-hover-hidden
a.btn.btn-info.mod-hover-hidden-item href=fork_post_path(post)
| このテンプレートを利用する
a.btn.btn-default.mod-hover-hidden-item href=edit_post_path(post)
| このテンプレートを編集する
/! view:templates/show
.row
h1
| Templates
small - テンプレートから作成
.panel.panel-info
.panel-heading
span.glyphicon.glyphicon-info-sign
| テンプレートの使い方
.panel-body
ul
li 「template」 というタグをつけた投稿はこのページに現れます
li コピーすることで同じフォーマットの文章が簡単に書けます
.list-group
- if template_tag = Tag.find_by(name: 'template')
- template_tag.posts.decorate.each do |_post|
= render partial: 'post', locals: { post: _post }
- else
| テンプレートが存在しません
#post-form #post-form
= form_for(@me, url: me_path) do |f| = form_for(@user, url: user_path) do |f|
- if @me.errors.any? - if @user.errors.any?
#error_explanation #error_explanation
h2 h2
= pluralize(@me.errors.count, "error") = pluralize(@user.errors.count, "error")
| prohibited this post from being saved: | prohibited this post from being saved:
ul ul
- @me.errors.full_messages.each do |msg| - @user.errors.full_messages.each do |msg|
li= msg li= msg
.row .row
......
/! view:flow/show
.row
h1
| Watchings
small - ウォッチ中の項目
.col-xs-8 role="navigation"
.list-group
- @posts.each do |_post|
= render partial: 'posts/large_item', locals: { post: _post }
= paginate(@posts)
<CORSConfiguration>
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>*</AllowedHeader>
<ExposeHeader>Accept-Ranges</ExposeHeader>
<ExposeHeader>Content-Encoding</ExposeHeader>
<ExposeHeader>Content-Length </ExposeHeader>
<ExposeHeader>Content-Range</ExposeHeader>
</CORSRule>
</CORSConfiguration>
Teaspoon.setup do |config|
# This determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to
# http://localhost:3000/jasmine to run your specs.
config.mount_at = "/teaspoon"
# This defaults to Rails.root if left nil. If you're testing an engine using a dummy application it can be useful to
# set this to your engines root.. E.g. `Teaspoon::Engine.root`
config.root = nil
# These paths are appended to the Rails assets paths (relative to config.root), and by default is an array that you
# can replace or add to.
config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"]
# Fixtures are rendered through a standard controller. This means you can use things like HAML or RABL/JBuilder, etc.
# to generate fixtures within this path.
config.fixture_path = "spec/javascripts/fixtures"
# You can modify the default suite configuration and create new suites here. Suites can be isolated from one another.
# When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can
# omit various directives and the defaults will be used.
#
# To run a specific suite
# - in the browser: http://localhost/teaspoon/[suite_name]
# - from the command line: rake teaspoon suite=[suite_name]
config.suite do |suite|
# You can specify a file matcher and all matching files will be loaded when the suite is run. It's important that
# these files are serve-able from sprockets.
#
# Note: Can also be set to nil.
suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}"
# Each suite can load a different helper, which can in turn require additional files. This file is loaded before
# your specs are loaded, and can be used as a manifest.
suite.helper = "spec_helper"
# These are the core Teaspoon javascripts. It's strongly encouraged to include only the base files here. You can
# require other support libraries in your spec helper, which allows you to change them without having to restart the
# server.
#
# Available frameworks: teaspoon-jasmine, teaspoon-mocha, teaspoon-qunit
#
# Note: To use the CoffeeScript source files use `"teaspoon/jasmine"` etc.
suite.javascripts = ["teaspoon-jasmine"]
# If you want to change how Teaspoon looks, or include your own stylesheets you can do that here. The default is the
# stylesheet for the HTML reporter.
suite.stylesheets = ["teaspoon"]
# When running coverage reports, you probably want to exclude libraries that you're not testing.
# Accepts an array of filenames or regular expressions. The default is to exclude assets from vendors or gems.
suite.no_coverage = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}]
# suite.no_coverage << "jquery.min.js" # excludes jquery from coverage reports
end
# Example suite. Since we're just filtering to files already within the root spec/javascripts, these files will also
# be run in the default suite -- but can be focused into a more specific suite.
#config.suite :targeted do |suite|
# suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}"
#end
end if defined?(Teaspoon) && Teaspoon.respond_to?(:setup) # let Teaspoon be undefined outside of development/test/asset groups
...@@ -11,11 +11,14 @@ Rendezvous::Application.routes.draw do ...@@ -11,11 +11,14 @@ Rendezvous::Application.routes.draw do
get 'stock' => 'stock#show', as: 'stock' get 'stock' => 'stock#show', as: 'stock'
get 'flow' => 'flow#show', as: 'flow' get 'flow' => 'flow#show', as: 'flow'
get 'search' => 'search#show', as: 'search' get 'search' => 'search#show', as: 'search'
get 'templates' => 'templates#show', as: 'templates'
get 'watchings' => 'watchings#show', as: 'watching'
get 'posts/:id/fork' => 'posts#fork', as: 'fork_post' get 'posts/:id/fork' => 'posts#fork', as: 'fork_post'
post 'posts/:id/mail' => 'posts#mail', as: 'mail_post' post 'posts/:id/mail' => 'posts#mail', as: 'mail_post'
post 'posts/:id/comment' => 'posts#comment', as: 'comment_post' post 'posts/:id/comment' => 'posts#comment', as: 'comment_post'
get 'posts/:id/slideshow' => 'posts#slideshow', as: 'slideshow_post' get 'posts/:id/slideshow' => 'posts#slideshow', as: 'slideshow_post'
get 'posts/:id/watch' => 'posts#watch', as: 'watch_post'
resources :posts, except: [:index] resources :posts, except: [:index]
get 'notification_bridge/:id' => 'notifications#bridge', as: 'notification_bridge' get 'notification_bridge/:id' => 'notifications#bridge', as: 'notification_bridge'
...@@ -25,9 +28,16 @@ Rendezvous::Application.routes.draw do ...@@ -25,9 +28,16 @@ Rendezvous::Application.routes.draw do
resources :tags, :param => :name, except: [:index] resources :tags, :param => :name, except: [:index]
devise_for :users, devise_for :users,
path_names: { current_user: 'me' }, controllers: {
controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }, omniauth_callbacks: 'users/omniauth_callbacks',
skip: [:passwords] registrations: 'users/registrations'
},
skip: [
:passwords,
:registrations,
]
resource :user
# The priority is based upon order of creation: first created -> highest priority. # The priority is based upon order of creation: first created -> highest priority.
......
...@@ -4,10 +4,16 @@ ...@@ -4,10 +4,16 @@
defaults: &defaults defaults: &defaults
rendezvous: rendezvous:
# For amil body
app_host: "http://rendezvous.dev" app_host: "http://rendezvous.dev"
# For login
google_api: google_api:
client_id: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com" client_id: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
secret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" secret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# For file upload
s3: s3:
access_key_id: "xxxxxxxxxxxxxxxxxxxxx" access_key_id: "xxxxxxxxxxxxxxxxxxxxx"
secret_access_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" secret_access_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
......
class CreateFootprints < ActiveRecord::Migration
def change
create_table :footprints do |t|
t.integer :user_id, null: false
t.integer :post_id, null: false
t.timestamps
end
add_index :footprints, [:user_id, :post_id]
add_index :footprints, :post_id
end
end
class CreateWatches < ActiveRecord::Migration
def change
create_table :watches do |t|
t.integer :watcher_id, null: false
t.string :watchable_type, null: false
t.integer :watchable_id, null: false
t.timestamps
end
add_index :watches, [:watcher_id, :watchable_type, :watchable_id], unique: true
add_index :watches, [:watchable_type, :watchable_id]
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20140501045300) do ActiveRecord::Schema.define(version: 20140719145016) do
create_table "comments", force: true do |t| create_table "comments", force: true do |t|
t.integer "author_id" t.integer "author_id"
...@@ -24,6 +24,16 @@ ActiveRecord::Schema.define(version: 20140501045300) do ...@@ -24,6 +24,16 @@ ActiveRecord::Schema.define(version: 20140501045300) do
add_index "comments", ["author_id", "updated_at"], name: "index_comments_on_author_id_and_updated_at", using: :btree add_index "comments", ["author_id", "updated_at"], name: "index_comments_on_author_id_and_updated_at", using: :btree
add_index "comments", ["post_id", "updated_at"], name: "index_comments_on_post_id_and_updated_at", using: :btree add_index "comments", ["post_id", "updated_at"], name: "index_comments_on_post_id_and_updated_at", using: :btree
create_table "footprints", force: true do |t|
t.integer "user_id", null: false
t.integer "post_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "footprints", ["post_id"], name: "index_footprints_on_post_id", using: :btree
add_index "footprints", ["user_id", "post_id"], name: "index_footprints_on_user_id_and_post_id", using: :btree
create_table "notifications", force: true do |t| create_table "notifications", force: true do |t|
t.integer "user_id" t.integer "user_id"
t.datetime "read_at" t.datetime "read_at"
...@@ -105,4 +115,15 @@ ActiveRecord::Schema.define(version: 20140501045300) do ...@@ -105,4 +115,15 @@ ActiveRecord::Schema.define(version: 20140501045300) do
add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id", using: :btree add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id", using: :btree
create_table "watches", force: true do |t|
t.integer "watcher_id", null: false
t.string "watchable_type", null: false
t.integer "watchable_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "watches", ["watchable_type", "watchable_id"], name: "index_watches_on_watchable_type_and_watchable_id", using: :btree
add_index "watches", ["watcher_id", "watchable_type", "watchable_id"], name: "index_watches_on_watcher_id_and_watchable_type_and_watchable_id", unique: true, using: :btree
end end
/**
* Copyright (C) 2013-2014 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* This file is part of WebODF.
*
* WebODF is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License (GNU AGPL)
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* WebODF is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
* @licend
*
* @source: http://www.webodf.org/
* @source: https://github.com/kogmbh/WebODF/
*/
@namespace cursor url(urn:webodf:names:cursor);
.caret {
opacity: 0 !important;
}
/**
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* This file is part of WebODF.
*
* WebODF is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License (GNU AGPL)
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* WebODF is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
* @licend
*
* @source: http://www.webodf.org/
* @source: https://github.com/kogmbh/WebODF/
*/
/*global runtime, document, odf, gui, console, webodf*/
function ODFViewerPlugin() {
"use strict";
function init(callback) {
var lib = document.createElement('script'),
pluginCSS;
lib.async = false;
lib.src = './webodf.js';
lib.type = 'text/javascript';
lib.onload = function () {
runtime.loadClass('gui.HyperlinkClickHandler');
runtime.loadClass('odf.OdfCanvas');
runtime.loadClass('ops.Session');
runtime.loadClass('gui.CaretManager');
runtime.loadClass("gui.HyperlinkTooltipView");
runtime.loadClass('gui.SessionController');
runtime.loadClass('gui.SvgSelectionView');
runtime.loadClass('gui.SelectionViewManager');
runtime.loadClass('gui.ShadowCursor');
runtime.loadClass('gui.SessionView');
callback();
};
document.getElementsByTagName('head')[0].appendChild(lib);
pluginCSS = document.createElement('link');
pluginCSS.setAttribute("rel", "stylesheet");
pluginCSS.setAttribute("type", "text/css");
pluginCSS.setAttribute("href", "./ODFViewerPlugin.css");
document.head.appendChild(pluginCSS);
}
// that should probably be provided by webodf
function nsResolver(prefix) {
var ns = {
'draw' : "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
'presentation' : "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",
'text' : "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
'office' : "urn:oasis:names:tc:opendocument:xmlns:office:1.0"
};
return ns[prefix] || console.log('prefix [' + prefix + '] unknown.');
}
var self = this,
pluginName = "WebODF",
pluginURL = "http://webodf.org",
odfCanvas = null,
odfElement = null,
initialized = false,
root = null,
documentType = null,
pages = [],
currentPage = null;
this.initialize = function (viewerElement, documentUrl) {
// If the URL has a fragment (#...), try to load the file it represents
init(function () {
var session,
sessionController,
sessionView,
odtDocument,
shadowCursor,
selectionViewManager,
caretManager,
localMemberId = 'localuser',
hyperlinkTooltipView,
eventManager;
odfElement = document.getElementById('canvas');
odfCanvas = new odf.OdfCanvas(odfElement);
odfCanvas.load(documentUrl);
odfCanvas.addListener('statereadychange', function () {
root = odfCanvas.odfContainer().rootElement;
initialized = true;
documentType = odfCanvas.odfContainer().getDocumentType(root);
if (documentType === 'text') {
odfCanvas.enableAnnotations(true, false);
session = new ops.Session(odfCanvas);
odtDocument = session.getOdtDocument();
shadowCursor = new gui.ShadowCursor(odtDocument);
sessionController = new gui.SessionController(session, localMemberId, shadowCursor, {});
eventManager = sessionController.getEventManager();
caretManager = new gui.CaretManager(sessionController);
selectionViewManager = new gui.SelectionViewManager(gui.SvgSelectionView);
sessionView = new gui.SessionView({
caretAvatarsInitiallyVisible: false
}, localMemberId, session, sessionController.getSessionConstraints(), caretManager, selectionViewManager);
selectionViewManager.registerCursor(shadowCursor);
hyperlinkTooltipView = new gui.HyperlinkTooltipView(odfCanvas,
sessionController.getHyperlinkClickHandler().getModifier);
eventManager.subscribe("mousemove", hyperlinkTooltipView.showTooltip);
eventManager.subscribe("mouseout", hyperlinkTooltipView.hideTooltip);
var op = new ops.OpAddMember();
op.init({
memberid: localMemberId,
setProperties: {
fillName: runtime.tr("Unknown Author"),
color: "blue"
}
});
session.enqueue([op]);
sessionController.insertLocalCursor();
}
self.onLoad();
});
});
};
this.isSlideshow = function () {
return documentType === 'presentation';
};
this.onLoad = function () {};
this.getWidth = function () {
return odfElement.clientWidth;
};
this.getHeight = function () {
return odfElement.clientHeight;
};
this.fitToWidth = function (width) {
odfCanvas.fitToWidth(width);
};
this.fitToHeight = function (height) {
odfCanvas.fitToHeight(height);
};
this.fitToPage = function (width, height) {
odfCanvas.fitToContainingElement(width, height);
};
this.fitSmart = function (width) {
odfCanvas.fitSmart(width);
};
this.getZoomLevel = function () {
return odfCanvas.getZoomLevel();
};
this.setZoomLevel = function (value) {
odfCanvas.setZoomLevel(value);
};
// return a list of tuples (pagename, pagenode)
this.getPages = function () {
var pageNodes = Array.prototype.slice.call(root.getElementsByTagNameNS(nsResolver('draw'), 'page')),
pages = [],
i,
tuple;
for (i = 0; i < pageNodes.length; i += 1) {
tuple = [
pageNodes[i].getAttribute('draw:name'),
pageNodes[i]
];
pages.push(tuple);
}
return pages;
};
this.showPage = function (n) {
odfCanvas.showPage(n);
};
this.getPluginName = function () {
return pluginName;
};
this.getPluginVersion = function () {
var version;
if (String(typeof webodf) !== "undefined") {
version = webodf.Version;
} else {
version = "Unknown";
}
return version;
};
this.getPluginURL = function () {
return pluginURL;
};
}
.page {
margin: 7px auto 7px auto;
position: relative;
overflow: hidden;
background-clip: content-box;
background-color: white;
box-shadow: 0px 0px 7px rgba(0, 0, 0, 0.75);
-webkit-box-shadow: 0px 0px 7px rgba(0, 0, 0, 0.75);
-moz-box-shadow: 0px 0px 7px rgba(0, 0, 0, 0.75);
-ms-box-shadow: 0px 0px 7px rgba(0, 0, 0, 0.75);
-o-box-shadow: 0px 0px 7px rgba(0, 0, 0, 0.75);
}
.textLayer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
color: #000;
font-family: sans-serif;
overflow: hidden;
}
.textLayer > div {
color: transparent;
position: absolute;
line-height: 1;
white-space: pre;
cursor: text;
}
::selection { background:rgba(0,0,255,0.3); }
::-moz-selection { background:rgba(0,0,255,0.3); }
/**
* @license
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* (GNU AGPL) as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. The code is distributed
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this code. If not, see <http://www.gnu.org/licenses/>.
*
* As additional permission under GNU AGPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* As a special exception to the AGPL, any HTML file which merely makes function
* calls to this code, and for that purpose includes it by reference shall be
* deemed a separate work for copyright law purposes. In addition, the copyright
* holders of this code give you permission to combine this code with free
* software libraries that are released under the GNU LGPL. You may copy and
* distribute such a system following the terms of the GNU AGPL for this code
* and the LGPL for the libraries. If you modify this code, you may extend this
* exception to your version of the code, but you are not obligated to do so.
* If you do not wish to do so, delete this exception statement from your
* version.
*
* This license applies to this entire compilation.
* @licend
* @source: http://viewerjs.org/
* @source: http://github.com/kogmbh/ViewerJS
*/
/*global document, window, Viewer, ODFViewerPlugin, PDFViewerPlugin*/
var viewer;
function loadPlugin(pluginName, callback) {
"use strict";
var script, style;
// Load script
script = document.createElement('script');
script.async = false;
script.onload = callback;
script.src = pluginName + '.js';
script.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(script);
}
function loadDocument(documentUrl) {
"use strict";
if (documentUrl) {
var extension = documentUrl.split('.').pop(),
Plugin;
extension = extension.toLowerCase();
switch (extension) {
case 'odt':
case 'odp':
case 'ods':
case 'fodt':
loadPlugin('./ODFViewerPlugin', function () {
Plugin = ODFViewerPlugin;
});
break;
case 'pdf':
loadPlugin('./PDFViewerPlugin', function () {
Plugin = PDFViewerPlugin;
});
break;
}
window.onload = function () {
if (Plugin) {
viewer = new Viewer(new Plugin());
}
};
}
}
/* This is just a sample file with CSS rules. You should write your own @font-face declarations
* to add support for your desired fonts.
*/
@font-face {
font-family: 'Novecentowide Book';
src: url("/ViewerJS/fonts/Novecentowide-Bold-webfont.eot");
src: url("/ViewerJS/fonts/Novecentowide-Bold-webfont.eot?#iefix") format("embedded-opentype"),
url("/ViewerJS/fonts/Novecentowide-Bold-webfont.woff") format("woff"),
url("/fonts/Novecentowide-Bold-webfont.ttf") format("truetype"),
url("/fonts/Novecentowide-Bold-webfont.svg#NovecentowideBookBold") format("svg");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'exotica';
src: url('/ViewerJS/fonts/Exotica-webfont.eot');
src: url('/ViewerJS/fonts/Exotica-webfont.eot?#iefix') format('embedded-opentype'),
url('/ViewerJS/fonts/Exotica-webfont.woff') format('woff'),
url('/ViewerJS/fonts/Exotica-webfont.ttf') format('truetype'),
url('/ViewerJS/fonts/Exotica-webfont.svg#exoticamedium') format('svg');
font-weight: normal;
font-style: normal;
}
<!DOCTYPE html>
<!--
Copyright (C) 2012-2014 KO GmbH <copyright@kogmbh.com>
@licstart
This file is part of WebODF.
WebODF is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License (GNU AGPL)
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
WebODF is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with WebODF. If not, see <http://www.gnu.org/licenses/>.
@licend
@source: http://www.webodf.org/
@source: https://github.com/kogmbh/WebODF/
-->
<!--
This file is a derivative from a part of Mozilla's PDF.js project. The
original license header follows.
-->
<!--
Copyright 2012 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html dir="ltr" lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>ViewerJS</title>
<!-- If you want to use custom CSS (@font-face rules, for example) you should uncomment
the following reference and use a local.css file for that. See the example.local.css
file for a sample.
<link rel="stylesheet" type="text/css" href="local.css" media="screen"/>
-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" type="text/css" href="viewer.css" media="screen"/>
<script src="viewer.js" type="text/javascript" charset="utf-8"></script>
<script src="PluginLoader.js" type="text/javascript" charset="utf-8"></script>
<script>
loadDocument(window.location.hash);
</script>
</head>
<body>
<div id = "viewer">
<div id = "titlebar">
<div id = "documentName"></div>
<div id = "toolbarRight">
<button id = "presentation" class = "toolbarButton presentation" title = "Presentation"></button>
<button id = "fullscreen" class = "toolbarButton fullscreen" title = "Fullscreen"></button>
<button id = "download" class = "toolbarButton download" title = "Download"></button>
</div>
</div>
<div id = "toolbarContainer">
<div id = "toolbar">
<div id = "toolbarLeft">
<div id = "navButtons" class = "splitToolbarButton">
<button id = "previous" class = "toolbarButton pageUp" title = "Previous Page"></button>
<div class="splitToolbarButtonSeparator"></div>
<button id = "next" class = "toolbarButton pageDown" title = "Next Page"></button>
</div>
<label id = "pageNumberLabel" class = "toolbarLabel" for = "pageNumber">Page:</label>
<input type = "number" id = "pageNumber" class = "toolbarField pageNumber"/>
<span id = "numPages" class = "toolbarLabel"></span>
</div>
<div id = "toolbarMiddleContainer" class = "outerCenter">
<div id = "toolbarMiddle" class = "innerCenter">
<div id = 'zoomButtons' class = "splitToolbarButton">
<button id = "zoomOut" class = "toolbarButton zoomOut" title = "Zoom Out"></button>
<div class="splitToolbarButtonSeparator"></div>
<button id = "zoomIn" class = "toolbarButton zoomIn" title = "Zoom In"></button>
</div>
<span id="scaleSelectContainer" class="dropdownToolbarButton">
<select id="scaleSelect" title="Zoom" oncontextmenu="return false;">
<option id="pageAutoOption" value="auto" selected>Automatic</option>
<option id="pageActualOption" value="page-actual">Actual Size</option>
<option id="pageWidthOption" value="page-width">Full Width</option>
<option id="customScaleOption" value="custom"> </option>
<option value="0.5">50%</option>
<option value="0.75">75%</option>
<option value="1">100%</option>
<option value="1.25">125%</option>
<option value="1.5">150%</option>
<option value="2">200%</option>
</select>
</span>
<div id = "sliderContainer">
<div id = "slider"></div>
</div>
</div>
</div>
</div>
</div>
<div id = "canvasContainer">
<div id = "canvas"></div>
</div>
<div id = "overlayNavigator">
<div id = "previousPage"></div>
<div id = "nextPage"></div>
</div>
<div id = "overlayCloseButton">
&#10006;
</div>
<div id = "dialogOverlay"></div>
</div>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
/* globals PDFFindController, FindStates, mozL10n */
/**
* Creates a "search bar" given set of DOM elements
* that act as controls for searching, or for setting
* search preferences in the UI. This object also sets
* up the appropriate events for the controls. Actual
* searching is done by PDFFindController
*/
var PDFFindBar = {
opened: false,
bar: null,
toggleButton: null,
findField: null,
highlightAll: null,
caseSensitive: null,
findMsg: null,
findStatusIcon: null,
findPreviousButton: null,
findNextButton: null,
initialize: function(options) {
if(typeof PDFFindController === 'undefined' || PDFFindController === null) {
throw 'PDFFindBar cannot be initialized ' +
'without a PDFFindController instance.';
}
this.bar = options.bar;
this.toggleButton = options.toggleButton;
this.findField = options.findField;
this.highlightAll = options.highlightAllCheckbox;
this.caseSensitive = options.caseSensitiveCheckbox;
this.findMsg = options.findMsg;
this.findStatusIcon = options.findStatusIcon;
this.findPreviousButton = options.findPreviousButton;
this.findNextButton = options.findNextButton;
var self = this;
this.toggleButton.addEventListener('click', function() {
self.toggle();
});
this.findField.addEventListener('input', function() {
self.dispatchEvent('');
});
this.bar.addEventListener('keydown', function(evt) {
switch (evt.keyCode) {
case 13: // Enter
if (evt.target === self.findField) {
self.dispatchEvent('again', evt.shiftKey);
}
break;
case 27: // Escape
self.close();
break;
}
});
this.findPreviousButton.addEventListener('click',
function() { self.dispatchEvent('again', true); }
);
this.findNextButton.addEventListener('click', function() {
self.dispatchEvent('again', false);
});
this.highlightAll.addEventListener('click', function() {
self.dispatchEvent('highlightallchange');
});
this.caseSensitive.addEventListener('click', function() {
self.dispatchEvent('casesensitivitychange');
});
},
dispatchEvent: function(aType, aFindPrevious) {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('find' + aType, true, true, {
query: this.findField.value,
caseSensitive: this.caseSensitive.checked,
highlightAll: this.highlightAll.checked,
findPrevious: aFindPrevious
});
return window.dispatchEvent(event);
},
updateUIState: function(state, previous) {
var notFound = false;
var findMsg = '';
var status = '';
switch (state) {
case FindStates.FIND_FOUND:
break;
case FindStates.FIND_PENDING:
status = 'pending';
break;
case FindStates.FIND_NOTFOUND:
findMsg = mozL10n.get('find_not_found', null, 'Phrase not found');
notFound = true;
break;
case FindStates.FIND_WRAPPED:
if (previous) {
findMsg = mozL10n.get('find_reached_top', null,
'Reached top of document, continued from bottom');
} else {
findMsg = mozL10n.get('find_reached_bottom', null,
'Reached end of document, continued from top');
}
break;
}
if (notFound) {
this.findField.classList.add('notFound');
} else {
this.findField.classList.remove('notFound');
}
this.findField.setAttribute('data-status', status);
this.findMsg.textContent = findMsg;
},
open: function() {
if (!this.opened) {
this.opened = true;
this.toggleButton.classList.add('toggled');
this.bar.classList.remove('hidden');
}
this.findField.select();
this.findField.focus();
},
close: function() {
if (!this.opened) return;
this.opened = false;
this.toggleButton.classList.remove('toggled');
this.bar.classList.add('hidden');
PDFFindController.active = false;
},
toggle: function() {
if (this.opened) {
this.close();
} else {
this.open();
}
}
};
var /**@const{!string}*/pdfjs_version = "d45d7bc";
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
// optimised CSS custom property getter/setter
var CustomStyle = (function CustomStyleClosure() {
// As noted on: http://www.zachstronaut.com/posts/2009/02/17/
// animate-css-transforms-firefox-webkit.html
// in some versions of IE9 it is critical that ms appear in this list
// before Moz
var prefixes = ['ms', 'Moz', 'Webkit', 'O'];
var _cache = { };
function CustomStyle() {
}
CustomStyle.getProp = function get(propName, element) {
// check cache only when no element is given
if (arguments.length == 1 && typeof _cache[propName] == 'string') {
return _cache[propName];
}
element = element || document.documentElement;
var style = element.style, prefixed, uPropName;
// test standard property first
if (typeof style[propName] == 'string') {
return (_cache[propName] = propName);
}
// capitalize
uPropName = propName.charAt(0).toUpperCase() + propName.slice(1);
// test vendor specific properties
for (var i = 0, l = prefixes.length; i < l; i++) {
prefixed = prefixes[i] + uPropName;
if (typeof style[prefixed] == 'string') {
return (_cache[propName] = prefixed);
}
}
//if all fails then set to undefined
return (_cache[propName] = 'undefined');
};
CustomStyle.setProp = function set(propName, element, str) {
var prop = this.getProp(propName);
if (prop != 'undefined')
element.style[prop] = str;
};
return CustomStyle;
})();
function getFileName(url) {
var anchor = url.indexOf('#');
var query = url.indexOf('?');
var end = Math.min(
anchor > 0 ? anchor : url.length,
query > 0 ? query : url.length);
return url.substring(url.lastIndexOf('/', end) + 1, end);
}
/**
* Returns scale factor for the canvas. It makes sense for the HiDPI displays.
* @return {Object} The object with horizontal (sx) and vertical (sy)
scales. The scaled property is set to false if scaling is
not required, true otherwise.
*/
function getOutputScale(ctx) {
var devicePixelRatio = window.devicePixelRatio || 1;
var backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
var pixelRatio = devicePixelRatio / backingStoreRatio;
return {
sx: pixelRatio,
sy: pixelRatio,
scaled: pixelRatio != 1
};
}
/**
* Scrolls specified element into view of its parent.
* element {Object} The element to be visible.
* spot {Object} An object with optional top and left properties,
* specifying the offset from the top left edge.
*/
function scrollIntoView(element, spot) {
// Assuming offsetParent is available (it's not available when viewer is in
// hidden iframe or object). We have to scroll: if the offsetParent is not set
// producing the error. See also animationStartedClosure.
var parent = element.offsetParent;
var offsetY = element.offsetTop + element.clientTop;
var offsetX = element.offsetLeft + element.clientLeft;
if (!parent) {
console.error('offsetParent is not set -- cannot scroll');
return;
}
while (parent.clientHeight === parent.scrollHeight) {
if (parent.dataset._scaleY) {
offsetY /= parent.dataset._scaleY;
offsetX /= parent.dataset._scaleX;
}
offsetY += parent.offsetTop;
offsetX += parent.offsetLeft;
parent = parent.offsetParent;
if (!parent) {
return; // no need to scroll
}
}
if (spot) {
if (spot.top !== undefined) {
offsetY += spot.top;
}
if (spot.left !== undefined) {
offsetX += spot.left;
parent.scrollLeft = offsetX;
}
}
parent.scrollTop = offsetY;
}
/**
* Event handler to suppress context menu.
*/
function noContextMenuHandler(e) {
e.preventDefault();
}
/**
* Returns the filename or guessed filename from the url (see issue 3455).
* url {String} The original PDF location.
* @return {String} Guessed PDF file name.
*/
function getPDFFileNameFromURL(url) {
var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
// SCHEME HOST 1.PATH 2.QUERY 3.REF
// Pattern to get last matching NAME.pdf
var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
var splitURI = reURI.exec(url);
var suggestedFilename = reFilename.exec(splitURI[1]) ||
reFilename.exec(splitURI[2]) ||
reFilename.exec(splitURI[3]);
if (suggestedFilename) {
suggestedFilename = suggestedFilename[0];
if (suggestedFilename.indexOf('%') != -1) {
// URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf
try {
suggestedFilename =
reFilename.exec(decodeURIComponent(suggestedFilename))[0];
} catch(e) { // Possible (extremely rare) errors:
// URIError "Malformed URI", e.g. for "%AA.pdf"
// TypeError "null has no properties", e.g. for "%2F.pdf"
}
}
}
return suggestedFilename || 'document.pdf';
}
var ProgressBar = (function ProgressBarClosure() {
function clamp(v, min, max) {
return Math.min(Math.max(v, min), max);
}
function ProgressBar(id, opts) {
// Fetch the sub-elements for later.
this.div = document.querySelector(id + ' .progress');
// Get the loading bar element, so it can be resized to fit the viewer.
this.bar = this.div.parentNode;
// Get options, with sensible defaults.
this.height = opts.height || 100;
this.width = opts.width || 100;
this.units = opts.units || '%';
// Initialize heights.
this.div.style.height = this.height + this.units;
this.percent = 0;
}
ProgressBar.prototype = {
updateBar: function ProgressBar_updateBar() {
if (this._indeterminate) {
this.div.classList.add('indeterminate');
this.div.style.width = this.width + this.units;
return;
}
this.div.classList.remove('indeterminate');
var progressSize = this.width * this._percent / 100;
this.div.style.width = progressSize + this.units;
},
get percent() {
return this._percent;
},
set percent(val) {
this._indeterminate = isNaN(val);
this._percent = clamp(val, 0, 100);
this.updateBar();
},
setWidth: function ProgressBar_setWidth(viewer) {
if (viewer) {
var container = viewer.parentNode;
var scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
if (scrollbarWidth > 0) {
this.bar.setAttribute('style', 'width: calc(100% - ' +
scrollbarWidth + 'px);');
}
}
},
hide: function ProgressBar_hide() {
this.bar.classList.add('hidden');
this.bar.removeAttribute('style');
}
};
return ProgressBar;
})();
var Cache = function cacheCache(size) {
var data = [];
this.push = function cachePush(view) {
var i = data.indexOf(view);
if (i >= 0)
data.splice(i);
data.push(view);
if (data.length > size)
data.shift().destroy();
};
};
//#if !(FIREFOX || MOZCENTRAL || B2G)
var isLocalStorageEnabled = (function isLocalStorageEnabledClosure() {
// Feature test as per http://diveintohtml5.info/storage.html
// The additional localStorage call is to get around a FF quirk, see
// bug #495747 in bugzilla
try {
return ('localStorage' in window && window['localStorage'] !== null &&
localStorage);
} catch (e) {
return false;
}
})();
//#endif
function Viewer(c){function M(){var a,b,A,d,f;c&&(A=c.getPluginName(),d=c.getPluginVersion(),f=c.getPluginURL());a=document.createElement("div");a.id="aboutDialogCentererTable";b=document.createElement("div");b.id="aboutDialogCentererCell";n=document.createElement("div");n.id="aboutDialog";n.innerHTML='<h1>ViewerJS</h1><p>Open Source document viewer for webpages, built with HTML and JavaScript.</p><p>Learn more and get your own copy on the <a href="http://viewerjs.org/" target="_blank">ViewerJS website</a>.</p>'+
(c?'<p>Using the <a href = "'+f+'" target="_blank">'+A+'</a> (<span id = "pluginVersion">'+d+"</span>) plugin to show you this document.</p>":"")+'<p>Supported by <a href="http://nlnet.nl" target="_blank"><br><img src="images/nlnet.png" width="160" height="60" alt="NLnet Foundation"></a></p><p>Made by <a href="http://kogmbh.com" target="_blank"><br><img src="images/kogmbh.png" width="172" height="40" alt="KO GmbH"></a></p><button id = "aboutDialogCloseButton" class = "toolbarButton textButton">Close</button>';
u.appendChild(a);a.appendChild(b);b.appendChild(n);a=document.createElement("button");a.id="about";a.className="toolbarButton textButton about";a.title="About";a.innerHTML="ViewerJS";N.appendChild(a);a.addEventListener("click",function(){u.style.display="block"});document.getElementById("aboutDialogCloseButton").addEventListener("click",function(){u.style.display="none"})}function B(a){var b=O.options,c,d=!1,f;for(f=0;f<b.length;f+=1)c=b[f],c.value!==a?c.selected=!1:d=c.selected=!0;return d}function C(a,
c,d){a!==b.getZoomLevel()&&(b.setZoomLevel(a),d=document.createEvent("UIEvents"),d.initUIEvent("scalechange",!1,!1,window,0),d.scale=a,d.resetAutoSettings=c,window.dispatchEvent(d))}function D(){var a;if(c.onScroll)c.onScroll();c.getPageInView&&(a=c.getPageInView())&&(k=a,document.getElementById("pageNumber").value=a)}function E(a){window.clearTimeout(F);F=window.setTimeout(function(){D()},a)}function e(a,b,g){var e,f;if(e="custom"===a?parseFloat(document.getElementById("customScaleOption").textContent)/
100:parseFloat(a))C(e,!0,g);else{e=d.clientWidth-p;f=d.clientHeight-p;switch(a){case "page-actual":C(1,b,g);break;case "page-width":c.fitToWidth(e);break;case "page-height":c.fitToHeight(f);break;case "page-fit":c.fitToPage(e,f);break;case "auto":c.isSlideshow()?c.fitToPage(e+p,f+p):c.fitSmart(e)}B(a)}E(300)}function q(){l=!l;r&&!l&&b.togglePresentationMode()}function v(){s&&(w.className="viewer-touched",window.clearTimeout(G),G=window.setTimeout(function(){w.className=""},5E3))}function x(){h.classList.add("viewer-touched");
m.classList.add("viewer-touched");window.clearTimeout(H);H=window.setTimeout(function(){I()},5E3)}function I(){h.classList.remove("viewer-touched");m.classList.remove("viewer-touched")}function P(){h.classList.contains("viewer-touched")?I():x()}var b=this,p=40,r=!1,l=!1,J=!1,s=!1,y,g=document.getElementById("viewer"),d=document.getElementById("canvasContainer"),w=document.getElementById("overlayNavigator"),h=document.getElementById("titlebar"),m=document.getElementById("toolbarContainer"),K=document.getElementById("toolbarLeft"),
Q=document.getElementById("toolbarMiddleContainer"),O=document.getElementById("scaleSelect"),u=document.getElementById("dialogOverlay"),N=document.getElementById("toolbarRight"),n,L,t=[],k,F,G,H;this.initialize=function(){var a=String(document.location),z=a.indexOf("#"),a=a.substr(z+1);-1===z||0===a.length?console.log("Could not parse file path argument."):(y=a,L=y.replace(/^.*[\\\/]/,""),document.title=L,document.getElementById("documentName").innerHTML=document.title,c.onLoad=function(){document.getElementById("pluginVersion").innerHTML=
c.getPluginVersion();(s=c.isSlideshow())?(d.classList.add("slideshow"),K.style.visibility="visible"):(Q.style.visibility="visible",c.getPageInView&&(K.style.visibility="visible"));J=!0;t=c.getPages();document.getElementById("numPages").innerHTML="of "+t.length;b.showPage(1);e("auto");d.onscroll=D;E()},c.initialize(d,a))};this.showPage=function(a){0>=a?a=1:a>t.length&&(a=t.length);c.showPage(a);k=a;document.getElementById("pageNumber").value=k};this.showNextPage=function(){b.showPage(k+1)};this.showPreviousPage=
function(){b.showPage(k-1)};this.download=function(){var a=y.split("#")[0];window.open(a+"#viewer.action=download","_parent")};this.toggleFullScreen=function(){l?document.cancelFullScreen?document.cancelFullScreen():document.mozCancelFullScreen?document.mozCancelFullScreen():document.webkitCancelFullScreen?document.webkitCancelFullScreen():document.msExitFullscreen&&document.msExitFullscreen():g.requestFullScreen?g.requestFullScreen():g.mozRequestFullScreen?g.mozRequestFullScreen():g.webkitRequestFullScreen?
g.webkitRequestFullScreen():g.msRequestFullscreen&&g.msRequestFullscreen()};this.togglePresentationMode=function(){var a=document.getElementById("overlayCloseButton");r?(h.style.display=m.style.display="block",a.style.display="none",d.classList.remove("presentationMode"),d.onmouseup=function(){},d.oncontextmenu=function(){},d.onmousedown=function(){},e("auto"),s=c.isSlideshow()):(h.style.display=m.style.display="none",a.style.display="block",d.classList.add("presentationMode"),s=!0,d.onmousedown=
function(a){a.preventDefault()},d.oncontextmenu=function(a){a.preventDefault()},d.onmouseup=function(a){a.preventDefault();1===a.which?b.showNextPage():b.showPreviousPage()},e("page-fit"));r=!r};this.getZoomLevel=function(){return c.getZoomLevel()};this.setZoomLevel=function(a){c.setZoomLevel(a)};this.zoomOut=function(){var a=(b.getZoomLevel()/1.1).toFixed(2),a=Math.max(0.25,a);e(a,!0)};this.zoomIn=function(){var a=(1.1*b.getZoomLevel()).toFixed(2),a=Math.min(4,a);e(a,!0)};(function(){M();c&&(b.initialize(),
document.cancelFullScreen||document.mozCancelFullScreen||document.webkitCancelFullScreen||document.msExitFullscreen||(document.getElementById("fullscreen").style.visibility="hidden",document.getElementById("presentation").style.visibility="hidden"),document.getElementById("overlayCloseButton").addEventListener("click",b.toggleFullScreen),document.getElementById("fullscreen").addEventListener("click",b.toggleFullScreen),document.getElementById("presentation").addEventListener("click",function(){l||
b.toggleFullScreen();b.togglePresentationMode()}),document.addEventListener("fullscreenchange",q),document.addEventListener("webkitfullscreenchange",q),document.addEventListener("mozfullscreenchange",q),document.addEventListener("MSFullscreenChange",q),document.getElementById("download").addEventListener("click",function(){b.download()}),document.getElementById("zoomOut").addEventListener("click",function(){b.zoomOut()}),document.getElementById("zoomIn").addEventListener("click",function(){b.zoomIn()}),
document.getElementById("previous").addEventListener("click",function(){b.showPreviousPage()}),document.getElementById("next").addEventListener("click",function(){b.showNextPage()}),document.getElementById("previousPage").addEventListener("click",function(){b.showPreviousPage()}),document.getElementById("nextPage").addEventListener("click",function(){b.showNextPage()}),document.getElementById("pageNumber").addEventListener("change",function(){b.showPage(this.value)}),document.getElementById("scaleSelect").addEventListener("change",
function(){e(this.value)}),d.addEventListener("click",v),w.addEventListener("click",v),d.addEventListener("click",P),h.addEventListener("click",x),m.addEventListener("click",x),window.addEventListener("scalechange",function(a){var b=document.getElementById("customScaleOption"),c=B(String(a.scale));b.selected=!1;c||(b.textContent=Math.round(1E4*a.scale)/100+"%",b.selected=!0)},!0),window.addEventListener("resize",function(a){J&&(document.getElementById("pageWidthOption").selected||document.getElementById("pageAutoOption").selected)&&
e(document.getElementById("scaleSelect").value);v()}),window.addEventListener("keydown",function(a){var c=a.shiftKey;switch(a.keyCode){case 33:case 38:case 37:b.showPreviousPage();break;case 34:case 40:case 39:b.showNextPage();break;case 32:c?b.showPreviousPage():b.showNextPage()}}))})()};
This source diff could not be displayed because it is too large. You can view the blob instead.
require 'spec_helper' require 'rails_helper'
describe ApisController do describe ApisController, type: :controller do
describe "GET 'markdown_preview'" do describe "GET 'markdown_preview'" do
it "returns http redirect" do it "returns http redirect" do
...@@ -10,9 +10,9 @@ describe ApisController do ...@@ -10,9 +10,9 @@ describe ApisController do
end end
describe "GET 'markdown_preview'" do describe "GET 'markdown_preview'" do
login_user
it "returns http success" do it "returns http success" do
sign_in FactoryGirl.create(:alice)
get 'markdown_preview' get 'markdown_preview'
expect(response).to be_success expect(response).to be_success
end end
......
require 'spec_helper' require 'rails_helper'
describe ApplicationController do describe ApplicationController do
......
require 'spec_helper' require 'rails_helper'
class DummyClass; end class DummyClass; end
...@@ -6,7 +6,7 @@ describe RV::Mailer do ...@@ -6,7 +6,7 @@ describe RV::Mailer do
let(:klass) { DummyClass.new.extend(RV::Mailer) } let(:klass) { DummyClass.new.extend(RV::Mailer) }
let(:alice) { FactoryGirl.create(:alice) } let(:alice) { FactoryGirl.create(:alice) }
let(:post) { Post.create title: 'ruby rspec', body: 'This is first espec test: ruby' } let(:post) { Post.create title: 'ruby rspec', body: 'This is first espec test: ruby', author: create(:author) }
it 'valid' do it 'valid' do
expect { klass.compose_mail(post, user: alice, to: 'dummy@example.com') }.not_to raise_error expect { klass.compose_mail(post, user: alice, to: 'dummy@example.com') }.not_to raise_error
......
require 'spec_helper' require 'rails_helper'
describe FlowController do describe FlowController, type: :controller do
describe "GET 'show' without login" do describe "GET 'show' without login" do
it "returns http redirect" do it "returns http redirect" do
...@@ -10,9 +10,9 @@ describe FlowController do ...@@ -10,9 +10,9 @@ describe FlowController do
end end
describe "GET 'show' with login" do describe "GET 'show' with login" do
login_user
it "returns http success" do it "returns http success" do
sign_in FactoryGirl.create(:alice)
get 'show' get 'show'
expect(response).to be_success expect(response).to be_success
end end
......
require 'rails_helper'
describe PostsController, type: :controller do
let(:post) { FactoryGirl.create(:post) }
describe "GET 'show' without login" do
it "returns http redirect" do
get 'show', id: post.id
expect(response).to redirect_to('/')
end
end
describe "GET 'show' with login" do
let(:alice) { FactoryGirl.create(:alice) }
it "returns http success" do
sign_in FactoryGirl.create(:alice)
get 'show', id: post.id
expect(response).to be_success
end
it "returns http success" do
sign_in alice
get 'show', id: post.id
expect(Footprint.where(user_id: alice.id, post_id: post.id)).to exist
end
end
end
require 'spec_helper' require 'rails_helper'
describe SearchController do describe SearchController, type: :controller do
describe "GET 'show' without login" do describe "GET 'show' without login" do
it "returns http redirect" do it "returns http redirect" do
...@@ -10,9 +10,9 @@ describe SearchController do ...@@ -10,9 +10,9 @@ describe SearchController do
end end
describe "GET 'show' with login" do describe "GET 'show' with login" do
login_user
it "returns http success" do it "returns http success" do
sign_in FactoryGirl.create(:alice)
get 'show' get 'show'
expect(response).to be_success expect(response).to be_success
end end
......
require 'spec_helper' require 'rails_helper'
describe StockController do describe StockController, type: :controller do
describe "GET 'show' without login" do describe "GET 'show' without login" do
it "returns http redirect" do it "returns http redirect" do
...@@ -10,9 +10,9 @@ describe StockController do ...@@ -10,9 +10,9 @@ describe StockController do
end end
describe "GET 'show' with login" do describe "GET 'show' with login" do
login_user
it "returns http success" do it "returns http success" do
sign_in FactoryGirl.create(:alice)
get 'show' get 'show'
expect(response).to be_success expect(response).to be_success
end end
......
require 'spec_helper' require 'rails_helper'
describe TagsController do describe TagsController, type: :controller do
let(:tag) { FactoryGirl.create(:tag_ruby) } let(:tag) { FactoryGirl.create(:tag_ruby) }
...@@ -12,9 +12,9 @@ describe TagsController do ...@@ -12,9 +12,9 @@ describe TagsController do
end end
describe "GET 'show' with login" do describe "GET 'show' with login" do
login_user
it "returns http success" do it "returns http success" do
sign_in FactoryGirl.create(:alice)
get 'show', name: tag.name get 'show', name: tag.name
expect(response).to be_success expect(response).to be_success
end end
......
require 'rails_helper'
RSpec.describe TemplatesController, :type => :controller do
describe "GET 'show' without template" do
it "returns http success" do
sign_in FactoryGirl.create(:alice)
get 'show'
expect(response).to be_success
end
end
describe "GET 'show' with template" do
it "returns http success" do
sign_in FactoryGirl.create(:alice)
get 'show'
expect(response).to be_success
end
end
end
require 'spec_helper' require 'rails_helper'
describe MeController do describe UsersController, type: :controller do
before do
end
describe "GET 'edit'" do describe "GET 'edit'" do
login_user
it "returns http success" do it "returns http success" do
sign_in FactoryGirl.create(:alice)
get :edit get :edit
response.should be_success response.should be_success
end end
end end
describe "GET 'update'" do describe "GET 'update'" do
login_user
it "returns http success" do it "returns http success" do
sign_in FactoryGirl.create(:alice)
patch :update, user: { nickname: 'bob' } patch :update, user: { nickname: 'bob' }
response.should redirect_to('/me/edit') response.should redirect_to('/user/edit')
end end
end end
......
require 'rails_helper'
RSpec.describe WatchingsController, :type => :controller do
describe "GET 'show' without login" do
it "returns http redirect" do
get 'show'
expect(response).to redirect_to('/')
end
end
describe "GET 'show' with login" do
it "returns http success" do
sign_in FactoryGirl.create(:alice)
get 'show'
expect(response).to be_success
end
end
end
require 'spec_helper' require 'rails_helper'
describe WelcomeController do describe WelcomeController, type: :controller do
describe "GET 'top'" do describe "GET 'top'" do
it 'should be successful' do it 'should be successful' do
...@@ -12,22 +12,23 @@ describe WelcomeController do ...@@ -12,22 +12,23 @@ describe WelcomeController do
describe 'Login' do describe 'Login' do
before(:each) do let(:user) { FactoryGirl.create(:alice) }
@user = FactoryGirl.create(:login_user_1)
sign_in @user
end
describe "GET 'top'" do describe "GET 'top'" do
it 'should be successful' do it 'should be successful' do
sign_in FactoryGirl.create(:alice)
get :top get :top
expect(subject).to redirect_to controller: 'flow', expect(subject).to redirect_to controller: 'flow',
action: 'show' action: 'show'
end end
it 'should find the right user' do it 'should find the right user' do
sign_in user
get :top get :top
expect(assigns(:current_user)).to eq(@user) expect(assigns(:current_user)).to eq(user)
end end
end end
......
require 'spec_helper' require 'rails_helper'
describe ApisDecorator do describe ApisDecorator do
end end
require 'spec_helper' require 'rails_helper'
describe FlowDecorator do describe FlowDecorator do
end end
require 'spec_helper' require 'rails_helper'
describe SearchDecorator do describe SearchDecorator do
end end
require 'spec_helper' require 'rails_helper'
describe StockDecorator do describe StockDecorator do
end end
require 'spec_helper' require 'rails_helper'
describe TagDecorator do describe TagDecorator do
end end
require 'rails_helper'
describe TemplateDecorator do
end
require 'spec_helper' require 'rails_helper'
describe UsersDecorator do describe UsersDecorator do
end end
require 'spec_helper' require 'spec_helper'
describe "apis/markdown_preview.html.erb" do describe WatchingDecorator do
pending "add some examples to (or delete) #{__FILE__}"
end end
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
FactoryGirl.define do FactoryGirl.define do
factory :post do factory :post do
association :author, factory: :author
title 'sample title' title 'sample title'
body 'sample body' body 'sample body'
specified_date Date.new(2014, 4, 1) specified_date Date.new(2014, 4, 1)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment