Default value for Mongoid Hash field accessor - ruby-on-rails

Given a Mongoid model:
class Counts
include Mongoid::Document
# lists of tag counts
field :tags, type: Hash, default: {}
end
c = Counts.new( tags = {new: 12, old: 7})
I would like to override c#tags[] so that if a key isn't set on the tags field, it should return a default of 0 instead of nil like this:
c.tags['ancient']
# => 0

Try setting default hash values as below:
class Counts
...
field :tags, type: Hash, default: Hash.new{ |h, k| h[k] = 0 }
end

Related

Rails jsonb columns in graphql

I am trying to set up types for my 'A' model that uses store_accessor. I am not sure how to define the column that has the JSON column - :foo
class A < ActiveRecord
store_accessor :foo, :bar, :baz
end
Types::AType = GraphQL::ObjectType.define do
name ‘A’
field: id, !types.ID
field :bar, !types.String, hash_key: :bar
field :baz, !types.String, hash_key: :baz
end
Create a custom ScalarType called JSONType
JSONType = GraphQL::ScalarType.define do
name "JSON Type"
coerce_input -> (value) { JSON.parse(value) }
coerce_result -> (value) { value }
end
And consume it in ObjectType definition
field :foo, JSONType do
resolve Resolvers::Foo.new
end

How to get the string version of an enum?

I have a model enum like:
class User < AR::Base
enum status [:pending, :member, :banned]
end
Now I want to output the string value of 'banned' but it outputs the int value:
User.statuses[:banned]
I'm not sure that's how they work. Looking at some docs:
http://api.rubyonrails.org/classes/ActiveRecord/Enum.html
You would have something like
# User.status = 2
User.status = "banned"
This is something of a non-answer, but the question belies the implementation of ActiveRecord::Enum:
# to get the string value for User.statuses[:banned]…
"banned"
# or
:banned.to_s
# to get the string value for all values in the User.statuses enum…
User.statuses.keys
# => ["pending", "member", "banned"]
The key isn't the important part here, really. All Rails is doing is taking the array of symbols you give it here…
enum status: [:pending, :member, :banned]
…and assigning it to a hash with incremented integer values while providing you a bunch of convenient methods for accessing the value:
user.status #=> 'pending'
user.pending? #=> true
You can verify this if you like…
User.defined_enums.class #=> Hash
User.defined_enums
#=> { "status" => { "pending" => 0, "member" => 1, "banned" => 2 } }

Rails/mongoid: Advanced querying of arrays

Im stuck with an advanced query in rails. I need a solution that works in mongoid and if possible also active record (probably not possible). I've put together a simplified example below.
Consider the following model:
class Announcement
include Mongoid::Document
field :title, type: String
field :user_group, type: Array
field :year, type: Array
field :tags, type: Array
has_and_belongs_to_many :subjects
before_save :generate_tags
private
def generate_tags
tags = []
if self.subjects.present?
self.subjects.each { |x| tags << x.name.downcase.gsub(" ", "_") }
end
if self.year.present?
self.year.each { |x| tags << "year_" + x.to_s }
end
self.tags = tags
end
end
Given the tags array of document 1:
["hsc_mathematics", "hsc_chemistry", "year_9"]
And document 2:
["hsc_mathematics", "hsc_chemistry"]
And document 3:
["hsc_mathematics", "hsc_chemistry", "year_9", "year_10"]
And document 4:
["year_9", "year_10"]
Now consider the following model:
class Student < User
include Mongoid::Document
field :year, type: Integer
has_many :subjects
def announcements
tags = []
if self.subjects.present?
self.subjects.each { |x| subjects << x.name.downcase.gsub(" ", "_") }
end
tags << "year_" + self.year.to_s
Announcement.where("user_group" => { "$in" => ["Student", "all_groups"]}).any_of({"tags" => { "$in" => tags }}, {tags: []})
end
end
For the purpose of our example our student has the following tags:
[ "hsc_mathematics", "hsc_physics", "year_10" ]
My query is incorrect as I want to return documents 2, 3 and 4 but not document 1.
I need the query to adhere to the following when returning announcements:
i. If the announcement has subject tags match on any subject
ii. If the announcement has year tags match on any year
iii. If announcement has year and subject tags match on any year and any subject
How would I go about writing this?
EDIT
Im happy to split year out of my tags but im still stuck
Announcement.where("user_group" => { "$in" => ["Student", "all_groups"]}).any_of({"tags" => { "$in" => ["hsc_mathematics", "hsc_physics"] }}, {tags: []}).any_of({"year_at_school" => { "$in" => 10 }}, {year_at_school: []})
So the solution was to adjust my models and use a more organised query rather then an entire tag bank.
Announcement model:
class Announcement
include Mongoid::Document
field :title, type: String
field :user_group, type: Array, default: [""]
field :year, type: Array, default: [""]
field :tags, type: Array, default: [""]
has_and_belongs_to_many :subjects
before_save :generate_tags
private
def generate_tags
tags = []
if self.subjects.present?
self.subjects.each { |x| tags << x.name.downcase.gsub(" ", "_") }
end
self.tags = tags
end
end
User model:
class Student < User
include Mongoid::Document
field :year, type: Integer
has_many :subjects
def announcements
year = "year_" + self.year.to_s
tags = [""]
if self.subjects.present?
self.subjects.each { |x| tags << x.name.downcase.gsub(" ", "_") }
end
Announcement.where("user_group" => { "$in" => ["Student", ""] }).and("year" => { "$in" => [year, ""]}).any_in(tags: tags).all.entries
end
end
EDIT: Heres a neater version of the query as suggested
This example also has an expiry field which assumes nil = never expires
Announcement.where(:user_group.in => ["Student", ""], :year.in => [year, ""], :tags.in => tags).any_of({:expires_at.gte => Time.zone.now}, {:expires_at => nil}).all.entries

Have to_json return a mongoid as a string

In my Rails API, I'd like a Mongo object to return as a JSON string with the Mongo UID as an "id" property rather than as an "_id" object.
I want my API to return the following JSON:
{
"id": "536268a06d2d7019ba000000",
"created_at": null,
}
Instead of:
{
"_id": {
"$oid": "536268a06d2d7019ba000000"
},
"created_at": null,
}
My model code is:
class Profile
include Mongoid::Document
field :name, type: String
def to_json(options={})
#what to do here?
# options[:except] ||= :_id #%w(_id)
super(options)
end
end
You can monkey patch Moped::BSON::ObjectId:
module Moped
module BSON
class ObjectId
def to_json(*)
to_s.to_json
end
def as_json(*)
to_s.as_json
end
end
end
end
to take care of the $oid stuff and then Mongoid::Document to convert _id to id:
module Mongoid
module Document
def serializable_hash(options = nil)
h = super(options)
h['id'] = h.delete('_id') if(h.has_key?('_id'))
h
end
end
end
That will make all of your Mongoid objects behave sensibly.
For guys using Mongoid 4+ use this,
module BSON
class ObjectId
alias :to_json :to_s
alias :as_json :to_s
end
end
Reference
You can change the data in as_json method, while data is a hash:
class Profile
include Mongoid::Document
field :name, type: String
def as_json(*args)
res = super
res["id"] = res.delete("_id").to_s
res
end
end
p = Profile.new
p.to_json
result:
{
"id": "536268a06d2d7019ba000000",
...
}
Use for example:
user = collection.find_one(...)
user['_id'] = user['_id'].to_s
user.to_json
this return
{
"_id": "54ed1e9896188813b0000001"
}
class Profile
include Mongoid::Document
field :name, type: String
def to_json
as_json(except: :_id).merge(id: id.to_s).to_json
end
end
If you don't want to change default behavior of MongoId, just convert result of as_json.
profile.as_json.map{|k,v| [k, v.is_a?(BSON::ObjectId) ? v.to_s : v]}.to_h
Also, this convert other BSON::ObjectId like user_id.
# config/initializers/mongoid.rb
# convert object key "_id" to "id" and remove "_id" from displayed attributes on mongoid documents when represented as JSON
module Mongoid
module Document
def as_json(options={})
attrs = super(options)
id = {id: attrs["_id"].to_s}
attrs.delete("_id")
id.merge(attrs)
end
end
end
# converts object ids from BSON type object id to plain old string
module BSON
class ObjectId
alias :to_json :to_s
alias :as_json :to_s
end
end

Mapping hash maps to class instances

Looking for gem or at least idea how to approach this problem, the ones I have are not exactly elegant :)
Idea is simple I would like to map hashes such as:
{ :name => 'foo',
:age => 15,
:job => {
:job_name => 'bar',
:position => 'something'
...
}
}
To objects of classes (with flat member structure) or Struct such as:
class Person {
#name
#age
#job_name
...
}
Thanks all.
Assuming that you can be certain sub-entry keys won't conflict with containing entry keys, here's some code that should work...
require 'ostruct'
def flatten_hash(hash)
hash = hash.dup
hash.entries.each do |k,v|
next unless v.is_a?(Hash)
v = flatten_hash(v)
hash.delete(k)
hash.merge! v
end
hash
end
def flat_struct_from_hash(hash)
hash = flatten_hash(hash)
OpenStruct.new(hash)
end
Solution that I used it solves problem with same key names but it does not give flat class structure. Somebody might find this handy just keep in mind that values with reserved names such as id, type need to be handled.
require 'ostruct'
def to_open_struct(element)
struct = OpenStruct.new
element.each do |k,v|
value = Hash === v ? to_open_struct(v) : v
eval("object.#{k}=value")
end
return struct
end
An alternate answer where you know the keys before hand
class Job
attr_accessor :job_name, :position
def initialize(params = {})
self.job_name = params.fetch(:job_name, nil)
self.position = params.fetch(:position, nil)
end
end
class Person
attr_accessor :name, :age, :job
def initialize(params = {})
self.name = params.fetch(:name, nil)
self.age = params.fetch(:age, nil)
self.job = Job.new(params.fetch(:job, {}))
end
end
hash = { :name => 'foo', :age => 1, :job => { :job_name => 'bar', :position => 'soetmhing' }}
p = Person.new(hash)
p.name
==> "foo"
p.job
==> #<Job:0x96cacd8 #job_name="bar", #position="soetmhing">
p.job.name
==> "bar"

Resources