diff --git a/_sass/2017/components/related-posts.scss b/_sass/2017/components/related-post-list.scss
similarity index 57%
rename from _sass/2017/components/related-posts.scss
rename to _sass/2017/components/related-post-list.scss
index b828fabd2..e889bb391 100644
--- a/_sass/2017/components/related-posts.scss
+++ b/_sass/2017/components/related-post-list.scss
@@ -1,21 +1,3 @@
-/*
- * Related posts
- */
-
-.related-posts {
- & {
- @include section-with-container;
- padding-top: 16px;
- padding-bottom: 16px;
- background: $gray-bg;
- }
-
- @media (min-width: 481px) {
- padding-top: 64px;
- padding-bottom: 64px;
- }
-}
-
.related-post-list {
&,
& > li {
diff --git a/_sass/2017/components/related-posts-area.scss b/_sass/2017/components/related-posts-area.scss
new file mode 100644
index 000000000..4a61127ea
--- /dev/null
+++ b/_sass/2017/components/related-posts-area.scss
@@ -0,0 +1,17 @@
+/*
+ * Related posts
+ */
+
+.related-posts-area {
+ & {
+ @include section-with-container;
+ padding-top: 16px;
+ padding-bottom: 16px;
+ background: $gray-bg;
+ }
+
+ @media (min-width: 481px) {
+ padding-top: 64px;
+ padding-bottom: 64px;
+ }
+}
diff --git a/_sass/2017/components/related-posts-section.scss b/_sass/2017/components/related-posts-section.scss
index a6d09ff09..dd42b85d7 100644
--- a/_sass/2017/components/related-posts-section.scss
+++ b/_sass/2017/components/related-posts-section.scss
@@ -6,13 +6,15 @@
.related-posts-section {
& {
display: flex;
- margin-left: -16px;
- margin-right: -16px;
+ @include section-gutter(margin-left, $multiplier: -1);
+ @include section-gutter(margin-right, $multiplier: -1);
}
& > .callout,
& > .group {
- margin: 0 16px;
+ margin: 0;
+ @include section-gutter(margin-left);
+ @include section-gutter(margin-right);
}
& > .callout {
@@ -39,8 +41,8 @@
& > .callout,
& > .group {
- margin-top: 16px;
- margin-bottom: 16px;
+ @include section-gutter(margin-top);
+ @include section-gutter(margin-bottom);
flex: 1 1 100%;
}
}
@@ -54,8 +56,8 @@
& > .callout,
& > .group {
- margin-top: 16px;
- margin-bottom: 16px;
+ @include section-gutter(margin-top);
+ @include section-gutter(margin-bottom);
flex: 1 1 100%;
}
diff --git a/_sass/2017/style.scss b/_sass/2017/style.scss
index 946b982f9..653cbfe2d 100644
--- a/_sass/2017/style.scss
+++ b/_sass/2017/style.scss
@@ -32,10 +32,11 @@
@import './components/pages-list';
@import './components/pre-footer';
@import './components/push-button';
-@import './components/related-posts';
+@import './components/related-posts-area';
@import './components/related-posts-callout';
@import './components/related-posts-group';
@import './components/related-posts-section';
+@import './components/related-post-list';
@import './components/related-post-item';
@import './components/search-box';
@import './components/search-footer';
diff --git a/rails-models.md b/rails-models.md
index 3ac3e5458..98faf3c56 100644
--- a/rails-models.md
+++ b/rails-models.md
@@ -1,112 +1,161 @@
---
-title: Models
+title: Rails models
category: Rails
+layout: 2017/sheet
---
-### [QueryMethods](http://devdocs.io/rails/activerecord/querymethods)
+Generating
+----------
-```rb
+### Generating
+
+ $ rails g model User
+
+Using models
+------------
+
+### Query methods
+
+```ruby
items = Model
.where(first_name: 'Harvey')
.where('id = 3')
.where('id = ?', 3)
+```
+```ruby
.order(:title)
.order(title: :desc)
.order("title DESC")
+```
+```ruby
.reorder(:title) # discards other .order's
.rewhere(...) # discards other .where's
+```
+```ruby
.limit(2)
.offset(1)
.uniq
```
-Advanced:
+See: [QueryMethods](http://devdocs.io/rails/activerecord/querymethods)
-```rb
+### Advanced query methods
+
+```ruby
+items = Model
.select(:id)
.select([:id, :name])
+```
+```ruby
.group(:name) # GROUP BY name
.group('name AS grouped_name, age')
.having('SUM(price) > 30') # needs to be chained with .group
-
- .includes(:user)
- .includes(user: [:articles])
-
- .references(:posts)
- # .where("posts.name = 'foo'").references(:posts)
```
-### [FinderMethods](http://devdocs.io/rails/activerecord/findermethods)
+```ruby
+ .includes(:user)
+ .includes(user: [:articles])
+```
-```rb
+```ruby
+ .references(:posts)
+ # aka: .where("posts.name = 'foo'").references(:posts)
+```
+
+### Finder methods
+
+```ruby
item = Model.find(id)
item = Model.find_by_email(email)
item = Model.where(email: email).first
+```
+```ruby
Model
.exists?(5)
.exists?(name: "David")
+```
+```ruby
.first
.last
.find_nth(4, [offset])
```
-### [Persistence](http://devdocs.io/rails/activerecord/persistence)
+See: [FinderMethods](http://devdocs.io/rails/activerecord/findermethods)
-```rb
+### Persistence
+
+```ruby
item.new_record?
item.persisted?
item.destroyed?
item.serialize_hash
+```
+```ruby
item.save
item.save! # Same as above, but raises an Exception
+```
+```ruby
item.update name: 'John' # Saves immediately
item.update! name: 'John'
+```
-
+```ruby
item.update_column :name, 'John' # skips validations and callbacks
item.update_columns name: 'John'
item.update_columns! name: 'John'
+```
+```ruby
item.touch # updates :updated_at
item.touch :published_at
+```
+```ruby
item.destroy
item.delete # skips callbacks
```
-```rb
+```ruby
Model.create # Same an #new then #save
Model.create! # Same as above, but raises an Exception
```
-### [AttributeAssignment](http://devdocs.io/rails/activerecord/attributeassignment)
+See: [Persistence](http://devdocs.io/rails/activerecord/persistence)
-```rb
+### Attribute Assignment
+
+```ruby
item.attributes # #
+```
+```ruby
item.attributes = { name: 'John' } # Merges attributes in. Doesn't save.
item.assign_attributes name: 'John' # Same as above
```
-### [Dirty](http://devdocs.io/rails/activemodel/dirty)
+See: [AttributeAssignment](http://devdocs.io/rails/activerecord/attributeassignment)
-```rb
+### Dirty
+
+```ruby
item.changed?
item.changed # ['name']
item.changed_attributes # { 'name' => 'Bob' } - original values
item.changes # { 'name' => ['Bob', 'Robert'] }
item.previous_changes # available after #save
item.restore_attributes
+```
+```ruby
item.name = 'Robert'
item.name_was # 'Bob'
item.name_change # [ 'Bob', 'Robert' ]
@@ -114,224 +163,318 @@ item.name_changed? # true
item.name_changed?(from: 'Bob', to: 'Robert')
```
-### [Validations](http://devdocs.io/rails/activerecord/validations)
+See: [Dirty](http://devdocs.io/rails/activemodel/dirty)
-```rb
+### Validations
+
+```ruby
item.valid?
item.invalid?
```
-### [Calculations](http://devdocs.io/rails/activerecord/calculations)
+See: [Validations](http://devdocs.io/rails/activerecord/validations)
-```rb
+### Calculations
+
+```ruby
Person.count
Person.count(:age) # counts non-nil's
+```
+```ruby
Person.average(:age)
Person.maximum(:age)
Person.minimum(:age)
Person.sum('2 * age')
+```
+```ruby
Person.calculate(:count, :all)
```
Advanced:
-```
+```ruby
Person.distinct.count
Person.group(:city).count
```
-Generating
-----------
+See: [Calculations](http://devdocs.io/rails/activerecord/calculations)
- $ rails g model User
+### Dynamic attribute-based finders
+
+Given a field called `name`:
+{: .-setup}
+
+```ruby
+# Returns one record
+Person.find_by_name(name)
+Person.find_last_by_name(name)
+Person.find_or_create_by_name(name)
+Person.find_or_initialize_by_name(name)
+```
+
+```ruby
+# Returns a list of records
+Person.find_all_by_name(name)
+```
+
+```ruby
+# Add a bang to make it raise an exception
+Person.find_by_name!(name)
+```
+
+```ruby
+# You may use `scoped` instead of `find`
+Person.scoped_by_user_name
+```
Associations
------------
- belongs_to
- has_one
- has_many
- has_many :through
- has_one :through
- has_and_belongs_to_many
+### Associations
+
+- `belongs_to`
+- `has_one`
+- `has_many`
+- `has_many :through`
+- `has_one :through`
+- `has_and_belongs_to_many`
### Has many
- belongs_to :parent, :foreign_key => 'parent_id' class_name: 'Folder'
- has_many :folders, :foreign_key => 'parent_id', class_name: 'Folder'
+```ruby
+belongs_to :parent, :foreign_key => 'parent_id' class_name: 'Folder'
+has_many :folders, :foreign_key => 'parent_id', class_name: 'Folder'
- has_many :comments, :order => "posted_on"
- has_many :comments, :include => :author
- has_many :people, :class_name => "Person"
- has_many :people, :conditions => "deleted = 0"
- has_many :tracks, :order => "position"
- has_many :comments, :dependent => :nullify
- has_many :comments, :dependent => :destroy
- has_many :tags, :as => :taggable
- has_many :reports, :readonly => true
- has_many :subscribers, :through => :subscriptions, class_name: "User", :source => :user
- has_many :subscribers, :finder_sql =>
- 'SELECT DISTINCT people.* ' +
- 'FROM people p, post_subscriptions ps ' +
- 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
- 'ORDER BY p.first_name'
+has_many :comments, :order => "posted_on"
+has_many :comments, :include => :author
+has_many :people, :class_name => "Person"
+has_many :people, :conditions => "deleted = 0"
+has_many :tracks, :order => "position"
+has_many :comments, :dependent => :nullify
+has_many :comments, :dependent => :destroy
+has_many :tags, :as => :taggable
+has_many :reports, :readonly => true
+has_many :subscribers, :through => :subscriptions, class_name: "User", :source => :user
+has_many :subscribers, :finder_sql =>
+ 'SELECT DISTINCT people.* ' +
+ 'FROM people p, post_subscriptions ps ' +
+ 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
+ 'ORDER BY p.first_name'
+```
### belongs to
- belongs_to :author,
- :dependent => :destroy # or :delete
+```ruby
+belongs_to :author,
+ :dependent => :destroy # or :delete
- :class_name => "Person"
- :select => "*"
- :counter_cache => true
- :counter_cache => :custom_counter
- :include => "Book"
- :readonly => true
+ :class_name => "Person"
+ :select => "*"
+ :counter_cache => true
+ :counter_cache => :custom_counter
+ :include => "Book"
+ :readonly => true
- :conditions => 'published = true'
+ :conditions => 'published = true'
- :touch => true
- :touch => :authors_last_updated_at
+ :touch => true
+ :touch => :authors_last_updated_at
- :primary_key => "name"
- :foreign_key => "author_name"
+ :primary_key => "name"
+ :foreign_key => "author_name"
+```
### Many-to-many
If you have a join model:
+{: .-setup}
- class Programmer < ActiveRecord::Base
- has_many :assignments
- has_many :projects, :through => :assignments
- end
+```ruby
+class Programmer < ActiveRecord::Base
+ has_many :assignments
+ has_many :projects, :through => :assignments
+end
+```
+{: data-line="2,3"}
- class Project < ActiveRecord::Base
- has_many :assignments
- has_many :programmers, :through => :assignments
- end
+```ruby
+class Project < ActiveRecord::Base
+ has_many :assignments
+ has_many :programmers, :through => :assignments
+end
+```
+{: data-line="2,3"}
- class Assignment
- belongs_to :project
- belongs_to :programmer
- end
+```ruby
+class Assignment
+ belongs_to :project
+ belongs_to :programmer
+end
+```
+{: data-line="2,3"}
-Or HABTM:
+### Many-to-many (HABTM)
- has_and_belongs_to_many :projects
- has_and_belongs_to_many :projects, :include => [ :milestones, :manager ]
- has_and_belongs_to_many :nations, :class_name => "Country"
- has_and_belongs_to_many :categories, :join_table => "prods_cats"
- has_and_belongs_to_many :categories, :readonly => true
- has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
- "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}"
+```ruby
+has_and_belongs_to_many :projects
+has_and_belongs_to_many :projects, :include => [ :milestones, :manager ]
+has_and_belongs_to_many :nations, :class_name => "Country"
+has_and_belongs_to_many :categories, :join_table => "prods_cats"
+has_and_belongs_to_many :categories, :readonly => true
+has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
+"DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}"
+```
### Polymorphic associations
- class Post
- has_many :attachments, :as => :parent
- end
+```ruby
+class Post
+ has_many :attachments, as: :parent
+end
+```
+{: data-line="2"}
- class Image
- belongs_to :parent, :polymorphic => true
- end
+```ruby
+class Image
+ belongs_to :parent, polymorphic: true
+end
+```
+{: data-line="2"}
And in migrations:
- create_table :images do |t|
- t.references :post, :polymorphic => true
- end
-
-### Dynamic attribute-based finders
-
- # Returns one record
- Person.find_by_name(name)
- Person.find_last_by_name(name)
- Person.find_or_create_by_name(name)
- Person.find_or_initialize_by_name(name)
-
- # Returns a list of recordns
- Person.find_all_by_name(name)
-
- # Add a bang to make it raise an exception
- Person.find_by_name!(name)
-
- # You may use `scoped` instead of `find`
- Person.scoped_by_user_name
+```ruby
+create_table :images do |t|
+ t.references :post, polymorphic: true
+end
+```
+{: data-line="2"}
Validation
----------
- class Person < ActiveRecord::Base
+### Validation
- validates :name, presence: true
+```ruby
+class Person < ActiveRecord::Base
+```
+{:.-setup}
- validates :terms, acceptance: true
+```ruby
+ # Presence
+ validates :name, presence: true
+```
+{: data-line="2"}
- validates :email, confirmation: true
+```ruby
+ # Acceptance
+ validates :terms, acceptance: true
+```
- validates :slug, uniqueness: true
- validates :slug, uniqueness: { case_sensitive: false }
- validates :holiday, uniqueness: { scope: :year, :message => "only once a year" }
+```ruby
+ # Confirm
+ validates :email, confirmation: true
+```
- validates :code, format: /regex/
- validates :code, format: { with: /regex/ }
+```ruby
+ # Unique
+ validates :slug, uniqueness: true
+ validates :slug, uniqueness: { case_sensitive: false }
+ validates :holiday, uniqueness: { scope: :year, message: 'yearly only' }
+```
- validates :name, length: { minimum: 2 }
- validates :bio, length: { maximum: 500 }
- validates :password, length: { in: => 6..20 }
- validates :number, length: { is: => 6 }
+```ruby
+ # Format
+ validates :code, format: /regex/
+ validates :code, format: { with: /regex/ }
+```
- validates :gender, inclusion: %w(male female)
- validates :gender, inclusion: { in: %w(male female) }
- validates :lol, exclusion: %w(xyz)
+```ruby
+ # Length
+ validates :name, length: { minimum: 2 }
+ validates :bio, length: { maximum: 500 }
+ validates :password, length: { in: => 6..20 }
+ validates :number, length: { is: => 6 }
+```
- validates :points, numericality: true
- validates :played, numericality: { only_integer: true }
- # ... greater_than, greater_than_or_equal_to,
- # ... less_than, less_than_or_equal_to
- # ... odd, even, equal_to
+```ruby
+ # Include/exclude
+ validates :gender, inclusion: %w(male female)
+ validates :gender, inclusion: { in: %w(male female) }
+ validates :lol, exclusion: %w(xyz)
+```
- # Validate the associated records to ensure they're valid as well
- has_many :books
- validates_associated :books
+```ruby
+ # Numeric
+ validates :points, numericality: true
+ validates :played, numericality: { only_integer: true }
+ # ... greater_than, greater_than_or_equal_to,
+ # ... less_than, less_than_or_equal_to
+ # ... odd, even, equal_to
+```
- # Length (full enchalada)
- validates :content, length: {
- minimum: 300,
- maximum: 400,
- tokenizer: lambda { |str| str.scan(/\w+/) },
- too_short: "must have at least %{count} words",
- too_long: "must have at most %{count} words" }
+```ruby
+ # Validate the associated records to ensure they're valid as well
+ has_many :books
+ validates_associated :books
+```
- # Multiple
- validates :login, :email, presence: true
+```ruby
+ # Length (full options)
+ validates :content, length: {
+ minimum: 300,
+ maximum: 400,
+ tokenizer: lambda { |str| str.scan(/\w+/) },
+ too_short: "must have at least %{count} words",
+ too_long: "must have at most %{count} words" }
+```
- # Conditional
- validates :description, presence: true, if: :published?
- validates :description, presence: true, if: lambda { |obj| .. }
+```ruby
+ # Multiple
+ validates :login, :email, presence: true
+```
- validates :title, presence: true, on: :save # :save | :create | :update
- end
+```ruby
+ # Conditional
+ validates :description, presence: true, if: :published?
+ validates :description, presence: true, if: lambda { |obj| .. }
+```
+
+```ruby
+ validates :title, presence: true, on: :save # :save | :create | :update
+```
+
+```ruby
+end
+```
+{: .-setup}
### Custom validations
- class Person < ActiveRecord::Base
- validate :foo_cant_be_nil
+```ruby
+class Person < ActiveRecord::Base
+ validate :foo_cant_be_nil
- def foo_cant_be_nil
- errors.add(:foo, 'cant be nil') if foo.nil?
- end
- end
+ def foo_cant_be_nil
+ errors.add(:foo, 'cant be nil') if foo.nil?
+ end
+end
+```
+{: data-line="2"}
### Errors
- record.errors.valid? #=> false
- record.errors #=> { :name => ["can't be blank"] }
- record.errors.messages #=> { :name => ["can't be blank"] }
+```ruby
+record.errors.valid? # → false
+record.errors # → { :name => ["can't be blank"] }
+record.errors.messages # → { :name => ["can't be blank"] }
+```
- record.errors[:name].any?
+```ruby
+record.errors[:name].any?
+```
Other API
---------
@@ -342,70 +485,109 @@ Other API
### Mass updates
- # Updates person id 15
- Person.update 15, name: "John", age: 24
- Person.update [1,2], [{name: "John"}, {name: "foo"}]
+ ```ruby
+# Updates person id 15
+Person.update 15, name: "John", age: 24
+Person.update [1,2], [{name: "John"}, {name: "foo"}]
+```
### Joining
- Student.joins(:schools).where(schools: { type: 'public' })
- Student.joins(:schools).where('schools.type' => 'public' )
+```ruby
+# Basic joins
+Student.joins(:schools).where(schools: { type: 'public' })
+Student.joins(:schools).where('schools.type' => 'public' )
+```
- # multiple associations
- Article.joins(:category, :comments)
+```ruby
+# Multiple associations
+Article.joins(:category, :comments)
+```
- # nested assocations
- Article.joins(comments: :guest)
+```ruby
+# Nested assocations
+Article.joins(comments: :guest)
+```
- # sql
- Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'")
+```ruby
+# SQL
+Author.joins(
+ 'INNER JOIN posts ' +
+ 'ON posts.author_id = authors.id ' +
+ 'AND posts.published = "t"'
+)
+```
### Where interpolation
- where("name = ?", "John")
- where(["name = :name", { name: "John" }])
+```ruby
+where('name = ?', 'John')
+where(['name = :name', { name: 'John' }])
+```
### Serialize
- class User < ActiveRecord::Base
- serialize :preferences
- end
+```ruby
+class User < ActiveRecord::Base
+ serialize :preferences
+end
+```
+{: data-line="2"}
- user = User.create(:preferences => { "background" => "black", "display" => large })
+```ruby
+user = User.create(
+ preferences: {
+ 'background' => 'black',
+ 'display' => 'large'
+ }
+)
+```
You can also specify a class option as the second parameter that’ll raise an
exception if a serialized object is retrieved as a descendant of a class not in
the hierarchy.
- class User < ActiveRecord::Base
- serialize :preferences, Hash
- end
+```ruby
+# Only Hash allowed!
+class User < ActiveRecord::Base
+ serialize :preferences, Hash
+end
+```
+{: data-line="3"}
- user = User.create(:preferences => %w( one two three ))
- User.find(user.id).preferences # raises SerializationTypeMismatch
+```ruby
+# Reading it raises SerializationTypeMismatch
+user = User.create(preferences: %w(one two three))
+User.find(user.id).preferences
+```
-Overriding accessors
---------------------
+Other tricks
+------------
- class Song < ActiveRecord::Base
- # Uses an integer of seconds to hold the length of the song
+### Overriding accessors
- def length=(minutes)
- write_attribute(:length, minutes.to_i * 60)
- end
+```ruby
+class Song < ActiveRecord::Base
+ # Uses an integer of seconds to hold the length of the song
- def length
- read_attribute(:length) / 60
- end
- end
+ def length=(minutes)
+ write_attribute(:length, minutes.to_i * 60)
+ end
- * http://api.rubyonrails.org/classes/ActiveRecord/Base.html
+ def length
+ read_attribute(:length) / 60
+ end
+end
+```
+{: data-line="4,8"}
+
+See:
Callbacks
---------
- after_create
- after_initialize
- after_validation
- after_save
- after_commit
+- after_create
+- after_initialize
+- after_validation
+- after_save
+- after_commit