{"id":13877927,"url":"https://github.com/brendon/positioning","last_synced_at":"2025-05-15T01:04:40.633Z","repository":{"id":221295551,"uuid":"753971485","full_name":"brendon/positioning","owner":"brendon","description":"Simple positioning for Active Record models.","archived":false,"fork":false,"pushed_at":"2024-12-03T23:58:27.000Z","size":171,"stargazers_count":274,"open_issues_count":2,"forks_count":16,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-04-06T21:05:16.758Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/brendon.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["brendon"]}},"created_at":"2024-02-07T06:19:04.000Z","updated_at":"2025-04-04T03:49:57.000Z","dependencies_parsed_at":"2024-02-25T09:32:53.450Z","dependency_job_id":"d86b43f8-7880-4bab-8ce9-d1cd6d98ec01","html_url":"https://github.com/brendon/positioning","commit_stats":{"total_commits":143,"total_committers":6,"mean_commits":"23.833333333333332","dds":0.05594405594405594,"last_synced_commit":"4ce54e4730f3a5cb19534a44665ef0a51dfc3156"},"previous_names":["brendon/positioning"],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendon%2Fpositioning","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendon%2Fpositioning/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendon%2Fpositioning/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendon%2Fpositioning/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brendon","download_url":"https://codeload.github.com/brendon/positioning/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248799693,"owners_count":21163398,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-06T08:01:35.116Z","updated_at":"2025-04-13T23:45:59.861Z","avatar_url":"https://github.com/brendon.png","language":"Ruby","readme":"# Positioning\n\nThe aim of this gem is to allow you to easily position Active Record model instances within a scope of your choosing. In an ideal world this gem will give your model instances sequential integer positions beginning with `1`. Attempts are made to make all changes within a transaction so that position integers remain consistent. To this end, directly assigning a position is discouraged, instead you can move items by declaring an item's prior or subsequent item in the list and your item will be moved to be relative to that item.\n\nPositioning supports multiple lists per model with global, simple, and complex scopes.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'positioning'\n```\n\nAnd then execute:\n\n    $ bundle install\n\nOr install it yourself as:\n\n    $ gem install positioning\n\n## Usage\n\nIn the simplest case our database column should be named `position` and not allow `NULL` as a value:\n\n`add_column :items, :position, :integer, null: false`\n\nYou should also add an index to ensure that the `position` column value is unique within its scope:\n\n`add_index :items, [:list_id, :position], unique: true`\n\nThe above assumes that your items are scoped to a parent table called `lists`.\n\nIf you have a polymorphic `belongs_to` then you'll want to add the type column to the index also:\n\n`add_index :items, [:listable_id, :listable_type, :position], unique: true`\n\nThe Positioning gem uses `0` and negative integers to rearrange the lists it manages so don't add database validations to restrict the usage of these. You are also restricted from using `0` and negative integers as position values. If you try, the position value will become `1`. If you try to set an explicit position value that is greater than the next available list position, it will be rounded down to that value.\n\n### Declaring Positioning\n\nTo declare that your model should keep track of the position of its records you can use the `positioned` method. Here are some examples:\n\n```ruby\n# The scope is global (all records will belong to the same list) and the database column\n# is 'position'\npositioned\n\n# The scope is on the belongs_to relationship 'list' and the database column is 'position'\n# We check if the scope is a belongs_to relationship and use its declared foreign_key as\n# the scope value. In this case it would be 'list_id' since we haven't overridden the\n# default foreign key.\nbelongs_to :list\npositioned on: :list\n\n# If you want to change the database column used to record positions you can do so via the\n# ':column' parameter. This is most useful when you are keeping track of more than one\n# list on a model.\nbelongs_to :list\nbelongs_to :category\npositioned on: :list\npositioned on: :category, column: :category_position\n\n# A scope need not be a belongs_to relationship; it can be any column in the database table.\npositioned on: :type\n\n# Finally, you can have more complex scopes defined as an array of relationships and/or\n# columns.\nbelongs_to :list\nbelongs_to :category\npositioned on: [:list, :category, :enabled]\n\n# If your belongs_to is polymorphic positioning will automatically add the type to the scope\nbelongs_to :listable, polymorphic: true\npositioned on: :listable\n```\n\n### Initialising a List\n\nIf you are adding `positioning` to a model with existing database records, or you're migrating from another gem like `acts_as_list` or `ranked-model` and have an existing position column, you will need to do some work to ensure you have well formed position values for your records. `positioning` has a helper method per `positioned` declaration that allows you to 'heal' the position column, ensuring that positions are positive integers starting at 1 with no gaps.\n\nFor example, in the usual case:\n\n```\nbelongs_to :list\npositioned on: :list\n```\n\nyou'll have a method called `heal_position_column!`. You can call this method and it will cycle through every existing scope combination in your database (every list with items in this case) and reset those items' position based on their current position order by default. You can pass in a custom order if you don't trust (or don't have) an existing order column. The custom order is passed through to the Active Record `reorder` method, so you can provide anything that that method accepts:\n\n```\nItem.heal_position_column! name: :desc\n```\n\nYou may need to introduce your database constraints after healing your position column:\n\n* We recommend a `null: false` constraint on the position column but if your existing column has `NULL` values, you'll need to fix those first. The heal method will heal `NULL` positions but depending on your database engine `NULL` positioned items might be placed at the start of the returned records or at the end (if positioning on the position column). Some databases allow this behaviour to be customised.\n* We also recommend a unique index on the scope columns and the position column. If you have repeated position integers per scope you'll need to use the heal method to fix these first before applying the unique index in a separate migration step.\n\nThe heal method name is named after the column used to store position values. By default this is `position` but if you override it then the method name will change:\n\n```\npositioned on: :category, column: :category_position\n```\n\nwill have a class method named `heal_category_position_column!`.\n\n### Manipulating Positioning\n\nThe tools for manipulating the position of records in your list have been kept intentionally terse. Priority has also been given to minimal pollution of the model namespace. Only two class methods are defined on all models (`positioning_columns` and `positioned`), and two instance methods are defined on models that call `positioned`:\n\n#### Accessing Relative List Items\n\nThe two instance methods that we add are for finding the prior and subsequent items relative to the current item in the list. These methods are named after the database column used to track positioning. By default the methods are named `prior_position` and `subsequent_position`. In the example above where we used the column `category_position` then the methods would be named `prior_category_position` and `subsequent_category_position`.\n\n#### Assigning Positions\n\nIf you don't provide a position when creating a record, your record will be added to the end of the list.\n\nTo assign a specific position when creating or updating a record you can simply declare a specific value for the database column tracking the position of records (by default this is `position`). The valid options for this column are:\n\n* A specific integer value as an `Integer` or a `String`. Values are automatically clamped to between `1` and the next available position at the end of the list (inclusive). You should use explicit position values as a last resort, instead you can use:\n* `:first` or `\"first\"` places the record at the start of the list.\n* `:last` or `\"last\"` places the record at the end of the list.\n* `nil` and `\"\"` also places the record at the end of the list.\n* `before:` and `after:` allow you to define the position relative to other records in the list. You can define the relative record by its primary key (usually `id`) or by providing the record itself. You can also provide `nil` or `\"\"` in which case the item will be placed at the start or end of the list (see below).\n\n**You can provide the position value as a JSON string and it will be decoded first. This could be useful if you have no other way to provide `before:` or `after:` as a hash (e.g. `\"{\\\"after\\\":33}\"`). See below for a technique to provide `before:` and `after:` using form helpers.**\n\nPosition parameters can be strings or symbols, so you can provide them from the browser.\n\nHere are some examples:\n\n##### Creating\n\n```ruby\n# Added to the third position, other records are moved out of the way\nlist.items.create name: 'Item', position: 3\n\n# Added to the end of the list\nlist.items.create name: 'Item'\nlist.items.create name: 'Item', position: :last\nlist.items.create name: 'Item', position: nil\nlist.items.create name: 'Item', position: {before: nil}\n\n# Added to the start of the list\nlist.items.create name: 'Item', position: :first\nlist.items.create name: 'Item', position: {after: nil}\n\n# Added before other_item\nlist.items.create name: 'Item', position: {before: other_item}\n# or\nother_item.id # =\u003e 22\nlist.items.create name: 'Item', position: {before: 22}\n\n# Added after other_item\nlist.items.create name: 'Item', position: {after: other_item}\n# or\nother_item.id # =\u003e 11\nlist.items.create name: 'Item', position: {after: 11}\n```\n\n##### Updating\n\n```ruby\n# Moved to the third position, other records are moved out of the way\nitem.update position: 3\n\n# Moved to the end of the list\nitem.update position: :last\nitem.update position: nil\nitem.update position: {before: nil}\n\n# Moved to the start of the list\nitem.update position: :first\nitem.update position: {after: nil}\n\n# Moved to before other_item\nitem.update position: {before: other_item}\n# or\nother_item.id # =\u003e 22\nitem.update position: {before: 22}\n\n# Moved to after other_item\nitem.update position: {after: other_item}\n# or\nother_item.id # =\u003e 11\nitem.update position: {after: 11}\n```\n\n##### Duplicating (`dup`)\n\nWhen you call `dup` on an instance in the list, all position columns on the duplicate will be set to `nil` so that when this duplicate is saved it will be added either to the end of the current scopes (if unchanged) or to the end of any new scopes. Of course you can then override the position of the duplicate before you save it if necessary.\n\n##### Relative Positioning in Forms\n\nIt can be tricky to provide the hash forms of relative positioning using Rails form helpers, but it is possible. We've declared a special `Struct` for you to use for this purpose.\n\nFirstly you need to allow both scalar and nested Strong Parameters for the `position` column like so:\n\n```ruby\ndef item_params\n  params.require(:item).permit :name, :position, { position: :before }\nend\n```\n\nIn the example above we're always declaring what item (by its `id`) we want to position our item **before**. You could change this to `:after` if you'd rather.\n\nNext, in your `new` method you may wish to initialise the `position` column with a value supplied by incoming parameters:\n\n```ruby\ndef new\n  item.position = { before: params[:before] }\nend\n```\n\nYou can now just pass the `before` parameter (the `id` of the item you want to add this record before) via the URL to the `new` action. For example: `items/new?before=22`.\n\nIn the form itself, so that your intended position survives a failed `create` attempt and form redisplay you can declare the `position` value like so:\n\n```\n  \u003c% if item.new_record? %\u003e\n    \u003c%= form.fields :position, model: Positioning::RelativePosition.new(item.position_before_type_cast) do |fields| %\u003e\n      \u003c%= fields.hidden_field :before %\u003e\n    \u003c% end %\u003e\n  \u003c% end %\u003e\n```\n\nThe key part here is `Positioning::RelativePosition.new(item.position_before_type_cast)`. `Positioning::RelativePosition` is a `Struct` that can take `before` and `after` as parameters. You should only provide one or the other. Because `position` is an `Integer` column, the hash structure is obliterated when it is assigned but we can still access it with `position_before_type_cast`. Remember to adjust the method if your position column has a different name (e.g. `category_position_before_type_cast`). The `Struct` provides the correct methods for `fields` to display the nested value.\n\n#### Destroying\n\nWhen a record is destroyed, the positions of relative items in the scope will be shuffled to close the gap left by the destroyed record. If we detect that records are being destroyed via a scope dependency (e.g. `has_many :items, dependent: :destroy`) then we skip closing the gaps because all records in the scope will eventually be destroyed anyway.\n\n#### Scopes\nPositioning handles things for you when you change the scope of a record. If you move a record from one scope to another, the gap in the position column will be healed in the scope the record is leaving, and by default (unless you specify an explicit position) the record will be added to the end of the list in the new scope.\n\nHere are some examples of scope management:\n\n```ruby\n# Moved to being the third item in other_list\nitem.update list: other_list, position: 3\n\n# Moved to the end of other_list\nitem.update list: other_list\nitem.update list: other_list, position: :last\nitem.update list: other_list, position: nil\nitem.update list: other_list, position: {before: nil}\n\n# Moved to the start of other_list\nitem.update list: other_list, position: :first\nitem.update list: other_list, position: {after: nil}\n\n# Moved to before other_item in other_list\nitem.update list: other_list, position: {before: other_item}\n# or\nother_item.id # =\u003e 22\nitem.update list: other_list, position: {before: 22}\n\n# Moved to after other_item in other_list\nitem.update list: other_list, position: {after: other_item}\n# or\nother_item.id # =\u003e 11\nitem.update list: other_list, position: {after: 11}\n```\n\nIt's important to note that in the examples above, `other_item` must already belong to the `other_list` scope.\n\n## Concurrency\n\nThe queries that this gem runs (especially those that seek the next position integer available) are vulnerable to race conditions. To this end, we lock the scope records to ensure that our model callbacks that determine and assign positions run sequentially. Previously we used an Advisory Lock for this purpose but this was difficult to test and a bit overkill in most situations. Where a scope doesn't exist, we lock all the records in the table.\n\n**Please Note SQLite Users:** Row locking isn't supported by SQLite. Since writes are non-concurrent by default, the worst you'll probably see are errors about the database being locked under high load.\n\nIf you have any concerns or improvements please file a GitHub issue.\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nThis gem is tested against SQLite, PostgreSQL and MySQL. The default database for testing is MySQL. You can target other databases by prepending the environment variable `DB=sqlite` or `DB=postgresql` before `rake test`. For example: `DB=sqlite rake test`.\n\nThe PostgreSQL and MySQL environments are configured under `test/support/database.yml`. You can edit this file, or preferably adjust your environment to support password-less socket based connections to these two database engines. You'll also need to manually create a database named `positioning_test` in each.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/brendon/positioning.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","funding_links":["https://github.com/sponsors/brendon"],"categories":["Ruby","ORM/ODM Extensions"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrendon%2Fpositioning","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrendon%2Fpositioning","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrendon%2Fpositioning/lists"}