diff --git a/images/arrow-down.png b/images/arrow-down.png new file mode 100644 index 0000000..585b0bd Binary files /dev/null and b/images/arrow-down.png differ diff --git a/images/octocat-small.png b/images/octocat-small.png new file mode 100644 index 0000000..66c2539 Binary files /dev/null and b/images/octocat-small.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..ce1fd01 --- /dev/null +++ b/index.html @@ -0,0 +1,200 @@ + + + + + + GroupedScope by metaskills + + + + + + + + +
+
+

GroupedScope

+

Has Many Associations IN (GROUPS)

+ + + +

This project is maintained by metaskills

+ + +
+
+

Has Many Associations IN (GROUPS)

+ +

Jack Has Many Things

+ +

GroupedScope provides an easy way to group objects and to allow those groups to share association collections via existing has_many relationships. You may enjoy my original article titled Jack has_many :things.

+ +

Installation

+ +

Install the gem with bundler. We follow a semantic versioning format that tracks ActiveRecord's minor version. So this means to use the latest 3.2.x version of GroupedScope with any ActiveRecord 3.2 version.

+ +
gem 'grouped_scope', '~> 3.2.0'
+
+ +

Setup

+ +

To use GroupedScope on a model it must have a :group_id column.

+ +
class AddGroupId < ActiveRecord::Migration
+  def up
+    add_column :employees, :group_id, :integer
+  end
+  def down
+    remove_column :employees, :group_id
+  end
+end
+
+ +

General Usage

+ +

Assume the following model.

+ +
class Employee < ActiveRecord::Base
+  has_many :reports
+  grouped_scope :reports
+end
+
+ +

By calling grouped_scope on any association you create a new group accessor for each +instance. The object returned will act just like an array and at least include the +current object that called it.

+ +
@employee_one.group   # => [#<Employee id: 1, group_id: nil>]
+
+ +

To group resources, just assign the same :group_id to each record in that group.

+ +
@employee_one.update_attribute :group_id, 1
+@employee_two.update_attribute :group_id, 1
+@employee_one.group   # => [#<Employee id: 1, group_id: 1>, #<Employee id: 2, group_id: 1>]
+
+ +

Calling grouped_scope on the :reports association leaves the existing association intact.

+ +
@employee_one.reports  # => [#<Report id: 2, employee_id: 1>]
+@employee_two.reports  # => [#<Report id: 18, employee_id: 2>, #<Report id: 36, employee_id: 2>]
+
+ +

Now the good part, all associations passed to the grouped_scope method can be called +on the group proxy. The collection will return resources shared by the group.

+ +
@employee_one.group.reports # => [#<Report id: 2, employee_id: 1>, 
+                                  #<Report id: 18, employee_id: 2>, 
+                                  #<Report id: 36, employee_id: 2>]
+
+ +

You can even call scopes or association extensions defined on the objects in the collection +defined on the original association. For instance:

+ +
@employee.group.reports.urgent.assigned_to(user)
+
+ +

Advanced Usage

+ +

The group scoped object can respond to either blank? or present? which checks the group's +target group_id presence or not. We use this internally so that grouped scopes only use grouping +SQL when absolutely needed.

+ +
@employee_one = Employee.create :group_id => nil
+@employee_two = Employee.create :group_id => 38
+
+@employee_one.group.blank?   # => true
+@employee_two.group.present? # => true
+
+ +

The object returned by the #group method is an ActiveRecord relation on the targets class, +in this case Employee. Given this, you can further scope the grouped proxy if needed. Below, +we use the :email_present scope to refine the group down.

+ +
class Employee < ActiveRecord::Base
+  has_many :reports
+  grouped_scope :reports
+  scope :email_present, where("email IS NOT NULL")
+end
+
+@employee_one = Employee.create :group_id => 5, :name => 'Ken'
+@employee_two = Employee.create :group_id => 5, :name => 'MetaSkills', :email => 'ken@metaskills.net'
+
+# Only one employee is returned now.
+@employee_one.group.email_present # => [#<Employee id: 1, group_id: 5, name: 'MetaSkills', email: 'ken@metaskills.net']
+
+ +

We always use raw SQL to get the group ids vs. mapping them to an array and using those in scopes. +This means that large groups can avoid pushing down hundreds of keys in SQL form. So given an employee +with a group_id of 43 and calling @employee.group.reports, you would get something similar to +the following SQL.

+ +
SELECT "reports".* 
+FROM "reports"  
+WHERE "reports"."employee_id" IN (
+  SELECT "employees"."id" 
+  FROM "employees"  
+  WHERE "employees"."group_id" = 43
+)
+
+ +

You can pass the group scoped object as a predicate to ActiveRecord's relation interface. In past +versions, this would have treated the group object as an array of IDs. The new behavior is to return +a SQL literal to be used with IN statements. So note, the following would generate SQL similar to +the one above.

+ +
Employee.where(:group_id => @employee.group).all
+
+ +

If you need more control and you are working with the group at a lower level, you can always +use the #ids or #ids_sql methods on the group.

+ +
# Returns primary key array.
+@employee.group.ids # => [33, 58, 240]
+
+# Returns a Arel::Nodes::SqlLiteral object.
+@employee.group.ids_sql # => 'SELECT "employees"."id" FROM "employees"  WHERE "employees"."group_id" = 33'
+
+ +

Todo List

+ +

Testing

+ +

Simple! Just clone the repo, then run bundle install and bundle exec rake. The tests will begin to run. We also use Travis CI to run our tests too. Current build status is:

+ +

Build Status

+ +

License

+ +

Released under the MIT license. +Copyright (c) 2011 Ken Collins

+
+ +
+ + + + + + \ No newline at end of file diff --git a/javascripts/scale.fix.js b/javascripts/scale.fix.js new file mode 100644 index 0000000..08716c0 --- /dev/null +++ b/javascripts/scale.fix.js @@ -0,0 +1,20 @@ +fixScale = function(doc) { + + var addEvent = 'addEventListener', + type = 'gesturestart', + qsa = 'querySelectorAll', + scales = [1, 1], + meta = qsa in doc ? doc[qsa]('meta[name=viewport]') : []; + + function fix() { + meta.content = 'width=device-width,minimum-scale=' + scales[0] + ',maximum-scale=' + scales[1]; + doc.removeEventListener(type, fix, true); + } + + if ((meta = meta[meta.length - 1]) && addEvent in doc) { + fix(); + scales = [.25, 1.6]; + doc[addEvent](type, fix, true); + } + +}; \ No newline at end of file diff --git a/params.json b/params.json new file mode 100644 index 0000000..0c87212 --- /dev/null +++ b/params.json @@ -0,0 +1 @@ +{"note":"Don't delete this file! It's used internally to help with page regeneration.","name":"GroupedScope","google":"UA-34687749-1","tagline":"Has Many Associations IN (GROUPS)","body":"# Has Many Associations IN (GROUPS)\r\n\r\n\"Jack\r\n\r\nGroupedScope provides an easy way to group objects and to allow those groups to share association collections via existing `has_many` relationships. You may enjoy my original article titled [*Jack has_many :things*](http://metaskills.net/2008/09/28/jack-has_many-things/).\r\n\r\n\r\n## Installation\r\n\r\nInstall the gem with bundler. We follow a semantic versioning format that tracks ActiveRecord's minor version. So this means to use the latest 3.2.x version of GroupedScope with any ActiveRecord 3.2 version.\r\n\r\n```ruby\r\ngem 'grouped_scope', '~> 3.2.0'\r\n```\r\n\r\n\r\n## Setup\r\n\r\nTo use GroupedScope on a model it must have a `:group_id` column.\r\n\r\n```ruby\r\nclass AddGroupId < ActiveRecord::Migration\r\n def up\r\n add_column :employees, :group_id, :integer\r\n end\r\n def down\r\n remove_column :employees, :group_id\r\n end\r\nend\r\n```\r\n\r\n\r\n## General Usage\r\n\r\nAssume the following model.\r\n\r\n```ruby\r\nclass Employee < ActiveRecord::Base\r\n has_many :reports\r\n grouped_scope :reports\r\nend\r\n```\r\n\r\nBy calling grouped_scope on any association you create a new group accessor for each \r\ninstance. The object returned will act just like an array and at least include the \r\ncurrent object that called it.\r\n\r\n```ruby\r\n@employee_one.group # => [#]\r\n```\r\n\r\nTo group resources, just assign the same `:group_id` to each record in that group.\r\n\r\n```ruby\r\n@employee_one.update_attribute :group_id, 1\r\n@employee_two.update_attribute :group_id, 1\r\n@employee_one.group # => [#, #]\r\n```\r\n\r\nCalling grouped_scope on the :reports association leaves the existing association intact.\r\n\r\n```ruby\r\n@employee_one.reports # => [#]\r\n@employee_two.reports # => [#, #]\r\n```\r\n\r\nNow the good part, all associations passed to the grouped_scope method can be called \r\non the group proxy. The collection will return resources shared by the group.\r\n\r\n```ruby\r\n@employee_one.group.reports # => [#, \r\n #, \r\n #]\r\n```\r\n\r\nYou can even call scopes or association extensions defined on the objects in the collection\r\ndefined on the original association. For instance:\r\n\r\n```ruby\r\n@employee.group.reports.urgent.assigned_to(user)\r\n```\r\n\r\n\r\n## Advanced Usage\r\n\r\nThe group scoped object can respond to either `blank?` or `present?` which checks the group's \r\ntarget `group_id` presence or not. We use this internally so that grouped scopes only use grouping\r\nSQL when absolutely needed.\r\n\r\n```ruby\r\n@employee_one = Employee.create :group_id => nil\r\n@employee_two = Employee.create :group_id => 38\r\n\r\n@employee_one.group.blank? # => true\r\n@employee_two.group.present? # => true\r\n```\r\n\r\nThe object returned by the `#group` method is an ActiveRecord relation on the targets class, \r\nin this case `Employee`. Given this, you can further scope the grouped proxy if needed. Below,\r\nwe use the `:email_present` scope to refine the group down.\r\n\r\n```ruby\r\nclass Employee < ActiveRecord::Base\r\n has_many :reports\r\n grouped_scope :reports\r\n scope :email_present, where(\"email IS NOT NULL\")\r\nend\r\n\r\n@employee_one = Employee.create :group_id => 5, :name => 'Ken'\r\n@employee_two = Employee.create :group_id => 5, :name => 'MetaSkills', :email => 'ken@metaskills.net'\r\n\r\n# Only one employee is returned now.\r\n@employee_one.group.email_present # => [# @employee.group).all\r\n```\r\n\r\nIf you need more control and you are working with the group at a lower level, you can always \r\nuse the `#ids` or `#ids_sql` methods on the group.\r\n\r\n```ruby\r\n# Returns primary key array.\r\n@employee.group.ids # => [33, 58, 240]\r\n\r\n# Returns a Arel::Nodes::SqlLiteral object.\r\n@employee.group.ids_sql # => 'SELECT \"employees\".\"id\" FROM \"employees\" WHERE \"employees\".\"group_id\" = 33'\r\n```\r\n\r\n\r\n## Todo List\r\n\r\n* Raise errors for :finder_sql/:counter_sql.\r\n* Add a user definable group_id schema.\r\n* Remove SelfGrouping#with_relation, has not yet proved useful.\r\n\r\n\r\n\r\n## Testing\r\n\r\nSimple! Just clone the repo, then run `bundle install` and `bundle exec rake`. The tests will begin to run. We also use Travis CI to run our tests too. Current build status is:\r\n\r\n[![Build Status](https://secure.travis-ci.org/metaskills/grouped_scope.png)](http://travis-ci.org/metaskills/grouped_scope)\r\n\r\n\r\n\r\n## License\r\n\r\nReleased under the MIT license.\r\nCopyright (c) 2011 Ken Collins\r\n\r\n"} \ No newline at end of file diff --git a/stylesheets/pygment_trac.css b/stylesheets/pygment_trac.css new file mode 100644 index 0000000..c6a6452 --- /dev/null +++ b/stylesheets/pygment_trac.css @@ -0,0 +1,69 @@ +.highlight { background: #ffffff; } +.highlight .c { color: #999988; font-style: italic } /* Comment */ +.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ +.highlight .k { font-weight: bold } /* Keyword */ +.highlight .o { font-weight: bold } /* Operator */ +.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ +.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ +.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #aa0000 } /* Generic.Error */ +.highlight .gh { color: #999999 } /* Generic.Heading */ +.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ +.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #555555 } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */ +.highlight .gt { color: #aa0000 } /* Generic.Traceback */ +.highlight .kc { font-weight: bold } /* Keyword.Constant */ +.highlight .kd { font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ +.highlight .m { color: #009999 } /* Literal.Number */ +.highlight .s { color: #d14 } /* Literal.String */ +.highlight .na { color: #008080 } /* Name.Attribute */ +.highlight .nb { color: #0086B3 } /* Name.Builtin */ +.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ +.highlight .no { color: #008080 } /* Name.Constant */ +.highlight .ni { color: #800080 } /* Name.Entity */ +.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ +.highlight .nn { color: #555555 } /* Name.Namespace */ +.highlight .nt { color: #000080 } /* Name.Tag */ +.highlight .nv { color: #008080 } /* Name.Variable */ +.highlight .ow { font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mf { color: #009999 } /* Literal.Number.Float */ +.highlight .mh { color: #009999 } /* Literal.Number.Hex */ +.highlight .mi { color: #009999 } /* Literal.Number.Integer */ +.highlight .mo { color: #009999 } /* Literal.Number.Oct */ +.highlight .sb { color: #d14 } /* Literal.String.Backtick */ +.highlight .sc { color: #d14 } /* Literal.String.Char */ +.highlight .sd { color: #d14 } /* Literal.String.Doc */ +.highlight .s2 { color: #d14 } /* Literal.String.Double */ +.highlight .se { color: #d14 } /* Literal.String.Escape */ +.highlight .sh { color: #d14 } /* Literal.String.Heredoc */ +.highlight .si { color: #d14 } /* Literal.String.Interpol */ +.highlight .sx { color: #d14 } /* Literal.String.Other */ +.highlight .sr { color: #009926 } /* Literal.String.Regex */ +.highlight .s1 { color: #d14 } /* Literal.String.Single */ +.highlight .ss { color: #990073 } /* Literal.String.Symbol */ +.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #008080 } /* Name.Variable.Class */ +.highlight .vg { color: #008080 } /* Name.Variable.Global */ +.highlight .vi { color: #008080 } /* Name.Variable.Instance */ +.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ + +.type-csharp .highlight .k { color: #0000FF } +.type-csharp .highlight .kt { color: #0000FF } +.type-csharp .highlight .nf { color: #000000; font-weight: normal } +.type-csharp .highlight .nc { color: #2B91AF } +.type-csharp .highlight .nn { color: #000000 } +.type-csharp .highlight .s { color: #A31515 } +.type-csharp .highlight .sc { color: #A31515 } diff --git a/stylesheets/styles.css b/stylesheets/styles.css new file mode 100644 index 0000000..f14d9e4 --- /dev/null +++ b/stylesheets/styles.css @@ -0,0 +1,413 @@ +@import url(https://fonts.googleapis.com/css?family=Arvo:400,700,400italic); + +/* MeyerWeb Reset */ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font: inherit; + vertical-align: baseline; +} + + +/* Base text styles */ + +body { + padding:10px 50px 0 0; + font-family:"Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + color: #232323; + background-color: #FBFAF7; + margin: 0; + line-height: 1.8em; + -webkit-font-smoothing: antialiased; + +} + +h1, h2, h3, h4, h5, h6 { + color:#232323; + margin:36px 0 10px; +} + +p, ul, ol, table, dl { + margin:0 0 22px; +} + +h1, h2, h3 { + font-family: Arvo, Monaco, serif; + line-height:1.3; + font-weight: normal; +} + +h1,h2, h3 { + display: block; + border-bottom: 1px solid #ccc; + padding-bottom: 5px; +} + +h1 { + font-size: 30px; +} + +h2 { + font-size: 24px; +} + +h3 { + font-size: 18px; +} + +h4, h5, h6 { + font-family: Arvo, Monaco, serif; + font-weight: 700; +} + +a { + color:#C30000; + font-weight:200; + text-decoration:none; +} + +a:hover { + text-decoration: underline; +} + +a small { + font-size: 12px; +} + +em { + font-style: italic; +} + +strong { + font-weight:700; +} + +ul li { + list-style: inside; + padding-left: 25px; +} + +ol li { + list-style: decimal inside; + padding-left: 20px; +} + +blockquote { + margin: 0; + padding: 0 0 0 20px; + font-style: italic; +} + +dl, dt, dd, dl p { + font-color: #444; +} + +dl dt { + font-weight: bold; +} + +dl dd { + padding-left: 20px; + font-style: italic; +} + +dl p { + padding-left: 20px; + font-style: italic; +} + +hr { + border:0; + background:#ccc; + height:1px; + margin:0 0 24px; +} + +/* Images */ + +img { + position: relative; + margin: 0 auto; + max-width: 650px; + padding: 5px; + margin: 10px 0 32px 0; + border: 1px solid #ccc; +} + + +/* Code blocks */ + +code, pre { + font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; + color:#000; + font-size:14px; +} + +pre { + padding: 4px 12px; + background: #FDFEFB; + border-radius:4px; + border:1px solid #D7D8C8; + overflow: auto; + overflow-y: hidden; + margin-bottom: 32px; +} + + +/* Tables */ + +table { + width:100%; +} + +table { + border: 1px solid #ccc; + margin-bottom: 32px; + text-align: left; + } + +th { + font-family: 'Arvo', Helvetica, Arial, sans-serif; + font-size: 18px; + font-weight: normal; + padding: 10px; + background: #232323; + color: #FDFEFB; + } + +td { + padding: 10px; + background: #ccc; + } + + +/* Wrapper */ +.wrapper { + width:960px; +} + + +/* Header */ + +header { + background-color: #171717; + color: #FDFDFB; + width:170px; + float:left; + position:fixed; + border: 1px solid #000; + -webkit-border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + padding: 34px 25px 22px 50px; + margin: 30px 25px 0 0; + -webkit-font-smoothing: antialiased; +} + +p.header { + font-size: 16px; +} + +h1.header { + font-family: Arvo, sans-serif; + font-size: 30px; + font-weight: 300; + line-height: 1.3em; + border-bottom: none; + margin-top: 0; +} + + +h1.header, a.header, a.name, header a{ + color: #fff; +} + +a.header { + text-decoration: underline; +} + +a.name { + white-space: nowrap; +} + +header ul { + list-style:none; + padding:0; +} + +header li { + list-style-type: none; + width:132px; + height:15px; + margin-bottom: 12px; + line-height: 1em; + padding: 6px 6px 6px 7px; + + background: #AF0011; + background: -moz-linear-gradient(top, #AF0011 0%, #820011 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); + background: -webkit-linear-gradient(top, #AF0011 0%,#820011 100%); + background: -o-linear-gradient(top, #AF0011 0%,#820011 100%); + background: -ms-linear-gradient(top, #AF0011 0%,#820011 100%); + background: linear-gradient(top, #AF0011 0%,#820011 100%); + + border-radius:4px; + border:1px solid #0D0D0D; + + -webkit-box-shadow: inset 0px 1px 1px 0 rgba(233,2,38, 1); + box-shadow: inset 0px 1px 1px 0 rgba(233,2,38, 1); + +} + +header li:hover { + background: #C3001D; + background: -moz-linear-gradient(top, #C3001D 0%, #950119 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); + background: -webkit-linear-gradient(top, #C3001D 0%,#950119 100%); + background: -o-linear-gradient(top, #C3001D 0%,#950119 100%); + background: -ms-linear-gradient(top, #C3001D 0%,#950119 100%); + background: linear-gradient(top, #C3001D 0%,#950119 100%); +} + +a.buttons { + -webkit-font-smoothing: antialiased; + background: url(../images/arrow-down.png) no-repeat; + font-weight: normal; + text-shadow: rgba(0, 0, 0, 0.4) 0 -1px 0; + padding: 2px 2px 2px 22px; + height: 30px; +} + +a.github { + background: url(../images/octocat-small.png) no-repeat 1px; +} + +a.buttons:hover { + color: #fff; + text-decoration: none; +} + + +/* Section - for main page content */ + +section { + width:650px; + float:right; + padding-bottom:50px; +} + + +/* Footer */ + +footer { + width:170px; + float:left; + position:fixed; + bottom:10px; + padding-left: 50px; +} + +@media print, screen and (max-width: 960px) { + + div.wrapper { + width:auto; + margin:0; + } + + header, section, footer { + float:none; + position:static; + width:auto; + } + + footer { + border-top: 1px solid #ccc; + margin:0 84px 0 50px; + padding:0; + } + + header { + padding-right:320px; + } + + section { + padding:20px 84px 20px 50px; + margin:0 0 20px; + } + + header a small { + display:inline; + } + + header ul { + position:absolute; + right:130px; + top:84px; + } +} + +@media print, screen and (max-width: 720px) { + body { + word-wrap:break-word; + } + + header { + padding:10px 20px 0; + margin-right: 0; + } + + section { + padding:10px 0 10px 20px; + margin:0 0 30px; + } + + footer { + margin: 0 0 0 30px; + } + + header ul, header p.view { + position:static; + } +} + +@media print, screen and (max-width: 480px) { + + header ul li.download { + display:none; + } + + footer { + margin: 0 0 0 20px; + } + + footer a{ + display:block; + } + +} + +@media print { + body { + padding:0.4in; + font-size:12pt; + color:#444; + } +} \ No newline at end of file