Skip to content

MONGOID-5128 Scoped associations #5017

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Sep 23, 2021

Conversation

johnnyshields
Copy link
Contributor

@johnnyshields johnnyshields commented Jul 26, 2021

Ready for review

Fixes MONGOID-5128

ActiveRecord allows you to do this:

class Author < ApplicationRecord
  has_many :books, -> { where(processed: true) }
end

I'm proposing equivalent syntax in Mongoid using :scope keyword arg.
If you prefer I could match the Rails syntax exactly, but I thought this was a bit clearer:

class Author
  has_many :books, scope: -> { where(processed: true) }
end

In my implementation you can also use a Symbol to access a named scope:

class Trainer
  has_many :pokemons, scope: :grass_type # equivalent to -> { grass_type }
end

I have some planned enhancements to this functionality but this PR is a good "minimum mergeable spec".


See:

@pooooodles pooooodles added the tracked-in-jira Ticket filed in Mongo's Jira system label Jul 26, 2021
@pooooodles
Copy link

@pooooodles pooooodles changed the title Add :scope parameter to associations MONGOID-5128: Add :scope parameter to associations Jul 26, 2021
@johnnyshields
Copy link
Contributor Author

@agolin95 @p-mongo it would be great to get a code review on this when you have time. Please let me know if I'm headed in the right direction here.

@alcaeus
Copy link
Member

alcaeus commented Jul 27, 2021

@johnnyshields I've asked to take @comandeo to review this while @p-mongo is on well-deserved PTO. We'll get back to you once we've had a chance to take a look.

In the meantime, I've authorised the Evergreen patch so you may see some build results.

@alcaeus alcaeus requested a review from comandeo July 27, 2021 11:56
@johnnyshields
Copy link
Contributor Author

OK, great. Yes, Evergreen being actually 🟢 green would be really helpful.

@johnnyshields
Copy link
Contributor Author

johnnyshields commented Jul 27, 2021

Hmmm... so PR is progressing well, however, I'm running into a bit of a wall the more I think about my use case.

I'd like to do be able to define multiple scopes for the same association:

class Game
  has_many :players
  has_many :active_players, scope: ->{ is_active: true }, class_name: 'Player', foreign_key: :game_id, inverse_of: nil
end

This adds a lot of boilerplate to achieve what I want and feels hacky. There's got to be a better way.

My first thought was to do it as an (extension)[https://docs.mongodb.com/mongoid/current/tutorials/mongoid-relations/#extensions], i.e.:

class Game
  has_many :players do
    def active
      where(is_active: true)
    end
  end
end

However, this won't work with eager loading. Perhaps it could be defined as a scope such as:

  has_many :players do
    scope :active_players, ->{ is_active: true }    # Option A
  end

And do eager loading by callling Event.games.includes(:active_players)

Alternatively, we could do:

class Game
  has_many :players
  has_many :active_players, extends: :players, scope: ->{ is_active: true }   # Option B
  
  # or
  association_scope :active_players, :players, ->{ is_active: true }   # Option C
end

Option C association_scope may be the best. Perhaps Option A could be added as alternate syntax for it. It may actually make sense to introduce a new relationship class, perhaps Mongoid::Association::Scoped? Ideally these association_scopes are "read-only", or any assignment would be delegated to the underlying association.

@johnnyshields
Copy link
Contributor Author

johnnyshields commented Jul 28, 2021

I've implemented association_scope (Option C) from above. It's pretty cool!

@johnnyshields johnnyshields changed the title MONGOID-5128: Add :scope parameter to associations MONGOID-5128: Scope associations Jul 28, 2021
@johnnyshields johnnyshields changed the title MONGOID-5128: Scope associations MONGOID-5128: Scoped associations Jul 28, 2021
@johnnyshields
Copy link
Contributor Author

@p-mongo would like to get your comments on this.

@p-mongo
Copy link
Contributor

p-mongo commented Aug 3, 2021

I support implementing the AR-compatible syntax for defining the scope criteria.

Mongoid (and I believe AR too) already provides scoping under associations:

class Foo
  include Mongoid::Document
  
  has_many :bars
end

class Bar
  include Mongoid::Document

  scope :open, -> { where(open: true) }
end

irb(main):012:0> Foo.create!
=> #<Foo _id: 6109526fa15d5d20c601e23d, >
irb(main):013:0> Foo.first.bars
=> []
irb(main):014:0> Foo.first.bars.open
=> 
#<Mongoid::Criteria
  selector: {"_id"=>BSON::ObjectId('6109526fa15d5d20c601e23d'), "open"=>true}
  options:  {}
  class:    Bar
  embedded: false>

@johnnyshields
Copy link
Contributor Author

johnnyshields commented Aug 3, 2021

Mongoid (and I believe AR too) already provides scoping under associations

Yes, BUT there is one important point. You can't eager load scoped associations.

In other words:

# you can't do this
Foo.all.includes(:"bars.open")

# my PR allows this
class Foo
  ...
  association_scope :open_bars, :bars, -> { where(open: true) }
end

Foo.all.includes(:open_bars)

That being said I'm fine to move association_scope to a separate PR, or implement it as a gem. Will do the :scope parameter first.

@p-mongo
Copy link
Contributor

p-mongo commented Aug 3, 2021

I believe that pattern is already possible to implement in Mongoid applications simply by defining additional associations for each needed scope and setting class_name appropriately.

@johnnyshields
Copy link
Contributor Author

Hmmm.... I'm not following. Can you provide a code (or pseudo-code) example?

@johnnyshields
Copy link
Contributor Author

@p-mongo this is now ready for review. I've dropped the association_scope function, can be considered in the future after more investigation.

@johnnyshields johnnyshields changed the title MONGOID-5128: Scoped associations MONGOID-5128: [Ready for review] Scoped associations Aug 15, 2021
@p-mongo p-mongo force-pushed the scoped-associations branch from 29c74b4 to c4e173d Compare August 25, 2021 01:09
end

let(:object) do
BSON::ObjectId.new
end

before do
expect(Person).to receive(:where).with(association.primary_key => object).and_call_original
expect_any_instance_of(Mongoid::Criteria).to receive(:where).with(association.primary_key => object).and_call_original
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this and similar changes throughout the PR please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's related to this change in buildable:

          def query_criteria(object, base)
            crit = klass.criteria                        # <== this line
            crit = crit.apply_scope(scope)
            crit = crit.where(foreign_key => object)
            with_polymorphic_criterion(crit, base)
          end

We're now explicitly creating the criteria first line, so the call doesn't happen on the Person class, it happens on a Criteria object.

@p-mongo
Copy link
Contributor

p-mongo commented Aug 25, 2021

Overall this looks to be very thorough, thank you for this PR.

AR defines its association thusly:

activerecord/lib/active_record/associations.rb:      def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)

The scope argument was added in 4.0. Previously the definition looked like this:

activerecord/lib/active_record/associations.rb:      def has_and_belongs_to_many(name, options = {}, &extension)

I imagine such a change would be a breaking one, hence I see the value in using a keyword argument for scope. At the same time it should be relatively straightforward to support both invocations by examining the type of scope and if it's a Hash, treating it as options. It is my impression that this compatibility mechanism would continue to work even with the double splat syntax and Ruby 3 which is what Rails currently has in its codebase:

activerecord/lib/active_record/associations.rb:        def has_and_belongs_to_many(name, scope = nil, **options, &extension)

@johnnyshields
Copy link
Contributor Author

johnnyshields commented Aug 25, 2021

@p-mongo I am intentionally not matching ActiveRecord's "second arg" syntax. scope will be used quite rarely, there is no reason to give it such premium treatment--it should be an option like any other.

There is no strict requirement in Mongoid that we exactly match ActiveRecord's decisions; we should be free to deviate where it makes sense.

@johnnyshields
Copy link
Contributor Author

@comandeo ready for your review.

@p-mongo
Copy link
Contributor

p-mongo commented Sep 6, 2021

@comandeo What is your opinion on the question of whether Mongoid should match AR behavior and accept the scope as a positional argument? I see three options at this point:

Positional argument only, matches AR behavior.

Keyword argument only. This is the current implementation in this PR. Not compatible with AR in either direction.

Both keyword argument and positional argument are allowed. Compatible with AR in that AR-targeting code will work with Mongoid, but permits also specifying all association options as keyword arguments which I can see how this can be more consistent.

@comandeo
Copy link
Contributor

comandeo commented Sep 6, 2021

I think in this case it is better to be consistent and treat all arguments similarly. I agree that we should be as close as possible to AR in general; however, supporting both ways here won't bring us benefits, actually.

@johnnyshields
Copy link
Contributor Author

@comandeo the AR syntax for this option in is quite ugly. It really doesn't make sense for scope to be a second argument, especially because it comes with significant caveats. I'd like to keep it as I have in this PR.

@johnnyshields
Copy link
Contributor Author

johnnyshields commented Sep 6, 2021

Well hmmm... on second thought its ok. I guess we can keep consistent with AR since all the other keys match exactly. I'll change it.

@johnnyshields
Copy link
Contributor Author

johnnyshields commented Sep 6, 2021

I take back my previous comment. Having it as a positional arg makes the code messier. Let's keep it as a keyword arg, think it's cleaner.

Another reason for it being a keyword is that it's not present on embedded relations, which do not exist in AR.

@comandeo
Copy link
Contributor

comandeo commented Sep 6, 2021

I think we should merge this as soon as CI is green. @johnnyshields I gave the Evergreen a kick, and all configurations (except app tests) fail with the following:

   1) Mongoid::Criteria::Scopable#apply_scope when the scope is a Criteria when standard scope merges the criteria
       Failure/Error: raise Errors::InvalidIncludes.new(_parent_class, [ relation_object ]) unless association
       
       Mongoid::Errors::InvalidIncludes:
       
         message:
           Invalid includes directive: Band.includes(:drugs)
         summary:
           Eager loading in Mongoid only supports providing arguments to Band.includes that are the names of associations on the Band.
         resolution:
           Ensure that each parameter passed to Band.includes is a valid name of an association on the Band model. These are: "records", "notes", "labels", "label", "same_name".
       # ./lib/mongoid/criteria/includable.rb:81:in `block in extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:71:in `each'
       # ./lib/mongoid/criteria/includable.rb:71:in `extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:29:in `includes'
       # ./spec/mongoid/criteria/scopable_spec.rb:135:in `block (5 levels) in <top (required)>'
       # ./spec/mongoid/criteria/scopable_spec.rb:128:in `block (3 levels) in <top (required)>'
       # ./spec/mongoid/criteria/scopable_spec.rb:139:in `block (5 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:68:in `block (3 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:67:in `block (2 levels) in <top (required)>'
 
 
    2) Mongoid::Criteria::Scopable#apply_scope when the scope is a Criteria when unscoped unscoped has no effect
       Failure/Error: raise Errors::InvalidIncludes.new(_parent_class, [ relation_object ]) unless association
       
       Mongoid::Errors::InvalidIncludes:
       
         message:
           Invalid includes directive: Band.includes(:drugs)
         summary:
           Eager loading in Mongoid only supports providing arguments to Band.includes that are the names of associations on the Band.
         resolution:
           Ensure that each parameter passed to Band.includes is a valid name of an association on the Band model. These are: "records", "notes", "labels", "label", "same_name".
       # ./lib/mongoid/criteria/includable.rb:81:in `block in extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:71:in `each'
       # ./lib/mongoid/criteria/includable.rb:71:in `extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:29:in `includes'
       # ./spec/mongoid/criteria/scopable_spec.rb:147:in `block (5 levels) in <top (required)>'
       # ./spec/mongoid/criteria/scopable_spec.rb:128:in `block (3 levels) in <top (required)>'
       # ./spec/mongoid/criteria/scopable_spec.rb:151:in `block (5 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:68:in `block (3 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:67:in `block (2 levels) in <top (required)>'
 
 
    3) Mongoid::Criteria::Scopable#apply_scope when the scope is a Proc when standard scope adds the scope
       Failure/Error: raise Errors::InvalidIncludes.new(_parent_class, [ relation_object ]) unless association
       
       Mongoid::Errors::InvalidIncludes:
       
         message:
           Invalid includes directive: Band.includes(:drugs)
         summary:
           Eager loading in Mongoid only supports providing arguments to Band.includes that are the names of associations on the Band.
         resolution:
           Ensure that each parameter passed to Band.includes is a valid name of an association on the Band model. These are: "records", "notes", "labels", "label", "same_name".
       # ./lib/mongoid/criteria/includable.rb:81:in `block in extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:71:in `each'
       # ./lib/mongoid/criteria/includable.rb:71:in `extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:29:in `includes'
       # ./spec/mongoid/criteria/scopable_spec.rb:162:in `block (6 levels) in <top (required)>'
       # ./lib/mongoid/criteria/scopable.rb:35:in `instance_exec'
       # ./lib/mongoid/criteria/scopable.rb:35:in `apply_scope'
       # ./spec/mongoid/criteria/scopable_spec.rb:128:in `block (3 levels) in <top (required)>'
       # ./spec/mongoid/criteria/scopable_spec.rb:166:in `block (5 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:68:in `block (3 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:67:in `block (2 levels) in <top (required)>'
 
 
    4) Mongoid::Criteria::Scopable#apply_scope when the scope is a Proc when unscoped removes existing scopes then adds the new scope
       Failure/Error: raise Errors::InvalidIncludes.new(_parent_class, [ relation_object ]) unless association
       
       Mongoid::Errors::InvalidIncludes:
       
         message:
           Invalid includes directive: Band.includes(:drugs)
         summary:
           Eager loading in Mongoid only supports providing arguments to Band.includes that are the names of associations on the Band.
         resolution:
           Ensure that each parameter passed to Band.includes is a valid name of an association on the Band model. These are: "records", "notes", "labels", "label", "same_name".
       # ./lib/mongoid/criteria/includable.rb:81:in `block in extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:71:in `each'
       # ./lib/mongoid/criteria/includable.rb:71:in `extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:29:in `includes'
       # ./spec/mongoid/criteria/scopable_spec.rb:174:in `block (6 levels) in <top (required)>'
       # ./lib/mongoid/criteria/scopable.rb:35:in `instance_exec'
       # ./lib/mongoid/criteria/scopable.rb:35:in `apply_scope'
       # ./spec/mongoid/criteria/scopable_spec.rb:128:in `block (3 levels) in <top (required)>'
       # ./spec/mongoid/criteria/scopable_spec.rb:178:in `block (5 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:68:in `block (3 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:67:in `block (2 levels) in <top (required)>'
 
 
    5) Mongoid::Criteria::Scopable#apply_scope when the scope is a Symbol when standard scope adds the scope
       Failure/Error: super
       
       NoMethodError:
         undefined method `highly_rated' for #<Mongoid::Criteria
           selector: {"name"=>"Black Sabbath"}
           options:  {:skip=>20}
           class:    Band
           embedded: false>
       # ./lib/mongoid/criteria.rb:529:in `method_missing'
       # ./lib/mongoid/criteria/scopable.rb:37:in `apply_scope'
       # ./spec/mongoid/criteria/scopable_spec.rb:128:in `block (3 levels) in <top (required)>'
       # ./spec/mongoid/criteria/scopable_spec.rb:193:in `block (5 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:68:in `block (3 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:67:in `block (2 levels) in <top (required)>'
 
 
    6) Mongoid::Criteria::Scopable#apply_scope when model has default_scope when standard scope merges the scope
       Failure/Error: raise Errors::InvalidIncludes.new(_parent_class, [ relation_object ]) unless association
       
       Mongoid::Errors::InvalidIncludes:
       
         message:
           Invalid includes directive: Band.includes(:drugs)
         summary:
           Eager loading in Mongoid only supports providing arguments to Band.includes that are the names of associations on the Band.
         resolution:
           Ensure that each parameter passed to Band.includes is a valid name of an association on the Band model. These are: "records", "notes", "labels", "label", "same_name".
       # ./lib/mongoid/criteria/includable.rb:81:in `block in extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:71:in `each'
       # ./lib/mongoid/criteria/includable.rb:71:in `extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:29:in `includes'
       # ./spec/mongoid/criteria/scopable_spec.rb:224:in `block (6 levels) in <top (required)>'
       # ./lib/mongoid/criteria/scopable.rb:35:in `instance_exec'
       # ./lib/mongoid/criteria/scopable.rb:35:in `apply_scope'
       # ./spec/mongoid/criteria/scopable_spec.rb:128:in `block (3 levels) in <top (required)>'
       # ./spec/mongoid/criteria/scopable_spec.rb:228:in `block (5 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:68:in `block (3 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:67:in `block (2 levels) in <top (required)>'
 
 
    7) Mongoid::Criteria::Scopable#apply_scope when model has default_scope when unscoped removes existing scopes then adds the new scope
       Failure/Error: raise Errors::InvalidIncludes.new(_parent_class, [ relation_object ]) unless association
       
       Mongoid::Errors::InvalidIncludes:
       
         message:
           Invalid includes directive: Band.includes(:drugs)
         summary:
           Eager loading in Mongoid only supports providing arguments to Band.includes that are the names of associations on the Band.
         resolution:
           Ensure that each parameter passed to Band.includes is a valid name of an association on the Band model. These are: "records", "notes", "labels", "label", "same_name".
       # ./lib/mongoid/criteria/includable.rb:81:in `block in extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:71:in `each'
       # ./lib/mongoid/criteria/includable.rb:71:in `extract_includes_list'
       # ./lib/mongoid/criteria/includable.rb:29:in `includes'
       # ./spec/mongoid/criteria/scopable_spec.rb:236:in `block (6 levels) in <top (required)>'
       # ./lib/mongoid/criteria/scopable.rb:35:in `instance_exec'
       # ./lib/mongoid/criteria/scopable.rb:35:in `apply_scope'
       # ./spec/mongoid/criteria/scopable_spec.rb:128:in `block (3 levels) in <top (required)>'
       # ./spec/mongoid/criteria/scopable_spec.rb:240:in `block (5 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:68:in `block (3 levels) in <top (required)>'
       # ./spec/lite_spec_helper.rb:67:in `block (2 levels) in <top (required)>'

It looks like something is missing there. Could you please take a look? Thank you!

@johnnyshields
Copy link
Contributor Author

@comandeo fixed!

@comandeo comandeo requested a review from p-mongo September 10, 2021 09:17
p added 3 commits September 22, 2021 15:23
* master:
  MONGOID-5151 Respect aliased fields in pluck/distinct by having Document.database_field_name recursively consider embedded docs (mongodb#5047)
  RUBY-2675 test coverage on the Mongoid side (mongodb#5030)
  MONGOID-4592 Add examples of using read_attribute and write_attribute to implement custom field behavior (mongodb#5082)
  MONGOId-5185 Remove tests for _id serialization (mongodb#5081)
  MONGOID-5103 Implement eq symbol operator (mongodb#5076)
  Fix MONGOID-5006 Link default auth source documentation to driver instead of incorrectly claiming "admin" is always the default (mongodb#5048)
  Create security policy following github's template (mongodb#5070)
@p-mongo
Copy link
Contributor

p-mongo commented Sep 22, 2021

I removed the link to named scopes because it was broken and I couldn't find anywhere in our docs where we actually document them. @johnnyshields where was that supposed to go?

@p-mongo p-mongo changed the title MONGOID-5128: [Ready for review] Scoped associations MONGOID-5128 Scoped associations Sep 22, 2021
@johnnyshields
Copy link
Contributor Author

Named scopes was supposed to link to here: https://docs.mongodb.com/mongoid/current/tutorials/mongoid-queries/#named-scopes

@p-mongo p-mongo merged commit ee710c1 into mongodb:master Sep 23, 2021
p-mongo pushed a commit to johnnyshields/mongoid that referenced this pull request Sep 23, 2021
* master: (26 commits)
  MONGOID-5128 Scoped associations (mongodb#5017)
  MONGOID-5005 - .sum, .count, and similar aggregables now ignore sort if not limiting/skipping (mongodb#5049)
  MONGOID-5098 Improve specs for timezone handling (specs only; no behavior change) (mongodb#5023)
  MONGOID-5151 Respect aliased fields in pluck/distinct by having Document.database_field_name recursively consider embedded docs (mongodb#5047)
  RUBY-2675 test coverage on the Mongoid side (mongodb#5030)
  MONGOID-4592 Add examples of using read_attribute and write_attribute to implement custom field behavior (mongodb#5082)
  MONGOId-5185 Remove tests for _id serialization (mongodb#5081)
  MONGOID-5103 Implement eq symbol operator (mongodb#5076)
  Fix MONGOID-5006 Link default auth source documentation to driver instead of incorrectly claiming "admin" is always the default (mongodb#5048)
  Create security policy following github's template (mongodb#5070)
  MONGOID-5131 Use proper mdb <-> ubuntu versions (mongodb#5067)
  MONGOID-5131: Set up build pipeline for outside contributors (mongodb#5043)
  MONGOID-5165 Fix documentation link (mongodb#5065)
  Fix indentation
  MONGOID-5029 #empty method should use an #exists? rather than a #count query (mongodb#5029)
  MONGOID-5177 Fix flaky contextual spec (mongodb#5064)
  MONGOID-5170 - Fix more typos mostly in code docs and code comments (mongodb#5056)
  MONGOID-5105 Allow block form in Mongoid::Association::EmbedsMany::Proxy#count  (mongodb#5060)
  MONGOID-5162 Add a release note for the planned changes in ObjectId#as_json (mongodb#5059)
  MONGOID-5171 - Make the minimum Ruby version 2.5 (mongodb#5058)
  ...
p-mongo pushed a commit to johnnyshields/mongoid that referenced this pull request Sep 23, 2021
* master:
  MONGOID-5128 Scoped associations (mongodb#5017)
  MONGOID-5005 - .sum, .count, and similar aggregables now ignore sort if not limiting/skipping (mongodb#5049)
  MONGOID-5098 Improve specs for timezone handling (specs only; no behavior change) (mongodb#5023)
  MONGOID-5151 Respect aliased fields in pluck/distinct by having Document.database_field_name recursively consider embedded docs (mongodb#5047)
  RUBY-2675 test coverage on the Mongoid side (mongodb#5030)
  MONGOID-4592 Add examples of using read_attribute and write_attribute to implement custom field behavior (mongodb#5082)
  MONGOId-5185 Remove tests for _id serialization (mongodb#5081)
  MONGOID-5103 Implement eq symbol operator (mongodb#5076)
  Fix MONGOID-5006 Link default auth source documentation to driver instead of incorrectly claiming "admin" is always the default (mongodb#5048)
  Create security policy following github's template (mongodb#5070)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
tracked-in-jira Ticket filed in Mongo's Jira system
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants