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.
Files changed (147) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +13 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +192 -0
  7. data/README.md +108 -0
  8. data/Rakefile +9 -0
  9. data/app/assets/images/mist/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  10. data/app/assets/images/mist/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  11. data/app/assets/images/mist/ui-bg_flat_10_000000_40x100.png +0 -0
  12. data/app/assets/images/mist/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  13. data/app/assets/images/mist/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  14. data/app/assets/images/mist/ui-bg_glass_65_ffffff_1x400.png +0 -0
  15. data/app/assets/images/mist/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  16. data/app/assets/images/mist/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  17. data/app/assets/images/mist/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  18. data/app/assets/images/mist/ui-icons_222222_256x240.png +0 -0
  19. data/app/assets/images/mist/ui-icons_228ef1_256x240.png +0 -0
  20. data/app/assets/images/mist/ui-icons_ef8c08_256x240.png +0 -0
  21. data/app/assets/images/mist/ui-icons_ffd27a_256x240.png +0 -0
  22. data/app/assets/images/mist/ui-icons_ffffff_256x240.png +0 -0
  23. data/app/assets/javascripts/mist/jquery-ui-1.8.17.custom.min.js +356 -0
  24. data/app/assets/javascripts/mist/posts.js.coffee +3 -0
  25. data/app/assets/javascripts/mist_core.js +3 -0
  26. data/app/assets/stylesheets/mist/posts.css.sass +97 -0
  27. data/app/assets/stylesheets/mist/scaffolds.css.scss +56 -0
  28. data/app/assets/stylesheets/mist/ui-lightness/jquery-ui-1.8.17.custom.css +565 -0
  29. data/app/assets/stylesheets/mist_core.css +4 -0
  30. data/app/controllers/application_controller.rb +3 -0
  31. data/app/controllers/mist/posts_controller.rb +130 -0
  32. data/app/helpers/mist/posts_helper.rb +61 -0
  33. data/app/mailers/.gitkeep +0 -0
  34. data/app/models/.gitkeep +0 -0
  35. data/app/models/mist/post.rb +240 -0
  36. data/app/models/mist/post_sweeper.rb +63 -0
  37. data/app/views/layouts/mist/posts.html.erb +66 -0
  38. data/app/views/mist/posts/_form.html.erb +55 -0
  39. data/app/views/mist/posts/_post.html.erb +19 -0
  40. data/app/views/mist/posts/_sidebar_title.html.erb +5 -0
  41. data/app/views/mist/posts/edit.html.erb +16 -0
  42. data/app/views/mist/posts/feed.atom.builder +17 -0
  43. data/app/views/mist/posts/index.html.erb +31 -0
  44. data/app/views/mist/posts/new.html.erb +15 -0
  45. data/app/views/mist/posts/show.html.erb +3 -0
  46. data/config/cucumber.yml +8 -0
  47. data/config/environment.rb +1 -0
  48. data/config/routes.rb +7 -0
  49. data/features/atom_feed.feature +20 -0
  50. data/features/authorize_create_post.feature +14 -0
  51. data/features/authorize_destroy_post.feature +27 -0
  52. data/features/authorize_update_post.feature +28 -0
  53. data/features/authorize_view_unpublished.feature +22 -0
  54. data/features/create_post.feature +13 -0
  55. data/features/destroy_post.feature +13 -0
  56. data/features/format_with_markdown.feature +13 -0
  57. data/features/popular_posts.feature +35 -0
  58. data/features/recent_posts.feature +28 -0
  59. data/features/similar_posts.feature +43 -0
  60. data/features/step_definitions/authorization_steps.rb +17 -0
  61. data/features/step_definitions/filesystem_steps.rb +13 -0
  62. data/features/step_definitions/page_steps.rb +43 -0
  63. data/features/step_definitions/post_steps.rb +58 -0
  64. data/features/step_definitions/sidebar_steps.rb +21 -0
  65. data/features/support/env.rb +58 -0
  66. data/features/support/setup.rb +15 -0
  67. data/features/update_post.feature +19 -0
  68. data/gemfiles/common +20 -0
  69. data/gemfiles/rails-3.1.3 +4 -0
  70. data/gemfiles/rails-3.1.3.lock +195 -0
  71. data/gemfiles/rails-3.2.0 +4 -0
  72. data/lib/generators/mist/USAGE +13 -0
  73. data/lib/generators/mist/setup_generator.rb +21 -0
  74. data/lib/generators/mist/templates/initializer.rb +17 -0
  75. data/lib/generators/mist/templates/mist.css.scss +2 -0
  76. data/lib/generators/mist/templates/mist.js.coffee +1 -0
  77. data/lib/generators/mist/views_generator.rb +11 -0
  78. data/lib/mist.rb +26 -0
  79. data/lib/mist/code_example_parser.rb +86 -0
  80. data/lib/mist/configuration.rb +68 -0
  81. data/lib/mist/configuration/author.rb +8 -0
  82. data/lib/mist/engine.rb +12 -0
  83. data/lib/mist/git_file_system_history.rb +66 -0
  84. data/lib/mist/git_model.rb +154 -0
  85. data/lib/mist/git_model/attributes.rb +21 -0
  86. data/lib/mist/git_model/class_methods.rb +141 -0
  87. data/lib/mist/permalink.rb +5 -0
  88. data/lib/mist/repository.rb +15 -0
  89. data/lib/mist/version.rb +8 -0
  90. data/mist.gemspec +33 -0
  91. data/spec/controllers/posts_controller_spec.rb +263 -0
  92. data/spec/dummy_rails_app/app/assets/javascripts/mist.js.coffee +1 -0
  93. data/spec/dummy_rails_app/app/assets/stylesheets/mist.css.scss +2 -0
  94. data/spec/dummy_rails_app/app/views/layouts/mist/posts.html.erb +60 -0
  95. data/spec/dummy_rails_app/config.ru +4 -0
  96. data/spec/dummy_rails_app/config/application.rb +48 -0
  97. data/spec/dummy_rails_app/config/boot.rb +7 -0
  98. data/spec/dummy_rails_app/config/database.yml +28 -0
  99. data/spec/dummy_rails_app/config/environment.rb +5 -0
  100. data/spec/dummy_rails_app/config/environments/development.rb +32 -0
  101. data/spec/dummy_rails_app/config/environments/production.rb +60 -0
  102. data/spec/dummy_rails_app/config/environments/test.rb +42 -0
  103. data/spec/dummy_rails_app/config/initializers/active_gist_credentials.rb +8 -0
  104. data/spec/dummy_rails_app/config/initializers/backtrace_silencers.rb +7 -0
  105. data/spec/dummy_rails_app/config/initializers/inflections.rb +10 -0
  106. data/spec/dummy_rails_app/config/initializers/mime_types.rb +5 -0
  107. data/spec/dummy_rails_app/config/initializers/mist.rb +17 -0
  108. data/spec/dummy_rails_app/config/initializers/secret_token.rb +7 -0
  109. data/spec/dummy_rails_app/config/initializers/session_store.rb +8 -0
  110. data/spec/dummy_rails_app/config/initializers/wrap_parameters.rb +14 -0
  111. data/spec/dummy_rails_app/config/locales/en.yml +5 -0
  112. data/spec/dummy_rails_app/config/routes.rb +3 -0
  113. data/spec/dummy_rails_app/db/schema.rb +22 -0
  114. data/spec/dummy_rails_app/db/seeds.rb +7 -0
  115. data/spec/dummy_rails_app/doc/README_FOR_APP +2 -0
  116. data/spec/dummy_rails_app/lib/assets/.gitkeep +0 -0
  117. data/spec/dummy_rails_app/lib/tasks/.gitkeep +0 -0
  118. data/spec/dummy_rails_app/lib/tasks/cucumber.rake +65 -0
  119. data/spec/dummy_rails_app/public/404.html +26 -0
  120. data/spec/dummy_rails_app/public/422.html +26 -0
  121. data/spec/dummy_rails_app/public/500.html +26 -0
  122. data/spec/dummy_rails_app/public/favicon.ico +0 -0
  123. data/spec/dummy_rails_app/public/robots.txt +5 -0
  124. data/spec/dummy_rails_app/script/cucumber +10 -0
  125. data/spec/dummy_rails_app/script/rails +6 -0
  126. data/spec/dummy_rails_app/vendor/assets/stylesheets/.gitkeep +0 -0
  127. data/spec/dummy_rails_app/vendor/plugins/.gitkeep +0 -0
  128. data/spec/factories/posts.rb +8 -0
  129. data/spec/fixtures/gist_404 +6 -0
  130. data/spec/fixtures/gist_with_1_code_example +66 -0
  131. data/spec/helpers/posts_helper_spec.rb +64 -0
  132. data/spec/lib/mist/code_example_parser_spec.rb +135 -0
  133. data/spec/lib/mist/configuration_spec.rb +88 -0
  134. data/spec/lib/mist/permalink_spec.rb +17 -0
  135. data/spec/lib/mist/repository_spec.rb +20 -0
  136. data/spec/models/mist/git_model_spec.rb +260 -0
  137. data/spec/models/mist/post_spec.rb +575 -0
  138. data/spec/requests/posts_caching_spec.rb +152 -0
  139. data/spec/spec_helper.rb +20 -0
  140. data/spec/support/cache_helpers.rb +71 -0
  141. data/spec/support/config.rb +42 -0
  142. data/spec/support/fakeweb.rb +3 -0
  143. data/spec/views/mist/posts/edit.html.erb_spec.rb +11 -0
  144. data/spec/views/mist/posts/index.html.erb_spec.rb +27 -0
  145. data/spec/views/mist/posts/new.html.erb_spec.rb +15 -0
  146. data/spec/views/mist/posts/show.html.erb_spec.rb +11 -0
  147. metadata +371 -0
@@ -0,0 +1,4 @@
1
+ /*
2
+ *= require_self
3
+ *= require_tree ./mist
4
+ */
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
3
+
@@ -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
+ '&nbsp;&bull;&nbsp;'.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
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