mist 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/.travis.yml +13 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +192 -0
- data/README.md +108 -0
- data/Rakefile +9 -0
- data/app/assets/images/mist/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- data/app/assets/images/mist/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- data/app/assets/images/mist/ui-bg_flat_10_000000_40x100.png +0 -0
- data/app/assets/images/mist/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- data/app/assets/images/mist/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- data/app/assets/images/mist/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/app/assets/images/mist/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- data/app/assets/images/mist/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- data/app/assets/images/mist/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- data/app/assets/images/mist/ui-icons_222222_256x240.png +0 -0
- data/app/assets/images/mist/ui-icons_228ef1_256x240.png +0 -0
- data/app/assets/images/mist/ui-icons_ef8c08_256x240.png +0 -0
- data/app/assets/images/mist/ui-icons_ffd27a_256x240.png +0 -0
- data/app/assets/images/mist/ui-icons_ffffff_256x240.png +0 -0
- data/app/assets/javascripts/mist/jquery-ui-1.8.17.custom.min.js +356 -0
- data/app/assets/javascripts/mist/posts.js.coffee +3 -0
- data/app/assets/javascripts/mist_core.js +3 -0
- data/app/assets/stylesheets/mist/posts.css.sass +97 -0
- data/app/assets/stylesheets/mist/scaffolds.css.scss +56 -0
- data/app/assets/stylesheets/mist/ui-lightness/jquery-ui-1.8.17.custom.css +565 -0
- data/app/assets/stylesheets/mist_core.css +4 -0
- data/app/controllers/application_controller.rb +3 -0
- data/app/controllers/mist/posts_controller.rb +130 -0
- data/app/helpers/mist/posts_helper.rb +61 -0
- data/app/mailers/.gitkeep +0 -0
- data/app/models/.gitkeep +0 -0
- data/app/models/mist/post.rb +240 -0
- data/app/models/mist/post_sweeper.rb +63 -0
- data/app/views/layouts/mist/posts.html.erb +66 -0
- data/app/views/mist/posts/_form.html.erb +55 -0
- data/app/views/mist/posts/_post.html.erb +19 -0
- data/app/views/mist/posts/_sidebar_title.html.erb +5 -0
- data/app/views/mist/posts/edit.html.erb +16 -0
- data/app/views/mist/posts/feed.atom.builder +17 -0
- data/app/views/mist/posts/index.html.erb +31 -0
- data/app/views/mist/posts/new.html.erb +15 -0
- data/app/views/mist/posts/show.html.erb +3 -0
- data/config/cucumber.yml +8 -0
- data/config/environment.rb +1 -0
- data/config/routes.rb +7 -0
- data/features/atom_feed.feature +20 -0
- data/features/authorize_create_post.feature +14 -0
- data/features/authorize_destroy_post.feature +27 -0
- data/features/authorize_update_post.feature +28 -0
- data/features/authorize_view_unpublished.feature +22 -0
- data/features/create_post.feature +13 -0
- data/features/destroy_post.feature +13 -0
- data/features/format_with_markdown.feature +13 -0
- data/features/popular_posts.feature +35 -0
- data/features/recent_posts.feature +28 -0
- data/features/similar_posts.feature +43 -0
- data/features/step_definitions/authorization_steps.rb +17 -0
- data/features/step_definitions/filesystem_steps.rb +13 -0
- data/features/step_definitions/page_steps.rb +43 -0
- data/features/step_definitions/post_steps.rb +58 -0
- data/features/step_definitions/sidebar_steps.rb +21 -0
- data/features/support/env.rb +58 -0
- data/features/support/setup.rb +15 -0
- data/features/update_post.feature +19 -0
- data/gemfiles/common +20 -0
- data/gemfiles/rails-3.1.3 +4 -0
- data/gemfiles/rails-3.1.3.lock +195 -0
- data/gemfiles/rails-3.2.0 +4 -0
- data/lib/generators/mist/USAGE +13 -0
- data/lib/generators/mist/setup_generator.rb +21 -0
- data/lib/generators/mist/templates/initializer.rb +17 -0
- data/lib/generators/mist/templates/mist.css.scss +2 -0
- data/lib/generators/mist/templates/mist.js.coffee +1 -0
- data/lib/generators/mist/views_generator.rb +11 -0
- data/lib/mist.rb +26 -0
- data/lib/mist/code_example_parser.rb +86 -0
- data/lib/mist/configuration.rb +68 -0
- data/lib/mist/configuration/author.rb +8 -0
- data/lib/mist/engine.rb +12 -0
- data/lib/mist/git_file_system_history.rb +66 -0
- data/lib/mist/git_model.rb +154 -0
- data/lib/mist/git_model/attributes.rb +21 -0
- data/lib/mist/git_model/class_methods.rb +141 -0
- data/lib/mist/permalink.rb +5 -0
- data/lib/mist/repository.rb +15 -0
- data/lib/mist/version.rb +8 -0
- data/mist.gemspec +33 -0
- data/spec/controllers/posts_controller_spec.rb +263 -0
- data/spec/dummy_rails_app/app/assets/javascripts/mist.js.coffee +1 -0
- data/spec/dummy_rails_app/app/assets/stylesheets/mist.css.scss +2 -0
- data/spec/dummy_rails_app/app/views/layouts/mist/posts.html.erb +60 -0
- data/spec/dummy_rails_app/config.ru +4 -0
- data/spec/dummy_rails_app/config/application.rb +48 -0
- data/spec/dummy_rails_app/config/boot.rb +7 -0
- data/spec/dummy_rails_app/config/database.yml +28 -0
- data/spec/dummy_rails_app/config/environment.rb +5 -0
- data/spec/dummy_rails_app/config/environments/development.rb +32 -0
- data/spec/dummy_rails_app/config/environments/production.rb +60 -0
- data/spec/dummy_rails_app/config/environments/test.rb +42 -0
- data/spec/dummy_rails_app/config/initializers/active_gist_credentials.rb +8 -0
- data/spec/dummy_rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy_rails_app/config/initializers/inflections.rb +10 -0
- data/spec/dummy_rails_app/config/initializers/mime_types.rb +5 -0
- data/spec/dummy_rails_app/config/initializers/mist.rb +17 -0
- data/spec/dummy_rails_app/config/initializers/secret_token.rb +7 -0
- data/spec/dummy_rails_app/config/initializers/session_store.rb +8 -0
- data/spec/dummy_rails_app/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy_rails_app/config/locales/en.yml +5 -0
- data/spec/dummy_rails_app/config/routes.rb +3 -0
- data/spec/dummy_rails_app/db/schema.rb +22 -0
- data/spec/dummy_rails_app/db/seeds.rb +7 -0
- data/spec/dummy_rails_app/doc/README_FOR_APP +2 -0
- data/spec/dummy_rails_app/lib/assets/.gitkeep +0 -0
- data/spec/dummy_rails_app/lib/tasks/.gitkeep +0 -0
- data/spec/dummy_rails_app/lib/tasks/cucumber.rake +65 -0
- data/spec/dummy_rails_app/public/404.html +26 -0
- data/spec/dummy_rails_app/public/422.html +26 -0
- data/spec/dummy_rails_app/public/500.html +26 -0
- data/spec/dummy_rails_app/public/favicon.ico +0 -0
- data/spec/dummy_rails_app/public/robots.txt +5 -0
- data/spec/dummy_rails_app/script/cucumber +10 -0
- data/spec/dummy_rails_app/script/rails +6 -0
- data/spec/dummy_rails_app/vendor/assets/stylesheets/.gitkeep +0 -0
- data/spec/dummy_rails_app/vendor/plugins/.gitkeep +0 -0
- data/spec/factories/posts.rb +8 -0
- data/spec/fixtures/gist_404 +6 -0
- data/spec/fixtures/gist_with_1_code_example +66 -0
- data/spec/helpers/posts_helper_spec.rb +64 -0
- data/spec/lib/mist/code_example_parser_spec.rb +135 -0
- data/spec/lib/mist/configuration_spec.rb +88 -0
- data/spec/lib/mist/permalink_spec.rb +17 -0
- data/spec/lib/mist/repository_spec.rb +20 -0
- data/spec/models/mist/git_model_spec.rb +260 -0
- data/spec/models/mist/post_spec.rb +575 -0
- data/spec/requests/posts_caching_spec.rb +152 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/cache_helpers.rb +71 -0
- data/spec/support/config.rb +42 -0
- data/spec/support/fakeweb.rb +3 -0
- data/spec/views/mist/posts/edit.html.erb_spec.rb +11 -0
- data/spec/views/mist/posts/index.html.erb_spec.rb +27 -0
- data/spec/views/mist/posts/new.html.erb_spec.rb +15 -0
- data/spec/views/mist/posts/show.html.erb_spec.rb +11 -0
- metadata +371 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
class Mist::PostsController < ApplicationController
|
|
2
|
+
# caches_action :index, :cache_path => proc { cache_path }
|
|
3
|
+
caches_page :feed
|
|
4
|
+
caches_page :index, :cache_path => proc { cache_path }
|
|
5
|
+
caches_action :show, :cache_path => proc { cache_path }
|
|
6
|
+
before_filter :bump_post_popularity, :only => :show
|
|
7
|
+
cache_sweeper Mist::PostSweeper
|
|
8
|
+
|
|
9
|
+
# GET /posts
|
|
10
|
+
# GET /posts.json
|
|
11
|
+
def index
|
|
12
|
+
if Mist.authorized? :view_drafts, self
|
|
13
|
+
@posts = Mist::Post.last(20).reverse
|
|
14
|
+
else
|
|
15
|
+
@posts = Mist::Post.recently_published(20)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
respond_to do |format|
|
|
19
|
+
format.html # index.html.erb
|
|
20
|
+
format.json { render :json => @posts }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# GET /posts/feed
|
|
25
|
+
def feed
|
|
26
|
+
respond_to do |format|
|
|
27
|
+
format.atom do
|
|
28
|
+
@title = Mist.title
|
|
29
|
+
@posts = Mist::Post.all_by_publication_date
|
|
30
|
+
unless @posts.empty?
|
|
31
|
+
@updated = @posts.inject(@posts.first.updated_at) do |date, post|
|
|
32
|
+
date > post.updated_at ? date : post.updated_at
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
render :layout => false
|
|
36
|
+
end
|
|
37
|
+
format.rss { redirect_to feed_posts_path(:format => :atom), :status => :moved_permanently }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# GET /posts/1
|
|
42
|
+
# GET /posts/1.json
|
|
43
|
+
def show
|
|
44
|
+
@post ||= Mist::Post.find(params[:id])
|
|
45
|
+
|
|
46
|
+
respond_to do |format|
|
|
47
|
+
format.html # show.html.erb
|
|
48
|
+
format.json { render :json => @post }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# GET /posts/new
|
|
53
|
+
# GET /posts/new.json
|
|
54
|
+
def new
|
|
55
|
+
redirect_to posts_path and return unless Mist.authorized?(:create_post, self)
|
|
56
|
+
@post = Mist::Post.new
|
|
57
|
+
|
|
58
|
+
respond_to do |format|
|
|
59
|
+
format.html # new.html.erb
|
|
60
|
+
format.json { render :json => @post }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# GET /posts/1/edit
|
|
65
|
+
def edit
|
|
66
|
+
redirect_to posts_path and return unless Mist.authorized?(:update_post, self)
|
|
67
|
+
@post = Mist::Post.find(params[:id])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# POST /posts
|
|
71
|
+
# POST /posts.json
|
|
72
|
+
def create
|
|
73
|
+
redirect_to posts_path and return unless Mist.authorized?(:create_post, self)
|
|
74
|
+
@post = Mist::Post.new(params[:post])
|
|
75
|
+
|
|
76
|
+
respond_to do |format|
|
|
77
|
+
if @post.save
|
|
78
|
+
format.html { redirect_to @post, :notice => 'Post was successfully created.' }
|
|
79
|
+
format.json { render :json => @post, :status => :created, :location => @post }
|
|
80
|
+
else
|
|
81
|
+
format.html { render :action => "new" }
|
|
82
|
+
format.json { render :json => @post.errors, :status => :unprocessable_entity }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# PUT /posts/1
|
|
88
|
+
# PUT /posts/1.json
|
|
89
|
+
def update
|
|
90
|
+
redirect_to posts_path and return unless Mist.authorized?(:update_post, self)
|
|
91
|
+
@post = Mist::Post.find(params[:id])
|
|
92
|
+
|
|
93
|
+
respond_to do |format|
|
|
94
|
+
if @post.update_attributes(params[:post])
|
|
95
|
+
format.html { redirect_to @post, :notice => 'Post was successfully updated.' }
|
|
96
|
+
format.json { head :ok }
|
|
97
|
+
else
|
|
98
|
+
format.html { render :action => "edit" }
|
|
99
|
+
format.json { render :json => @post.errors, :status => :unprocessable_entity }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# DELETE /posts/1
|
|
105
|
+
# DELETE /posts/1.json
|
|
106
|
+
def destroy
|
|
107
|
+
redirect_to posts_path and return unless Mist.authorized?(:destroy_post, self)
|
|
108
|
+
@post = Mist::Post.find(params[:id])
|
|
109
|
+
@post.destroy
|
|
110
|
+
|
|
111
|
+
respond_to do |format|
|
|
112
|
+
format.html { redirect_to posts_url }
|
|
113
|
+
format.json { head :ok }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
def cache_path
|
|
119
|
+
options = Mist.authorized_actions.inject(ActiveSupport::OrderedHash.new) do |hash, key|
|
|
120
|
+
hash[key] = true if Mist.authorized?(key, self)
|
|
121
|
+
hash
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def bump_post_popularity
|
|
126
|
+
@post = Mist::Post.find(params[:id])
|
|
127
|
+
redirect_to posts_path and return unless @post.published? || Mist.authorized?(:view_drafts, self)
|
|
128
|
+
Mist::Post.increase_popularity(@post) if @post
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Mist::PostsHelper
|
|
2
|
+
def authorized_link(type, *link_options)
|
|
3
|
+
if authorized? type
|
|
4
|
+
link_to *link_options
|
|
5
|
+
else
|
|
6
|
+
""
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def authorized_links(separator, *link_options)
|
|
11
|
+
link_options.collect { |link_args| authorized_link(*link_args) }.reject { |a| a.blank? }.join(separator).html_safe
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def admin_link_separator
|
|
15
|
+
' • '.html_safe
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authorized?(type)
|
|
19
|
+
Mist.authorized? type, controller
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render_posts
|
|
23
|
+
preview = false
|
|
24
|
+
@posts.collect { |post|
|
|
25
|
+
if post.published? || authorized?(:view_drafts)
|
|
26
|
+
render(:partial => 'post', :locals => { :post => post, :preview => preview }).tap do
|
|
27
|
+
preview = true
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
""
|
|
31
|
+
end
|
|
32
|
+
}.join.html_safe
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def recent_posts(count = 5)
|
|
36
|
+
@recent_posts ||= {}
|
|
37
|
+
@recent_posts[count] ||= begin
|
|
38
|
+
# without SQL, we don't have access to conveniences like Post.where(:published),
|
|
39
|
+
# so instead just find 2x count and return those that are published.
|
|
40
|
+
result = Mist::Post.recently_published count*2
|
|
41
|
+
result = result[0...count] if result.length > count
|
|
42
|
+
result
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def popular_posts(count = 5)
|
|
47
|
+
# same as recent posts -- get twice as many and filter out the extras
|
|
48
|
+
@popular_posts ||= {}
|
|
49
|
+
@popular_posts[count] ||= begin
|
|
50
|
+
result = Mist::Post.most_popular(count*2).select { |post| post.published? }
|
|
51
|
+
result = result[0...count] if result.length > count
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def similar_posts(count = 5)
|
|
57
|
+
return [] if @post.nil?
|
|
58
|
+
@similar_posts ||= {}
|
|
59
|
+
@similar_posts[count] ||= @post.similar_posts(count)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
File without changes
|
data/app/models/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
class Mist::Post < Mist::GitModel
|
|
2
|
+
TAG_DELIM = /\s*,\s*/
|
|
3
|
+
include Mist::Permalink
|
|
4
|
+
|
|
5
|
+
validates_presence_of :title
|
|
6
|
+
validates_presence_of :content
|
|
7
|
+
|
|
8
|
+
validate do |record|
|
|
9
|
+
if record.new_record? && self.class.exist?(record.id)
|
|
10
|
+
record.errors.add :title, 'has already been taken'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
timestamps
|
|
15
|
+
attribute :content
|
|
16
|
+
attribute :title
|
|
17
|
+
attribute :published_at
|
|
18
|
+
attribute :gist_id
|
|
19
|
+
attribute :popularity, :default => 0
|
|
20
|
+
attribute :tags, :default => []
|
|
21
|
+
|
|
22
|
+
before_validation { |r| r.id = permalink(r.title) unless r.title.blank? }
|
|
23
|
+
after_validation :update_gist_if_necessary
|
|
24
|
+
after_save :update_meta
|
|
25
|
+
after_initialize :load_code_examples_from_gist
|
|
26
|
+
after_destroy :destroy_gist
|
|
27
|
+
|
|
28
|
+
def self.load_existing_with_attribute(attribute_name, array)
|
|
29
|
+
array.collect { |(post_id, attribute_value)| find post_id, attribute_name => attribute_value }.reject { |i| i.nil? }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.most_popular(count)
|
|
33
|
+
# invert <=> so that result is descending order
|
|
34
|
+
load_existing_with_attribute :popularity, self[:popular_posts].sort { |a, b| -(a[1].to_i <=> b[1].to_i) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.increase_popularity(post)
|
|
38
|
+
self[:popular_posts][post.id] = popularity_for(post.id) + 1
|
|
39
|
+
save_meta_data :popular_posts
|
|
40
|
+
post.popularity = self[:popular_posts][post.id]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.popularity_for(post_id)
|
|
44
|
+
self[:popular_posts][post_id] || 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.recently_published(count)
|
|
48
|
+
all_by_publication_date.tap do |result|
|
|
49
|
+
result.pop while result.length > count
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.all_by_publication_date
|
|
54
|
+
# invert <=> so that result is descending order
|
|
55
|
+
load_existing_with_attribute :published_at, self[:published_at].sort { |a, b| -(a[1] <=> b[1]) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.matching_tags(tags)
|
|
59
|
+
return [] if tags.blank?
|
|
60
|
+
matches = self[:tags].inject({}) { |h,(k,v)| ((t = v.split(TAG_DELIM)) & tags).size > 0 ? h[k] = t : nil; h }
|
|
61
|
+
load_existing_with_attribute :tags, matches.sort { |a, b| -((a[1] & tags).size <=> (b[1] & tags).size) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def similar_posts(max_count = nil)
|
|
65
|
+
self.class.matching_tags(tags).tap do |matching|
|
|
66
|
+
matching.delete self # similar does not mean identical :)
|
|
67
|
+
while max_count && matching.length > max_count
|
|
68
|
+
matching.pop
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def tags=(t)
|
|
74
|
+
if t.kind_of?(String)
|
|
75
|
+
attributes[:tags] = t.split(TAG_DELIM)
|
|
76
|
+
else
|
|
77
|
+
attributes[:tags] = t
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def update_meta
|
|
82
|
+
if popularity_changed? || new_record?
|
|
83
|
+
self.class[:popular_posts][id] = popularity
|
|
84
|
+
self.class.save_meta_data :popular_posts
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if published_at_changed?
|
|
88
|
+
if published_at.blank?
|
|
89
|
+
self.class[:published_at].delete id
|
|
90
|
+
else
|
|
91
|
+
self.class[:published_at][id] = published_at
|
|
92
|
+
end
|
|
93
|
+
self.class.save_meta_data :published_at
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if tags && !tags.empty?
|
|
97
|
+
self.class[:tags][id] = tags.join(', ')
|
|
98
|
+
else
|
|
99
|
+
self.class[:tags].delete id
|
|
100
|
+
end
|
|
101
|
+
self.class.save_meta_data :tags
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def title=(value)
|
|
105
|
+
attributes[:title] = value
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def published?
|
|
109
|
+
!published_at.blank?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def draft?
|
|
113
|
+
!published?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def publish
|
|
117
|
+
self.published_at = Time.now unless published?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def unpublish
|
|
121
|
+
self.published_at = nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def published=(bool)
|
|
125
|
+
bool ? publish : unpublish
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def content=(c)
|
|
129
|
+
attributes[:content] = c.gsub(/\r/, "")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def generated_gist_description
|
|
133
|
+
'Code examples for "%s" - %s' % [title, url]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def gist
|
|
137
|
+
@gist ||= begin
|
|
138
|
+
if gist_id.blank?
|
|
139
|
+
if has_code_examples?
|
|
140
|
+
ActiveGist.new(:description => generated_gist_description, :public => true)
|
|
141
|
+
else
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
else
|
|
145
|
+
begin
|
|
146
|
+
ActiveGist.find(gist_id)
|
|
147
|
+
rescue RestClient::ResourceNotFound
|
|
148
|
+
self.gist_id = nil
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def content_as_html
|
|
156
|
+
GitHub::Markup.render("#{title}.markdown", content_with_embedded_gists).html_safe
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def content_as_html_preview
|
|
160
|
+
# just take to the first blank line -- that's probably the first paragraph
|
|
161
|
+
# TODO make this smarter by including more than 1 paragraph if it's short, or by omitting headers
|
|
162
|
+
first_paragraph = /\A(.+?)(\n\n|\n |\z)/m.match(content.gsub(/\r/, ''))
|
|
163
|
+
GitHub::Markup.render("#{title}.markdown", first_paragraph[1]).html_safe
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def content_with_embedded_gists
|
|
167
|
+
# by using gist_id directly we can avoid hitting the Gist API every time the
|
|
168
|
+
# post is rendered.
|
|
169
|
+
|
|
170
|
+
return content.dup if gist_id.blank?
|
|
171
|
+
|
|
172
|
+
template = '<script src="https://gist.github.com/__ID__.js?file=__FILENAME__"></script>'
|
|
173
|
+
template['__ID__'] = gist_id.to_s
|
|
174
|
+
|
|
175
|
+
content.dup.tap do |result|
|
|
176
|
+
# process last example first, so that changes to result don't taint offsets
|
|
177
|
+
code_examples.reverse.each do |example|
|
|
178
|
+
result[example.offset] = template.sub(/__FILENAME__/, example.filename) + "\n"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def has_code_examples?
|
|
184
|
+
code_examples.length > 0
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def code_examples
|
|
188
|
+
Mist::CodeExampleParser.new(content).examples
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def load_code_examples_from_gist
|
|
192
|
+
if gist && gist.persisted?
|
|
193
|
+
self.content = self.content.dup.tap do |result|
|
|
194
|
+
code_examples.reverse.each do |example|
|
|
195
|
+
if gist.files[example.filename]
|
|
196
|
+
lines = gist.files[example.filename][:content].split("\n")
|
|
197
|
+
lines.unshift " file: #{example.filename}"
|
|
198
|
+
result[example.offset] = lines.join("\n ") + "\n"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Assigns the file contents of each file in the gist according to what's found in
|
|
206
|
+
# #content. Does not save the gist. Returns the list of files themselves.
|
|
207
|
+
def set_gist_data
|
|
208
|
+
gist.description = generated_gist_description
|
|
209
|
+
gist.files.keys.each { |filename| gist.files[filename] = nil }
|
|
210
|
+
code_examples.each do |example|
|
|
211
|
+
gist.files[example.filename] = { :content => example }
|
|
212
|
+
end
|
|
213
|
+
gist.files
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def url(options = {})
|
|
217
|
+
Mist::Engine.routes.url_helpers.post_path(id, options.reverse_merge(:only_path => false).reverse_merge(Rails.application.default_url_options))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def update_gist_if_necessary
|
|
221
|
+
return unless errors.empty?
|
|
222
|
+
|
|
223
|
+
if has_code_examples?
|
|
224
|
+
set_gist_data
|
|
225
|
+
|
|
226
|
+
if gist.changed?
|
|
227
|
+
errors.add(:gist, "could not be saved: #{gist.errors.full_messages.join('; ')}") unless gist.save
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
self.gist_id = gist.id
|
|
231
|
+
else
|
|
232
|
+
# no code examples, delete gist
|
|
233
|
+
gist.destroy if gist && gist.persisted?
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def destroy_gist
|
|
238
|
+
gist.destroy if gist && gist.persisted?
|
|
239
|
+
end
|
|
240
|
+
end
|