@@ -454,6 +454,75 @@ def pluck(*fields)
454
454
end
455
455
end
456
456
457
+ # Get a hash of counts for the values of a single field. For example,
458
+ # if the following documents were in the database:
459
+ #
460
+ # { _id: 1, age: 21 }
461
+ # { _id: 2, age: 21 }
462
+ # { _id: 3, age: 22 }
463
+ #
464
+ # Model.tally("age")
465
+ #
466
+ # would yield the following result:
467
+ #
468
+ # { 21 => 2, 22 => 1 }
469
+ #
470
+ # When tallying a field inside an array or embeds_many association:
471
+ #
472
+ # { _id: 1, array: [ { x: 1 }, { x: 2 } ] }
473
+ # { _id: 2, array: [ { x: 1 }, { x: 2 } ] }
474
+ # { _id: 3, array: [ { x: 1 }, { x: 3 } ] }
475
+ #
476
+ # Model.tally("array.x")
477
+ #
478
+ # The keys of the resulting hash are arrays:
479
+ #
480
+ # { [ 1, 2 ] => 2, [ 1, 3 ] => 1 }
481
+ #
482
+ # Note that if tallying an element in an array of hashes, and the key
483
+ # doesn't exist in some of the hashes, tally will not include those
484
+ # nil keys in the resulting hash:
485
+ #
486
+ # { _id: 1, array: [ { x: 1 }, { x: 2 }, { y: 3 } ] }
487
+ #
488
+ # Model.tally("array.x")
489
+ # # => { [ 1, 2 ] => 1 }
490
+ #
491
+ # @param [ String | Symbol ] field The field name.
492
+ #
493
+ # @return [ Hash ] The hash of counts.
494
+ def tally ( field )
495
+ name = klass . cleanse_localized_field_names ( field )
496
+
497
+ fld = klass . traverse_association_tree ( name )
498
+ pipeline = [ { "$group" => { _id : "$#{ name } " , counts : { "$sum" : 1 } } } ]
499
+ pipeline . unshift ( "$match" => view . filter ) unless view . filter . blank?
500
+
501
+ collection . aggregate ( pipeline ) . reduce ( { } ) do |tallies , doc |
502
+ is_translation = "#{ name } _translations" == field . to_s
503
+ val = doc [ "_id" ]
504
+
505
+ key = if val . is_a? ( Array )
506
+ val . map do |v |
507
+ demongoize_with_field ( fld , v , is_translation )
508
+ end
509
+ else
510
+ demongoize_with_field ( fld , val , is_translation )
511
+ end
512
+
513
+ # The only time where a key will already exist in the tallies hash
514
+ # is when the values are stored differently in the database, but
515
+ # demongoize to the same value. A good example of when this happens
516
+ # is when using localized fields. While the server query won't group
517
+ # together hashes that have other values in different languages, the
518
+ # demongoized value is just the translation in the current locale,
519
+ # which can be the same across multiple of those unequal hashes.
520
+ tallies [ key ] ||= 0
521
+ tallies [ key ] += doc [ "counts" ]
522
+ tallies
523
+ end
524
+ end
525
+
457
526
# Skips the provided number of documents.
458
527
#
459
528
# @example Skip the documents.
@@ -695,6 +764,26 @@ def acknowledged_write?
695
764
collection . write_concern . nil? || collection . write_concern . acknowledged?
696
765
end
697
766
767
+ # Fetch the element from the given hash and demongoize it using the
768
+ # given field. If the obj is an array, map over it and call this method
769
+ # on all of its elements.
770
+ #
771
+ # @param [ Hash | Array<Hash> ] obj The hash or array of hashes to fetch from.
772
+ # @param [ String ] meth The key to fetch from the hash.
773
+ # @param [ Field ] field The field to use for demongoization.
774
+ #
775
+ # @return [ Object ] The demongoized value.
776
+ #
777
+ # @api private
778
+ def fetch_and_demongoize ( obj , meth , field )
779
+ if obj . is_a? ( Array )
780
+ obj . map { |doc | fetch_and_demongoize ( doc , meth , field ) }
781
+ else
782
+ res = obj . try ( :fetch , meth , nil )
783
+ field ? field . demongoize ( res ) : res . class . demongoize ( res )
784
+ end
785
+ end
786
+
698
787
# Extracts the value for the given field name from the given attribute
699
788
# hash.
700
789
#
@@ -703,21 +792,13 @@ def acknowledged_write?
703
792
#
704
793
# @param [ Object ] The value for the given field name
705
794
def extract_value ( attrs , field_name )
706
- def fetch_and_demongoize ( d , meth , klass )
707
- res = d . try ( :fetch , meth , nil )
708
- if field = klass . fields [ meth ]
709
- field . demongoize ( res )
710
- else
711
- res . class . demongoize ( res )
712
- end
713
- end
714
-
715
795
i = 1
716
796
num_meths = field_name . count ( '.' ) + 1
717
797
k = klass
718
798
curr = attrs . dup
719
799
720
800
klass . traverse_association_tree ( field_name ) do |meth , obj , is_field |
801
+ field = obj if is_field
721
802
is_translation = false
722
803
# If no association or field was found, check if the meth is an
723
804
# _translations field.
@@ -737,18 +818,18 @@ def fetch_and_demongoize(d, meth, klass)
737
818
# value so the full hash is returned.
738
819
# 4. Otherwise, fetch and demongoize the value for the key meth.
739
820
curr = if curr . is_a? Array
740
- res = curr . map { | x | fetch_and_demongoize ( x , meth , k ) }
821
+ res = fetch_and_demongoize ( curr , meth , field )
741
822
res . empty? ? nil : res
742
- elsif !is_translation && k . fields [ meth ] &.localized?
823
+ elsif !is_translation && field &.localized?
743
824
if i < num_meths
744
825
curr . try ( :fetch , meth , nil )
745
826
else
746
- fetch_and_demongoize ( curr , meth , k )
827
+ fetch_and_demongoize ( curr , meth , field )
747
828
end
748
829
elsif is_translation
749
830
curr . try ( :fetch , meth , nil )
750
831
else
751
- fetch_and_demongoize ( curr , meth , k )
832
+ fetch_and_demongoize ( curr , meth , field )
752
833
end
753
834
754
835
# If it's a relation, update the current klass with the relation klass.
@@ -771,12 +852,28 @@ def fetch_and_demongoize(d, meth, klass)
771
852
# @return [ Object ] The demongoized value.
772
853
def recursive_demongoize ( field_name , value , is_translation )
773
854
field = klass . traverse_association_tree ( field_name )
855
+ demongoize_with_field ( field , value , is_translation )
856
+ end
774
857
858
+ # Demongoize the value for the given field. If the field is nil or the
859
+ # field is a translations field, the value is demongoized using its class.
860
+ #
861
+ # @param [ Field ] field The field to use to demongoize.
862
+ # @param [ Object ] value The value to demongoize.
863
+ # @param [ Boolean ] is_translation The field we are retrieving is an
864
+ # _translations field.
865
+ #
866
+ # @return [ Object ] The demongoized value.
867
+ #
868
+ # @api private
869
+ def demongoize_with_field ( field , value , is_translation )
775
870
if field
776
871
# If it's a localized field that's not a hash, don't demongoize
777
872
# again, we already have the translation. If it's an _translations
778
873
# field, don't demongoize, we want the full hash not just a
779
874
# specific translation.
875
+ # If it is a hash, and it's not a transaltions field, we need to
876
+ # demongoize to get the correct translation.
780
877
if field . localized? && ( !value . is_a? ( Hash ) || is_translation )
781
878
value . class . demongoize ( value )
782
879
else
0 commit comments