mist 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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