Commit 5a4f1f2e by tady

Merge commit 'ea703555' into wip/130416_wysiwyg

Conflicts:
	Gemfile.lock
	app/assets/javascripts/lib/mod-md-editor.js.coffee
	app/controllers/posts_controller.rb
	app/views/posts/_form.html.slim
parents 21bec5f0 ea703555
...@@ -49,6 +49,7 @@ end ...@@ -49,6 +49,7 @@ end
gem 'mysql2' gem 'mysql2'
gem 'devise' gem 'devise'
gem 'omniauth-google-oauth2' gem 'omniauth-google-oauth2'
# Markdown # Markdown
......
...@@ -36,6 +36,9 @@ and get ...@@ -36,6 +36,9 @@ and get
- `http://localhost:3000` in [Authorized Javascript origins] - `http://localhost:3000` in [Authorized Javascript origins]
- `http://localhost:3000/users/auth/google_oauth2/callback` in [Authorized redirect URI] - `http://localhost:3000/users/auth/google_oauth2/callback` in [Authorized redirect URI]
5. Get [Client ID] and [Client secret] 5. Get [Client ID] and [Client secret]
6. Write your Client ID & Secret in config/settings/yml
7. Input form
-`rendevous` in [Project name] in Consent screen
## Create and edit config files. ## Create and edit config files.
......
/*! jquery-textcomplete - v0.1.3 - 2014-03-07 */!function(a){"use strict";var b=function(a){var b,d;return b=function(){d=!1},function(){var e;d||(d=!0,e=c(arguments),e.unshift(b),a.apply(this,e))}},c=function(a){var b;return b=Array.prototype.slice.call(a)},d=function(){var b;return b=a("<div></div>").css(["color"]).color,"undefined"!=typeof b?function(a,b){return a.css(b)}:function(b,c){var d;return d={},a.each(c,function(a,c){d[c]=b.css(c)}),d}}(),e=function(a){return a},f=function(a){var b={};return function(c,d){b[c]?d(b[c]):a.call(this,c,function(a){b[c]=(b[c]||[]).concat(a),d.apply(null,arguments)})}},g=function(a,b){var c,d;if(a.indexOf)return-1!=a.indexOf(b);for(c=0,d=a.length;d>c;c++)if(a[c]===b)return!0;return!1},h=function(){function c(b){var c;this.el=b.get(0),c=this.el===document.activeElement,this.$el=k(b),this.id="textComplete"+j++,this.strategies=[],c?(this.initialize(),this.$el.focus()):this.$el.one("focus.textComplete",a.proxy(this.initialize,this))}var e,f,g,h,j;e={wrapper:'<div class="textcomplete-wrapper"></div>',list:'<ul class="dropdown-menu"></ul>'},f={wrapper:{position:"relative"},list:{position:"absolute",top:0,left:0,zIndex:"100",display:"none"}},g=a(e.wrapper).css(f.wrapper),h=a(e.list).css(f.list),j=0,a.extend(c.prototype,{initialize:function(){var b,c;b=h.clone(),this.listView=new i(b,this),this.$el.before(b).on({"keyup.textComplete":a.proxy(this.onKeyup,this),"keydown.textComplete":a.proxy(this.listView.onKeydown,this.listView)}),c={},c["click."+this.id]=a.proxy(this.onClickDocument,this),c["keyup."+this.id]=a.proxy(this.onKeyupDocument,this),a(document).on(c)},register:function(a){this.strategies=this.strategies.concat(a)},renderList:function(a){this.clearAtNext&&(this.listView.clear(),this.clearAtNext=!1),a.length&&(this.listView.shown||(this.listView.setPosition(this.getCaretPosition()).clear().activate(),this.listView.strategy=this.strategy),a=a.slice(0,this.strategy.maxCount),this.listView.render(a)),!this.listView.data.length&&this.listView.shown&&this.listView.deactivate()},searchCallbackFactory:function(a){var b=this;return function(c,d){b.renderList(c),d||(a(),b.clearAtNext=!0)}},onKeyup:function(a){var b,c;if(!this.skipSearch(a))if(b=this.extractSearchQuery(this.getTextFromHeadToCaret()),b.length){if(c=b[1],this.term===c)return;this.term=c,this.search(b)}else this.term=null,this.listView.deactivate()},skipSearch:function(a){if(this.skipNextKeyup)return this.skipNextKeyup=!1,!0;switch(a.keyCode){case 40:case 38:return!0}},onSelect:function(b){var c,d,e;c=this.getTextFromHeadToCaret(),d=this.el.value.substring(this.el.selectionEnd),e=this.strategy.replace(b),a.isArray(e)&&(d=e[1]+d,e=e[0]),c=c.replace(this.strategy.match,e),this.$el.val(c+d).trigger("change").trigger("textComplete:select",b),this.el.focus(),this.el.selectionStart=this.el.selectionEnd=c.length,this.skipNextKeyup=!0},onClickDocument:function(a){a.originalEvent&&!a.originalEvent.keepTextCompleteDropdown&&this.listView.deactivate()},onKeyupDocument:function(a){this.listView.shown&&27===a.keyCode&&(this.listView.deactivate(),this.$el.focus())},destroy:function(){var b;this.$el.off(".textComplete"),a(document).off("."+this.id),this.listView&&this.listView.destroy(),b=this.$el.parent(),b.after(this.$el).remove(),this.$el.data("textComplete",void 0),this.$el=null},getCaretPosition:function(){if(0!==this.el.selectionEnd){var b,c,e,f,g,h;return h=this.$el.attr("dir")||this.$el.css("direction"),b=["border-width","font-family","font-size","font-style","font-variant","font-weight","height","letter-spacing","word-spacing","line-height","text-decoration","text-align","width","padding-top","padding-right","padding-bottom","padding-left","margin-top","margin-right","margin-bottom","margin-left"],c=a.extend({position:"absolute",overflow:"auto","white-space":"pre-wrap",top:0,left:-9999,direction:h},d(this.$el,b)),e=a("<div></div>").css(c).text(this.getTextFromHeadToCaret()),f=a("<span></span>").text(".").appendTo(e),this.$el.before(e),g=f.position(),g.top+=f.height()-this.$el.scrollTop(),"rtl"===h&&(g.left-=this.listView.$el.width()),e.remove(),g}},getTextFromHeadToCaret:function(){var a,b,c;return b=this.el.selectionEnd,"number"==typeof b?a=this.el.value.substring(0,b):document.selection&&(c=this.el.createTextRange(),c.moveStart("character",0),c.moveEnd("textedit"),a=c.text),a},extractSearchQuery:function(a){var b,c,d,e;for(b=0,c=this.strategies.length;c>b;b++)if(d=this.strategies[b],e=a.match(d.match))return[d,e[d.index]];return[]},search:b(function(a,b){var c;this.strategy=b[0],c=b[1],this.strategy.search(c,this.searchCallbackFactory(a))})});var k=function(a){return a.wrap(g.clone().css("display",a.css("display")))};return c}(),i=function(){function b(b,c){this.data=[],this.$el=b,this.index=0,this.completer=c,this.$el.on("click.textComplete","li.textcomplete-item",a.proxy(this.onClick,this))}return a.extend(b.prototype,{shown:!1,render:function(a){var b,c,d,e,f;for(b="",c=0,d=a.length;d>c&&(f=a[c],g(this.data,f)||(e=this.data.length,this.data.push(f),b+='<li class="textcomplete-item" data-index="'+e+'"><a>',b+=this.strategy.template(f),b+="</a></li>",this.data.length!==this.strategy.maxCount));c++);this.$el.append(b),this.data.length?this.activateIndexedItem():this.deactivate()},clear:function(){return this.data=[],this.$el.html(""),this.index=0,this},activateIndexedItem:function(){this.$el.find(".active").removeClass("active"),this.getActiveItem().addClass("active")},getActiveItem:function(){return a(this.$el.children().get(this.index))},activate:function(){return this.shown||(this.$el.show(),this.completer.$el.trigger("textComplete:show"),this.shown=!0),this},deactivate:function(){return this.shown&&(this.$el.hide(),this.completer.$el.trigger("textComplete:hide"),this.shown=!1,this.data=[],this.index=null),this},setPosition:function(a){return this.$el.css(a),this},select:function(a){var b=this;this.completer.onSelect(this.data[a]),setTimeout(function(){b.deactivate()},0)},onKeydown:function(a){this.shown&&(38===a.keyCode?(a.preventDefault(),0===this.index?this.index=this.data.length-1:this.index-=1,this.activateIndexedItem()):40===a.keyCode?(a.preventDefault(),this.index===this.data.length-1?this.index=0:this.index+=1,this.activateIndexedItem()):(13===a.keyCode||9===a.keyCode)&&(a.preventDefault(),this.select(parseInt(this.getActiveItem().data("index"),10))))},onClick:function(b){var c=a(b.target);b.originalEvent.keepTextCompleteDropdown=!0,c.hasClass("textcomplete-item")||(c=c.parents("li.textcomplete-item")),this.select(parseInt(c.data("index"),10))},destroy:function(){this.deactivate(),this.$el.off("click.textComplete").remove(),this.$el=null}}),b}();a.fn.textcomplete=function(b){var c,d,g,i;if(i="textComplete","destroy"===b)return this.each(function(){var b=a(this).data(i);b&&b.destroy()});for(c=0,d=b.length;d>c;c++)g=b[c],g.template||(g.template=e),null==g.index&&(g.index=2),g.cache&&(g.search=f(g.search)),g.maxCount||(g.maxCount=10);return this.each(function(){var c,d;c=a(this),d=c.data(i),d||(d=new h(c),c.data(i,d)),d.register(b)})}}(window.jQuery||window.Zepto);
/*
//@ sourceMappingURL=jquery.textcomplete.min.map
*/
\ No newline at end of file
# TODO:
# mod-mdEditorがページ内に複数あった場合の処理
$.fn.extend
mod_mdEditor: (options) ->
settings =
# preview api url
end_point: ''
settings = $.extend settings, options
return @each ()->
$root = $(@)
# Automaticaly change textarea height.
$root.find('.mod-mdEditor-body').autosize();
# disable tab key
$root.find('.mod-mdEditor-body').on 'keydown', (e) ->
$this = $(@)
keyCode = e.keyCode || e.which
console.log(keyCode)
console.log($this.data('autocompleting'))
# tab key
if keyCode is 9
e.preventDefault()
start = $this.get(0).selectionStart
end = $this.get(0).selectionEnd
# set textarea value to: text before caret + tab + text after caret
$this.val($this.val().substring(0, start) +
'\t' +
$this.val().substring(end))
# put caret at right position again
$this.get(0).selectionStart =
$this.get(0).selectionEnd = start + 1
# enter key
else if keyCode is 13
return true if $this.data('autocompleting')
val = $this.val()
start = $this.get(0).selectionStart
bl = val.lastIndexOf("\n", start-1)
line = val.substring(bl, start)
lm = line.match(/^\s+/)
ns = if lm? then lm[0].length - 1 else 0
nv = val.substring(0, start) + "\n"
_(ns).times ->
nv += "\t"
$this.val(nv + val.substring(start))
$this.get(0).selectionStart =
$this.get(0).selectionEnd = start + ns + 1
e.preventDefault()
# タグを選択可能に
$root.find('.mod-mdEditor-tags').select2 {
tags: window.RV.AllTags
}
# Previewを生成
generatePreview = ->
$.post(settings.end_point, {
'text': $root.find('.mod-mdEditor-body').val()
'authenticity_token': $("meta[name='csrf-token']").attr('content')
})
.done (data) ->
$root.find('.mod-mdEditor-preview').html(data)
# TODO
prettyPrint()
$('.mod-mdEditor-body').on('keyup mouseup change', generatePreview)
generatePreview()
/* Sample */
.dropdown-menu {
border: 1px solid #ddd;
background-color: white;
}
.dropdown-menu li {
border-top: 1px solid #ddd;
padding: 2px 5px;
}
.dropdown-menu li:first-child {
border-top: none;
}
.dropdown-menu li:hover,
.dropdown-menu .active {
background-color: rgb(110, 183, 219);
}
/* SHOULD not modify */
.dropdown-menu {
list-style: none;
padding: 0;
margin: 0;
}
.dropdown-menu a:hover {
cursor: pointer;
}
...@@ -32,4 +32,10 @@ class ApisController < ApplicationController ...@@ -32,4 +32,10 @@ class ApisController < ApplicationController
render json: { status: 'OK', files: s3_files } render json: { status: 'OK', files: s3_files }
end end
def user_mention
name_list = User.search(params[:q]).map{ |_user| "#{_user.nickname}[#{_user.name}]" }
render json: name_list
end
end end
...@@ -2,6 +2,6 @@ class FlowController < ApplicationController ...@@ -2,6 +2,6 @@ class FlowController < ApplicationController
before_action :require_login before_action :require_login
def show def show
@posts = Post.order(updated_at: :desc).limit(20).decorate @posts = Post.where(is_draft: false).order(updated_at: :desc).limit(20).decorate
end end
end end
...@@ -126,13 +126,13 @@ class PostsController < ApplicationController ...@@ -126,13 +126,13 @@ class PostsController < ApplicationController
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
def set_post def set_post
@post = Post.find(params[:id]) @post = Post.find(params[:id]).decorate
end end
# Never trust parameters from the scary internet, only allow the white list through. # Never trust parameters from the scary internet, only allow the white list through.
def post_params def post_params
@post_params ||= begin @post_params ||= begin
_param_hash = params.require(:post).permit(:title, :body, :tags).to_hash _param_hash = params.require(:post).permit(:title, :body, :tags, :is_draft, :specified_date).to_hash
# tags_text == 'Javascript,Ruby' # tags_text == 'Javascript,Ruby'
tags_text = _param_hash.delete('tags') tags_text = _param_hash.delete('tags')
......
...@@ -9,6 +9,6 @@ class SearchController < ApplicationController ...@@ -9,6 +9,6 @@ class SearchController < ApplicationController
end end
@count = scope.count @count = scope.count
@posts = scope.limit(10).decorate @posts = scope.limit(100).decorate
end end
end end
class UsersController < ApplicationController
before_action :set_user, only: [:edit, :update]
def edit
end
def update
respond_to do |format|
if @user.update(user_params)
format.html { redirect_to edit_user_path, flash: { notice: 'Post was successfully updated.' } }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
@user = current_user
end
def user_params
params.require(:user).permit(:nickname).to_hash
end
end
...@@ -19,4 +19,12 @@ class PostDecorator < Draper::Decorator ...@@ -19,4 +19,12 @@ class PostDecorator < Draper::Decorator
end end
end end
def display_date
if model.specified_date
model.specified_date.strftime('%Y-%m-%d')
else
model.updated_at.strftime('%Y-%m-%d')
end
end
end end
...@@ -9,10 +9,10 @@ class PostsDecorator < Draper::CollectionDecorator ...@@ -9,10 +9,10 @@ class PostsDecorator < Draper::CollectionDecorator
end end
def related_authors def related_authors
_authors = self.map do |_post| self.map do |_post|
_post.author _post.author
end.flatten.uniq end.flatten.uniq.map do |_author|
_author.decorate
UserDecorator.decorate_collection(_authors) end
end end
end end
class UserDecorator < Draper::Decorator class UserDecorator < Draper::Decorator
delegate_all delegate_all
def draft_count
model.posts.where(is_draft: true).count
end
end end
class UsersDecorator < 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 UsersHelper
end
...@@ -4,32 +4,42 @@ class Post < ActiveRecord::Base ...@@ -4,32 +4,42 @@ class Post < ActiveRecord::Base
belongs_to :author, class_name: 'User' belongs_to :author, class_name: 'User'
has_many :comments has_many :comments
default_scope { order(:updated_at => :desc) } # default_scope { where(is_draft: false).order(:updated_at => :desc) }
######################################################################
# validations
######################################################################
validates :title, presence: true
validates :body, presence: true
######################################################################
# Named scope # Named scope
######################################################################
scope :search, (lambda do |query| scope :search, (lambda do |query|
_where_list = includes(:author, :tags) _where_list = includes(:author, :tags).order(updated_at: :desc)
# Convert spaces to one space. # Convert spaces to one space.
query_list = query.gsub(/[\s ]+/, ' ').split(' ') query_list = query.split(/[\s ]+/)
query_list.each do |_query| query_list.each do |_query|
case _query case _query
when /^id:(.+)/ when /\Aid:(.+)/
_where_list = _where_list.where(id: Regexp.last_match[1]) _where_list = _where_list.where(id: Regexp.last_match[1])
when /^title:(.+)/ when /\Atitle:(.+)/
_where_list = _where_list.where('title LIKE ?', "%#{Regexp.last_match[1]}%") _where_list = _where_list.where('posts.title LIKE ?', "%#{Regexp.last_match[1]}%")
when /^body:(.+)/ when /\Abody:(.+)/
_where_list = _where_list.where('body LIKE ?', "%#{Regexp.last_match[1]}%") _where_list = _where_list.where('posts.body LIKE ?', "%#{Regexp.last_match[1]}%")
when /^@(.+)/ when /\A@(.+)/
_where_list = _where_list.where(users: { name: Regexp.last_match[1] }) _where_list = _where_list.where(users: { nickname: Regexp.last_match[1] })
when /^#(.+)/ when /\A#(.+)/
_where_list = _where_list.where(tags: { name: Regexp.last_match[1] }) _where_list = _where_list.where(tags: { name: Regexp.last_match[1] })
when /^date:(\d+)-(\d+)-(\d+)/ when /\Adate:(\d+)-(\d+)-(\d+)/
_date = Time.new(Regexp.last_match[1], Regexp.last_match[2], Regexp.last_match[3]) _date = Time.new(Regexp.last_match[1], Regexp.last_match[2], Regexp.last_match[3])
_where_list = _where_list.where('updated_at > ? AND updated_at < ?', _date, _date + 1.day) _where_list = _where_list.where('posts.updated_at > ? AND posts.updated_at < ?', _date, _date + 1.day)
when /\Adraft:1/
_where_list = _where_list.where(is_draft: true)
else else
_where_list = _where_list.where('title LIKE ? OR body LIKE ?', "%#{_query}%", "%#{_query}%") _where_list = _where_list.where('posts.title LIKE ? OR posts.body LIKE ?', "%#{_query}%", "%#{_query}%")
end end
end end
......
...@@ -7,23 +7,46 @@ class User < ActiveRecord::Base ...@@ -7,23 +7,46 @@ class User < ActiveRecord::Base
:recoverable, :rememberable, :trackable, :validatable :recoverable, :rememberable, :trackable, :validatable
devise :omniauthable, omniauth_providers: [:google_oauth2] devise :omniauthable, omniauth_providers: [:google_oauth2]
######################################################################
# association
######################################################################
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'
######################################################################
# scope
######################################################################
scope :post_recently, -> { scope :post_recently, -> {
User.joins(:posts).group('id').order('posts.updated_at desc') User.joins(:posts).group('id').order('posts.updated_at desc')
} }
scope :search, (lambda do |_query|
where('name LIKE ? OR nickname LIKE ?', "%#{_query}%", "%#{_query}%")
end)
######################################################################
# validations
######################################################################
validates :name, presence: true
validates :email, presence: true
validates :email, uniqueness: true
validates :nickname, presence: true
validates :nickname, format: { with: /\A[0-9A-Za-z]+\z/i }
validates :nickname, uniqueness: true
# Device # Device
def self.find_for_google_oauth2(access_token, signed_in_resource = nil) def self.find_for_google_oauth2(access_token, signed_in_resource = nil)
info = access_token.info info = access_token.info
user = User.where(email: info['email']).first user = User.where(email: info['email']).first
unless user unless user
new_nickname = (("a".."z").to_a + ("A".."Z").to_a + (0..9).to_a).shuffle[0..4].join
user = User.create(name: info['name'], user = User.create(name: info['name'],
image_url: info['image'], image_url: info['image'],
email: info['email'], email: info['email'],
password: Devise.friendly_token[0, 20] password: Devise.friendly_token[0, 20],
nickname: new_nickname
) )
end end
...@@ -36,6 +59,7 @@ class User < ActiveRecord::Base ...@@ -36,6 +59,7 @@ class User < ActiveRecord::Base
user user
end end
# check if google oauth token is expired # check if google oauth token is expired
def google_oauth_token_expired? def google_oauth_token_expired?
google_token_expires_at < Time.now google_token_expires_at < Time.now
...@@ -61,4 +85,6 @@ class User < ActiveRecord::Base ...@@ -61,4 +85,6 @@ class User < ActiveRecord::Base
) )
end end
end end
...@@ -23,6 +23,6 @@ ...@@ -23,6 +23,6 @@
h2.panel-title 最近投稿したユーザー(調整中) h2.panel-title 最近投稿したユーザー(調整中)
.panel-body.list-group .panel-body.list-group
- User.post_recently.limit(10).each_with_index do |author, i| - User.post_recently.limit(10).each_with_index do |author, i|
a.list-group-item.post-list data-author-id=author.id href="#" = author.name a.list-group-item.post-list data-author-id=author.id href=search_path(q: "@#{author.nickname}") = "#{author.name} (@#{author.nickname})"
nav.navbar.navbar-default.navbar-fixed-top role="navigation" nav.navbar.navbar-default.navbar-fixed-top role="navigation"
.container .container
.navbar-header .navbar-header
a.navbar-brand href=root_path Rendezvous a.navbar-brand href=root_path Rendezvous
...@@ -18,7 +19,6 @@ nav.navbar.navbar-default.navbar-fixed-top role="navigation" ...@@ -18,7 +19,6 @@ nav.navbar.navbar-default.navbar-fixed-top role="navigation"
a href=flow_path title="Frow" a href=flow_path title="Frow"
| Flow | Flow
ul.nav.navbar-nav.navbar-right ul.nav.navbar-nav.navbar-right
li li
form form
...@@ -29,14 +29,17 @@ nav.navbar.navbar-default.navbar-fixed-top role="navigation" ...@@ -29,14 +29,17 @@ nav.navbar.navbar-default.navbar-fixed-top role="navigation"
a.dropdown-toggle data-toggle="dropdown" a.dropdown-toggle data-toggle="dropdown"
= current_user.name = current_user.name
img src=current_user.image_url width="20" height="20"/ img src=current_user.image_url width="20" height="20"/
span.badge = current_user.decorate.draft_count
b.caret b.caret
ul.dropdown-menu ul.dropdown-menu
li li
a Account (todo) a href=search_path(q: "@#{current_user.nickname} draft:1")
| 下書き
span.badge.pull-right = current_user.decorate.draft_count
li li
a Settings (todo) a href=edit_user_path(current_user) マイページ
li.divider li.divider
li li
a href=destroy_user_session_path data-method="delete" rel="nofollow" SignOut a href=sign_out_path data-method="delete" rel="nofollow" SignOut
...@@ -10,19 +10,27 @@ ...@@ -10,19 +10,27 @@
li= msg li= msg
.row .row
.col-xs-10 .col-xs-9
.form-group .field
= f.label :title .input-group
= f.text_field :title, class: 'form-control mod-mdEditor-title', placeholder: 'post title' span.input-group-addon= f.label :title
.col-xs-2 = f.text_field :title, class: 'form-control mod-mdEditor-title'
.actions
.field
.input-group
span.input-group-addon= f.label :tags
= hidden_field :post, :tags, class: 'mod-mdEditor-tags', style: 'width:300px', value: @post.tags.map{ |_tag| _tag.name }.join(',')
.col-xs-3
p.actions
= f.submit class: 'btn btn-primary js-disable-confirm-unload', id: 'save_button' = f.submit class: 'btn btn-primary js-disable-confirm-unload', id: 'save_button'
p.actions
= f.check_box :is_draft
= f.label :is_draft, "下書き保存"
p.actions
= f.date_select :specified_date
.row br/
.col-xs-12
.form-group
= f.label :tags
= hidden_field :post, :tags, class: 'mod-mdEditor-tags', style: 'width:300px', value: @post.tags.map{ |_tag| _tag.name }.join(',')
.row .row
.col-xs-6 .col-xs-6
...@@ -56,6 +64,7 @@ ...@@ -56,6 +64,7 @@
.text-box.body.viewer.github.mod-mdEditor-preview .text-box.body.viewer.github.mod-mdEditor-preview
input#fileupload data-url="/apis/file_receiver" multiple="" name="files[]" style="display:none" type="file" / input#fileupload data-url="/apis/file_receiver" multiple="" name="files[]" style="display:none" type="file" /
- content_for :footer_js do - content_for :footer_js do
javascript: javascript:
// $.setConfirmUnload(); // $.setConfirmUnload();
...@@ -66,3 +75,54 @@ input#fileupload data-url="/apis/file_receiver" multiple="" name="files[]" style ...@@ -66,3 +75,54 @@ input#fileupload data-url="/apis/file_receiver" multiple="" name="files[]" style
$input: $('#fileupload'), $input: $('#fileupload'),
$textarea: $('.mod-mdEditor-textarea') $textarea: $('.mod-mdEditor-textarea')
}); });
// 下書き保存
$('.btn-save-draft').on('click', function(e){
e.preventDefault();
var val = $('.mod-mdEditor-body').val();
console.log(val);
});
// mention補完
// TODO: mod-md-editorに入れる?
$('textarea').textcomplete([
{ // html
mentions: ['yuku_t'],
match: /\B@([^\B]*)$/,
search: function (term, callback) {
$.getJSON('/apis/user_mention', { q: term })
.done(function (resp) { callback(resp); })
.fail(function () { callback([]); });
},
index: 1,
replace: function (mention) {
return '@' + mention + ' ';
}
}
]).on({
'textComplete:select': function (e, value) {
console.log('textComplete:select ' + value);
},
'textComplete:show': function (e) {
console.log('textComplete:show');
$(this).data('autocompleting', true);
},
'textComplete:hide': function (e) {
console.log('textComplete:hide');
$(this).data('autocompleting', false);
}
});
// .overlay([
// {
// match: /\B@\w+/g,
// css: {
// 'background-color': '#d8dfea'
// }
// }
// ]);
css:
.textcomplete-wrapper {
width: 100%;
}
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
a href=(posts_path(q: "@#{@post.author.name}")) a href=(posts_path(q: "@#{@post.author.name}"))
| @#{@post.author.name} | @#{@post.author.name}
span.label.label-danger span.label.label-danger
a href=(posts_path(q: "date:#{@post.updated_at.strftime('%Y-%m-%d')}")) = @post.updated_at.strftime('%Y-%m-%d') a href=(posts_path(q: "date:#{@post.display_date}")) = @post.display_date
.btn-group.pull-right style=("margin: -7px -12px 0 0;") .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)
span.glyphicon.glyphicon-pencil span.glyphicon.glyphicon-pencil
......
h1 編集
= render 'form' = render 'form'
...@@ -24,6 +24,6 @@ ...@@ -24,6 +24,6 @@
h2.panel-title "#{params[:q]}"に関連するユーザー h2.panel-title "#{params[:q]}"に関連するユーザー
.panel-body.list-group .panel-body.list-group
- @posts.related_authors.each do |_author| - @posts.related_authors.each do |_author|
a.list-group-item href=search_path(q: "@#{_author.name}") = _author.name a.list-group-item href=search_path(q: "@#{_author.nickname}") = _author.name
json.array!(@tag.posts) do |post| json.array!(@tag.posts) do |post|
json.extract! post, :title json.extract! post, :title
json.url post_url(post) json.url post_url(post)
json.start post.created_at json.start post.specified_date || post.created_at
end end
#post-form
= form_for(@user) do |f|
- if @user.errors.any?
#error_explanation
h2
= pluralize(@user.errors.count, "error")
| prohibited this post from being saved:
ul
- @user.errors.full_messages.each do |msg|
li= msg
.row
.form-horizontal role="form"
.form-group
= f.label :name, '名前', class: 'col-sm-2 control-label'
.col-sm-10
= f.text_field :name, class: 'form-control', readonly: true
.form-group
= f.label :email, 'メールアドレス', class: 'col-sm-2 control-label'
.col-sm-10
= f.text_field :email, class: 'form-control', readonly: true
.form-group
= f.label :nickname, 'ニックネーム', class: 'col-sm-2 control-label'
.col-sm-10
= f.text_field :nickname, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
button.btn.btn-success type="submit" 保存
h1 編集
= render 'form'
<h1>Users#update</h1>
<p>Find me in app/views/users/update.html.erb</p>
...@@ -42,7 +42,7 @@ common: &default_settings ...@@ -42,7 +42,7 @@ common: &default_settings
# - Ajax Service # - Ajax Service
# - All Services # - All Services
# #
app_name: My Application app_name: Rendezvous
# When "true", the agent collects performance data about your # When "true", the agent collects performance data about your
# application and reports this data to the New Relic service at # application and reports this data to the New Relic service at
......
...@@ -2,6 +2,7 @@ Rendezvous::Application.routes.draw do ...@@ -2,6 +2,7 @@ Rendezvous::Application.routes.draw do
post 'apis/markdown_preview' post 'apis/markdown_preview'
post 'apis/file_receiver' post 'apis/file_receiver'
get 'apis/user_mention'
get 'tags/:name/events' => 'tags#events', as: 'event_tag' get 'tags/:name/events' => 'tags#events', as: 'event_tag'
root 'welcome#top', as: 'root' root 'welcome#top', as: 'root'
...@@ -20,7 +21,19 @@ Rendezvous::Application.routes.draw do ...@@ -20,7 +21,19 @@ Rendezvous::Application.routes.draw do
post 'tags/:name/move_to/:move_to_name' => 'tags#move_to', as: 'move_to_tag' post 'tags/:name/move_to/:move_to_name' => 'tags#move_to', as: 'move_to_tag'
resources :tags, :param => :name resources :tags, :param => :name
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' } resource :user, :only => [:edit, :update]
# devise_for :users , controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
# devise_for :users , only: [:sign_in, :sign_out, :session]
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }, skip: [:sessions]
devise_scope :user do
# get 'sign_in', to: 'users/sessions#new', as: :new_user_session
delete 'sign_out', to: 'devise/sessions#destroy', as: :sign_out
end
# get 'users/edit' => 'users#edit', as: 'edit_user'
# post 'users/update' => 'users#update', as: 'update_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.
# See how all your routes lay out with "rake routes". # See how all your routes lay out with "rake routes".
......
class AddIsDraftToPosts < ActiveRecord::Migration
def change
add_column :posts, :is_draft, :boolean, default: false
add_index :posts, :is_draft
end
end
class AddNicknameToUsers < ActiveRecord::Migration
def change
add_column :users, :nickname, :string, default: '', null: false
add_index "users", ["nickname"]
end
end
class AddSpecifiedDateToPosts < ActiveRecord::Migration
def change
add_column :posts, :specified_date, :date
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: 20140302053916) do ActiveRecord::Schema.define(version: 20140328045902) do
create_table "comments", force: true do |t| create_table "comments", force: true do |t|
t.integer "author_id" t.integer "author_id"
...@@ -40,8 +40,12 @@ ActiveRecord::Schema.define(version: 20140302053916) do ...@@ -40,8 +40,12 @@ ActiveRecord::Schema.define(version: 20140302053916) do
t.integer "author_id" t.integer "author_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.boolean "is_draft", default: false
t.date "specified_date"
end end
add_index "posts", ["is_draft"], name: "index_posts_on_is_draft", using: :btree
create_table "tags", force: true do |t| create_table "tags", force: true do |t|
t.string "name" t.string "name"
t.datetime "created_at" t.datetime "created_at"
...@@ -70,9 +74,11 @@ ActiveRecord::Schema.define(version: 20140302053916) do ...@@ -70,9 +74,11 @@ ActiveRecord::Schema.define(version: 20140302053916) do
t.string "google_auth_token" t.string "google_auth_token"
t.string "google_refresh_token" t.string "google_refresh_token"
t.datetime "google_token_expires_at" t.datetime "google_token_expires_at"
t.string "nickname", default: "", null: false
end end
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["nickname"], name: "index_users_on_nickname", using: :btree
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
create_table "versions", force: true do |t| create_table "versions", force: true do |t|
......
namespace :migrations do
desc '001 nicknameを自動付与'
task task_001_user_nickname: :environment do
User.all.each do |_user|
next if _user.nickname.present?
new_nickname = (("a".."z").to_a + ("A".."Z").to_a + (0..9).to_a).shuffle[0..4].join
_user.update_attributes!(nickname: new_nickname)
end
end
end
...@@ -21,21 +21,21 @@ Feature: アクセス制限 ...@@ -21,21 +21,21 @@ Feature: アクセス制限
# Then response code is 200 # Then response code is 200
# Then response includes '<!--view:welcome/login-->' # Then response includes '<!--view:welcome/login-->'
Scenario: ログイン --> TOPページ # Scenario: ログイン --> TOPページ
Given login # Given login
When visit '/' # When visit '/'
Then response code is 200 # Then response code is 200
Then response includes '<!--view:flow/show-->' # Then response includes '<!--view:flow/show-->'
Scenario: ログイン --> flowページ # Scenario: ログイン --> flowページ
Given login # Given login
When visit '/flow' # When visit '/flow'
Then response code is 200 # Then response code is 200
Then response includes '<!--view:flow/show-->' # Then response includes '<!--view:flow/show-->'
Scenario: ログイン --> ログアウト # Scenario: ログイン --> ログアウト
Given login # Given login
When logout # When logout
Then response code is 200 # Then response code is 200
Then response includes '<!--view:welcome/login-->' # Then response includes '<!--view:welcome/login-->'
require 'spec_helper'
describe UsersController do
before do
end
describe "GET 'edit'" do
login_user
it "returns http success" do
get :edit
response.should be_success
end
end
describe "GET 'update'" do
login_user
it "returns http success" do
patch :update, user: { nickname: 'bob' }
response.should redirect_to('/user/edit')
end
end
end
require 'spec_helper'
describe UsersDecorator do
end
...@@ -2,6 +2,7 @@ FactoryGirl.define do ...@@ -2,6 +2,7 @@ FactoryGirl.define do
factory :alice, class: User do factory :alice, class: User do
name 'Alice' name 'Alice'
email 'alice@mail.com' email 'alice@mail.com'
nickname 'alice'
password Devise.friendly_token[0, 20] password Devise.friendly_token[0, 20]
google_token_expires_at Time.now + 30.minutes google_token_expires_at Time.now + 30.minutes
end end
...@@ -9,6 +10,7 @@ FactoryGirl.define do ...@@ -9,6 +10,7 @@ FactoryGirl.define do
factory :bob, class: User do factory :bob, class: User do
name 'Bob' name 'Bob'
email 'bob@mail.com' email 'bob@mail.com'
nickname 'bob'
password Devise.friendly_token[0, 20] password Devise.friendly_token[0, 20]
google_token_expires_at Time.now - 1.hour google_token_expires_at Time.now - 1.hour
end end
...@@ -16,6 +18,7 @@ FactoryGirl.define do ...@@ -16,6 +18,7 @@ FactoryGirl.define do
factory :login_user_1, class: User do factory :login_user_1, class: User do
name 'Test User' name 'Test User'
email 'example@example.com' email 'example@example.com'
nickname 'testuser'
password 'changeme' password 'changeme'
password_confirmation 'changeme' password_confirmation 'changeme'
# required if the Devise Confirmable module is used # required if the Devise Confirmable module is used
......
require 'spec_helper'
# Specs in this file have access to a helper object that includes
# the UsersHelper. For example:
#
# describe UsersHelper do
# describe "string concat" do
# it "concats two strings with spaces" do
# expect(helper.concat_strings("this","that")).to eq("this that")
# end
# end
# end
describe UsersHelper do
pending "add some examples to (or delete) #{__FILE__}"
end
...@@ -45,6 +45,7 @@ describe Post do ...@@ -45,6 +45,7 @@ describe Post do
@post1 = Post.create id: 1001, title: 'ruby rspec', body: 'This is first espec test: ruby' @post1 = Post.create id: 1001, title: 'ruby rspec', body: 'This is first espec test: ruby'
@post2 = Post.create id: 1002, title: 'php test', body: 'PHP is very easy', author_id: @alice.id @post2 = Post.create id: 1002, title: 'php test', body: 'PHP is very easy', author_id: @alice.id
@post3 = Post.create id: 1003, title: 'java java...', body: 'Java is not ruby...', updated_at: Time.new(1989, 2, 25, 5, 30, 0) @post3 = Post.create id: 1003, title: 'java java...', body: 'Java is not ruby...', updated_at: Time.new(1989, 2, 25, 5, 30, 0)
@post4 = Post.create id: 1004, title: 'about ruby TDD', body: 'test is the best ....', is_draft: true
@tag_java = Tag.create(name: 'java') @tag_java = Tag.create(name: 'java')
@post3.tags << @tag_java @post3.tags << @tag_java
end end
...@@ -55,7 +56,7 @@ describe Post do ...@@ -55,7 +56,7 @@ describe Post do
end end
it 'by title' do it 'by title' do
expect(Post.search('title:ruby')).to have(1).items expect(Post.search('title:ruby')).to have(2).items
expect(Post.search('title:ruby')).to include(@post1) expect(Post.search('title:ruby')).to include(@post1)
end end
...@@ -79,8 +80,13 @@ describe Post do ...@@ -79,8 +80,13 @@ describe Post do
expect(Post.search('date:1989-2-25')).to include(@post3) expect(Post.search('date:1989-2-25')).to include(@post3)
end end
it 'by draft' do
expect(Post.search('ruby')).to have(3).items
expect(Post.search('ruby draft:1')).to have(1).items
end
it 'by else' do it 'by else' do
expect(Post.search('ruby')).to have(2).items expect(Post.search('ruby')).to have(3).items
expect(Post.search('ruby')).to include(@post1) expect(Post.search('ruby')).to include(@post1)
expect(Post.search('ruby')).to include(@post3) expect(Post.search('ruby')).to include(@post3)
end end
......
...@@ -5,9 +5,9 @@ describe Tag do ...@@ -5,9 +5,9 @@ describe Tag do
before :each do before :each do
@tag_ruby = Tag.create(name: 'ruby') @tag_ruby = Tag.create(name: 'ruby')
@tag_java = Tag.create(name: 'java') @tag_java = Tag.create(name: 'java')
@post1 = Post.create id: 1001, title: 'ruby rspec', tags: [@tag_ruby] @post1 = Post.create id: 1001, title: 'ruby rspec', body: 'hoge', tags: [@tag_ruby]
@post2 = Post.create id: 1002, title: 'ruby is better than java', tags: [@tag_ruby, @tag_java] @post2 = Post.create id: 1002, title: 'ruby is better than java', body: 'hoge', tags: [@tag_ruby, @tag_java]
@post3 = Post.create id: 1003, title: 'java java...', tags: [@tag_java] @post3 = Post.create id: 1003, title: 'java java...', body: 'hoge', tags: [@tag_java]
end end
it 'successfully moved' do it 'successfully moved' do
......
...@@ -25,6 +25,7 @@ describe User do ...@@ -25,6 +25,7 @@ describe User do
@attr = { @attr = {
name: 'Example User', name: 'Example User',
email: 'user@example.com', email: 'user@example.com',
nickname: 'testnick',
password: 'changeme', password: 'changeme',
password_confirmation: 'changeme' password_confirmation: 'changeme'
} }
...@@ -118,4 +119,8 @@ describe User do ...@@ -118,4 +119,8 @@ describe User do
end end
end end
describe 'draft' do
pending 'draft'
end
end end
require 'spec_helper'
describe "users/edit.html.erb" do
pending "add some examples to (or delete) #{__FILE__}"
end
require 'spec_helper'
describe "users/update.html.erb" do
pending "add some examples to (or delete) #{__FILE__}"
end
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